Search code examples
c++templatesabstract-classallocationc++98

Avoid allocation of abstract type with templates


The context: I wrote some tools for archiving data, in a similar way of archives from boost. Then, as example, I can write this kind of code :

class A
{
  private:
    double a;
  public:
    A() : a(3.14159)
    {}
    A(const A& a_) : a(a_.a) {}
    virtual ~A()
    {}
    virtual A* clone() const = 0; // Then, A is virtual

    virtual void save(O_Archive& oa) const  //
    {                                       // 
      oa << this->a;                        // INTERESTING
    }                                       // PART OF THE
    virtual void load(I_archive& ia)        //    CLASS
    {                                       //
      ia >> this->a;                        //
    }                                       //
};
O_Archive& operator << (O_Archive& oa, const A& a)
{
  a.save(oa);
  return oa;
}
I_Archive& operator >> (I_Archive& ia, A& a)
{
  a.load(ia);
  return ia;
}

class B : public A
{
  private:
    double b;
  public:
    B() : A(), b(1.0) {}
    B(const B& b_) : A(b_), b(b_.b) {}
    virtual ~B() {}
    virtual A* clone() const
    {
      return new B(*this);
    }

    void save(O_Archive& oa) const  //
    {                               //
      this->A::save(oa);            //
      oa << this->b;                // INTERESTING
    }                               // PART OF THE
    void load(I_Archive& ia)        //    CLASS
    {                               //
      this->A::load(ia);            //
      ia >> this->b;                //
    }                               //
};

// Consider classes 'C' and 'D' similar to 'B'

void example_save(O_Archive& oa)
{
  A* p1 = new B;
  A* p2 = new C;
  D* p3 = new D;
  oa << Archive::declare_derived<A,B,C,D>();
  oa << p1 << p2; // Automatically detect the inheritance
  oa << p3; // Store the instance as a usual pointer
}
void example_load(I_Archive& ia)
{
  A* p1 = 0;
  A* p2 = 0;
  B* p3 = 0;
  ia << Archive::declare_derived<A,B,C,D>();
  ia >> p1 >> p2;
  ia >> p3;
}

Where is the problem ? This works with several functions like the following load_pointer function in the class I_Archive in charge of checking if the pointer was allocated, if it was an instance with a derived type, or simply a usual pointer.

template <typename T>
void I_Archive::load_pointer(T*& p)
{
    delete p;
    bool allocated;
    this->load_bool(allocated);
    if(allocated)
    {
        bool inheriance;
        this->load_bool(inheriance);
        if(inheriance)
        {
            unsigned long int i;
            this->load_unsigned_long_int(i);
            p = boost::static_pointer_cast< const Archive::allocator<T> >(this->alloc[&typeid(T)][i])->allocate();
        }
        else
            p = new T;  // ERROR AT THIS LINE
        *this >> *p;
    }
    else
        p = 0;
}

My problem: Actually, my code doesn't compile with the following error on the line p = new T; :

error: cannot allocate an object of abstract type ‘A’.

I was first surprised, but I really well understand why I have this error : when the function load_pointer is called on p1, the instruction new T becomes new A which is forbidden, even if the instruction is never run if the type is abstract.

My question: I can't find a way to correctly use templates to avoid my error. Is there a possible workaround to do that or to say to the compiler "I know what I'm doing, you'll never have to instanciate an abstract type" ?

Important note: I can't work with C++11 for compatibility reason.


Solution

  • The trait you're looking for is std::is_abstract. As you mentioned, you cannot use C++11, but you can use its implementation from boost.

    You can then use is_abstract together with std::enable_if (again, due to your restriction of not using C++11, you can just take the example implementation from here) to implement it similarly to this:

    #include <iostream>
    #include <type_traits>
    
    struct A {
        virtual void f() = 0;
    };
    
    struct B : A {
        void f() override {}
    };
    
    
    template<typename T>
    std::enable_if_t<std::is_abstract<T>::value, T*> allocate()
    {
        return nullptr;
    }
    
    template<typename T>
    std::enable_if_t<!std::is_abstract<T>::value, T*> allocate()
    {
        return new T;
    }
    
    // Test
    template<typename T>
    T* test_alloc()
    {
        return allocate<T>();
    }
    
    int main()
    {
        std::cout << test_alloc<A>() << "\n"; // Outputs nullptr
        std::cout << test_alloc<B>() << "\n"; // Outputs an address
    }
    

    LIVE