Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to store object of different class types into one container in modern c++?

Tags:

c++

I ran into this problem, where I want to store different classes (sharing same interface) into a common container.

Is it posssible to do that in modern C++?

Is this allowed, when I don't want to store objects as pointers? If I have to use pointer, then what should be the recommended or cleaner way to do that?

What should be the correct approach to handle such usecases?

#include <iostream>
#include <string>
#include <vector>

enum class TYPES: int {TYPE1 = 0, TYPE2, INVALID};

class IObj
{
public:
    virtual auto ObjType(void) -> TYPES = 0;
};

class InterfaceObj: IObj
{
public:
    virtual auto Printable(void) -> void = 0;
};

class InterfaceTesla
{
public:
    virtual auto Creatable(void) -> void = 0;
};

class CObj: InterfaceObj
{
private:
    std::string msg;
public:
    CObj()
    {
        msg = "Elon Mask!";
    }
    virtual auto ObjType(void) -> TYPES override
    {
        return TYPES::TYPE1;
    }
    virtual auto Printable(void) -> void override
    {
        std::cout<<msg<<std::endl;
    }
};

class CObjTesla: public CObj, InterfaceTesla
{
private:
    std::string vhc;
public:
    CObjTesla()
    : CObj()
    {
        vhc = "Cybertruck";
    }
    virtual auto ObjType(void) -> TYPES override
    {
        return TYPES::TYPE2;
    }
    virtual auto Creatable(void) -> void override
    {
        std::cout<<vhc<<" was launched by ";
        Printable();
    }
};


int main()
{
    std::vector<CObj> vec; // How am I supposed to declare this container?

    for(auto i = 0; i < 10; i++)
    {
        CObjTesla obj1;
        CObj obj2;
        vec.push_back(static_cast<CObj>(obj1)); // what should be the correct type of the container?
        vec.push_back((obj2));
    }

    for(auto &iter : vec)
    {
        switch(iter.ObjType())
        {
            case TYPES::TYPE1: 
                iter.Printable();
            break;
            case TYPES::TYPE2: 
                auto temp = const_cast<CObjTesla>(iter); //?? what shoud I do here?
                temp.Creatable();
            break;
            case TYPES::INVALID:
            default:
            break;
        }
    }
}
like image 460
kishoredbn Avatar asked Dec 13 '19 06:12

kishoredbn


2 Answers

You can store different object types in a std::variant. If you do so, there is no need to have a common interface and use virtual functions.

Example:

class A
{
    public:
        void DoSomething() { std::cout << "DoSomething from A" << std::endl; }
};

class B
{
    public:
        void DoSomething() { std::cout << "DoSomething from B" << std::endl; }
};

int main()
{
    std::vector< std::variant< A, B > > objects;

    objects.push_back( A{} );
    objects.push_back( B{} );

    for ( auto& obj: objects )
    {
        std::visit( [](auto& object ){ object.DoSomething(); }, obj);
    }
}

But using this solutions can have also drawbacks. Access via std::visit may be slow. Sometimes e.g. gcc generates very bad code in such situations. ( jump table is generated in runtime, no idea why! ). You always call the function via table access which takes some additional time. And storing the objects in std::variant consumes always the size of the biggest class you have in the variant and in addition you need some space for the tag variable inside the variant.

The "old" way is to store raw or better smart-pointers into the vector and simply call via base pointer the common interface functions. The drawback here is the additional vtable pointer in each instance ( which is typically the same size as the tag variable in the std::variant ). The indirection with vtable access to call the function comes also with a ( small ) cost.

Example with smart pointer of base type and vector:

class Interface
{
    public:
        virtual void DoSomething() = 0;
        virtual ~Interface() = default;
};

class A: public Interface
{
    public:
        void DoSomething() override { std::cout << "DoSomething from A" << std::endl; }
        virtual ~A(){ std::cout << "Destructor called for A" << std::endl; }
};

class B: public Interface
{
    public:
        void DoSomething() override { std::cout << "DoSomething from B" << std::endl; }
        virtual ~B(){ std::cout << "Destructor called for B" << std::endl; }
};

int main()
{
    std::vector< std::shared_ptr<Interface>> pointers;

    pointers.emplace_back( std::make_shared<A>() );
    pointers.emplace_back( std::make_shared<B>() );

    for ( auto& ptr: pointers )
    {
        ptr->DoSomething();
    }
}

If std::unique_ptr is sufficient for you, you can use that one. It depends on the need of passing pointers around or not in your design.

Hint: If you are using pointers to base class type never forget to make your destructors virtual! See also: When to use virtual destructors

In your case I would vote to use smart-pointers of base class type in simple vector!

BTW:

virtual auto ObjType(void) -> TYPES

That look ugly to me! No need for auto here as the return type is known before you write the function parameter list. In such cases, where template parameters are need to be deduced to define the return type, it is needed, but not here! Please do not use always auto!

like image 113
Klaus Avatar answered Oct 24 '22 09:10

Klaus


std::unique_ptr

The most common way to hold polymorphic types inside a vector is by using std::unique_ptr:

std::vector<std::unique_ptr<Base>> vec;
vec.push_back(std::make_unique<Derived1>(/*Derived1 params*/));
vec.push_back(std::make_unique<Derived2>(/*Derived2 params*/));

It sounds that you are not happy with this solution as you want to "hide" any use of pointers.

std::reference_wrapper

This is usually not helpful, but in case you need to hold reference to object that you KNOW would outlive the lifetime of the vector, you can use std::reference_wrapper:

Derived1 d1; // outlives the vector
Derived2 d2; // outlives the vector
// ...
// some inner scope or down the stack
    std::vector<std::reference_wrapper<Base>> vec;
    vec.push_back(d1);
    vec.push_back(d2);

This option is not relevant in your case, as the objects that are passed into the vector are local, so they must be copied to and managed by the vector.

Your own Holder Proxy class

In some cases it is reasonable to create your own Holder class that would conceal the use of pointers or even conceal the use of polymorphism.

This can be achieved in your case like this:

class CObjHolder {
    std::unique_ptr<CObj> pobj;
public:
    template<typename T>
    CObjHolder(const T& obj) {
        pobj = std::make_unique<T>(obj);
    }
    const CObj& operator*() const {
        return *pobj;
    }
    CObj& operator*() {
        return *pobj;
    }
    const CObj* operator->() const {
        return pobj.get();
    }
    CObj* operator->() {
        return pobj.get();
    }
};

With your own Holder class you can simply use a vector of Holder objects:

std::vector<CObjHolder> vec;

Adding to the vector would be easy:

for(auto i = 0; i < 10; i++)
{
    CObjTesla obj1;
    CObj obj2;
    vec.push_back(obj1);
    vec.push_back(obj2);
}

To use the objects you would go through operator-> of the Holder:

for(const auto& item : vec)
{
    switch(item->ObjType())
    {
        case OBJ_TYPE::TYPE1: 
            item->Print();
        break;
        case OBJ_TYPE::TYPE2: 
            static_cast<const CObjTesla&>(*item).DoSomething();
            // doable, though better if can avoid checking types
            // and rely more on dynamic or static polymorphism
        break;
        case OBJ_TYPE::INVALID:
        default:
            // error log
        break;
    }
}

http://coliru.stacked-crooked.com/a/2ce9f2246160b95f

One may argue that the use of Holder class is redundant. However the idea of hiding your polymorphic type may have its advantages. The Holder class is serving as a Proxy to the actual implementation.

The proxy may implement some of the functionality that is relevant for the usage of the types that it represents. For example in this case the switch that appears in main may move into the proxy which may then discard the need of direct access of the actual object it holds:

class CObjHolder {
    std::unique_ptr<CObj> pobj;
public:
    template<typename T>
    CObjHolder(const T& obj) {
        pobj = std::make_unique<T>(obj);
    }
    void DoYourThing() const {
        switch(pobj->ObjType())
        {
            case OBJ_TYPE::TYPE1: 
                pobj->Print();
            break;
            case OBJ_TYPE::TYPE2: 
                std::cout << "@@@" << std::endl;
                static_cast<const CObjTesla&>(*pobj).DoSomething();
            break;
            case OBJ_TYPE::INVALID:
            default:
                // error log
            break;
        }
    }
};

http://coliru.stacked-crooked.com/a/e8266e0a356d825a

like image 34
Amir Kirsh Avatar answered Oct 24 '22 10:10

Amir Kirsh