6

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;
        }
    }
}
peterchen
  • 40,917
  • 20
  • 104
  • 186
kishoredbn
  • 2,007
  • 4
  • 28
  • 47
  • Why do you want to avoid using pointers? – Stephen Newell Dec 13 '19 at 06:06
  • It cannot be done without pointers. Storing an instance into the container which has base class as element type, you will get [object slicing](https://stackoverflow.com/a/274634/7478597) (i.e. only the base class part of instance is copied into container). – Scheff's Cat Dec 13 '19 at 06:06
  • @StephenNewell just that sometime pointers are very difficult to handle the life spans. – kishoredbn Dec 13 '19 at 06:09
  • Have you taken a look at `std::unique_ptr` or `std::shared_ptr`? – Stephen Newell Dec 13 '19 at 06:10
  • @StephenNewell yes. I use them, but don't have a comprehensive plan on how to work with smart pointers in this problem. what should be the a clean solution, even if it has to be with smart pointers. – kishoredbn Dec 13 '19 at 06:13
  • 2
    It looks like you're using a tagged union and want `std::variant`, which can be handled with `std::visit` without virtual interfaces. `std::unique_ptr` is the way to go in the other direction. – parktomatomi Dec 13 '19 at 07:23
  • I added a smart pointer example to my answer... if you need further help, feel free to ask! – Klaus Dec 13 '19 at 10:07
  • 1
    @kishoredbn write modern does not imply write ugly. What's the point of all of those `void` input parameters and and `auto` return type for simple types? – Moia Dec 13 '19 at 10:52
  • 1
    @moia I think `auto` for return is just a cleaner way to make codes more readable(obviously, that is not the functional use of `auto` in return type). Just the 4-letter word in front of function name lays function names in fixed indentation making it more readable especially when you're looking at codes in a colorless text-editor. `void` input parameter is an industry-wide recommendation for programming in `c`, for days when you are actually mixing `c` and `.cpp`. [link](https://softwareengineering.stackexchange.com/questions/286490/what-is-the-difference-between-function-and-functionvoid) – kishoredbn Dec 13 '19 at 15:32
  • @kishoredbn you have a point so. – Moia Dec 17 '19 at 08:36

2 Answers2

7

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!

Klaus
  • 24,205
  • 7
  • 58
  • 113
  • Thanks for sharing your thoughts. I am accepting your answer. BTW.. there is no functional use of `auto` as I'm using the code. All I'm trying to do here is make the all the function declaration look consistent for better readability. – kishoredbn Dec 13 '19 at 15:53
  • @kishoredbn: As Moia also remarked, we have a different opinion about readability. :-) But yes, it is a matter of taste... – Klaus Dec 13 '19 at 16:14
  • I think when too many people said so, then I think I should reconsider this choice of code formatting. – kishoredbn Dec 13 '19 at 19:24
4

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

Amir Kirsh
  • 12,564
  • 41
  • 74