binaryguy
binaryguy

Reputation: 1187

Abstract base class using template argument from derived class

I have a base class which provides pure virtual interfaces. I need this to store pointers to derived-class objects in a list of pointers to the base class.

The derived class is created using the template mechanism. The problem is now that if I want to have a virtual interface to return a type which is known only to the derived class, I need to pass it as a template argument as well. This is where the dilemma starts...

template <typename ITEM>
class base {
public:
virtual ITEM* get() = 0;
};

template <typename ITEM>
class derived : public base<ITEM>{
public:
ITEM* get() {...};
};

But when using a template in base I need to know this even when creating a list of base pointers:

base* myList[10] = {derived1, derived2,...}

Of course I don't know that type when I define my list. So I need to get rid of the template in my base class somehow.

EDIT: Got rid of this approach because it wasn't a useful approach at all. So no solution for this issue.

Upvotes: 1

Views: 1282

Answers (1)

Javier Mart&#237;n
Javier Mart&#237;n

Reputation: 2605

The code you write is not valid; there is not a single base type that is then parameterised like in Java, but a number of base<T> types. There is a way to obtain a wrapper for a truly generic object, and it is called "type erasure". It is used, for example in the implementation of boost::any.

Basically, you have a non-template base class with virtual functions, and then you make a template derived class that implements them. Note that the simplified version shown here does not work if you want to have an array of base objects, because base has pure virtual functions and thus cannot be instantiated (and because the T member of the derived type would be sliced off).

struct base;
template<typename T>
struct derived;

struct base {
    virtual ~base();

    // In this class we don't know about T, so we cannot use it
    // Other operations that delegate to the derived class are possible, though
    virtual std::size_t sizeofT() const = 0;
    virtual const std::type_info& typeofT() const = 0;

    // Since all you want is a pointer in "get", you could write it as a void*
    virtual void* getPtr() = 0;

    // Otherwise, we can implement this template function here that calls the virtual.
    // Note that function templates cannot be virtual!
    template<typename U>
    U& getAs() {
        // Verify that the type is the _same_ (no up/downcasts allowed)
        // std::bad_cast is thrown here if U is not the same T used to build this object
        derived<U>& meAsU = dynamic_cast<derived<U>&>(*this);
        return meAsU.obj;
    }
};

template<typename T>
struct derived : public base {
    T obj;
    // A couple of ctors to initialize the object, and the default copy/move ctors/op=
    virtual ~derived();
    derived(const T& o) : obj(o) {}
    derived(T&& o) : obj(std::move(o)) {}

    std::size_t sizeofT() const override {
        return sizeof(T);
    }
    const std::type_info& typeofT() const override {
        return typeid(T);
    }
    void* getPtr() override {
        return static_cast<void*>(&obj);
    }
};

If you want to use the base type directly as a variable, or in an array or container (vector, list, etc.), you need dynamic allocation - there are no two ways around it. You have two choices, which differ on where to place the responsibility for the dynamic allocation:

  • You can use the solution above if you limit yourself to having arrays of pointers to base. E.g. an array of std::unique_ptr<base>. The pointed-to objects would be of type derived<something>.

    base err1; // Error, abstract class (what would it contain?)
    base err2 = derived<int>(2); // Still abstract class, and the int would be sliced off
    std::unique_ptr<base> ok(new derived<int>(3)); // Works
    
    std::vector<std::unique_ptr<base>> objects;
    objects.push_back(std::make_unique(new derived<int>(5)));
    objects.push_back(std::make_unique(new derived<std::string>(2)));
    int& a = objects[0].getAs<int>(); // works
    std::string& b = objects[1].getAs<std::string>(); // works too
    std::string& bad = objects[1].getAs<double>(); // exception thrown
    
  • Otherwise, you would have to implement the dynamic allocation in the base/derived classes themselves. This is what classes like boost::any or std::function do. The simplest any object would simply be a wrapper of an unique-ptr of the base class I showed here, with appropriate implementations of operators, etc. Then, you can have a variable of type any x = y; and the class would, inside its constructor, do the required new derived<Y>(y) required.

Upvotes: 1

Related Questions