0

I was going through the disadvantages of vtable-based polymorphism.

  1. Vtable Overhead: Each class with virtual functions needs to have a vtable, which contains function pointers to its virtual functions. This can increase the size of the objects and consume additional memory.

  2. Dynamic Cast Overhead: When using dynamic_cast to downcast pointers to derived classes, there is additional runtime overhead for performing type checks.

  3. Object Slicing: When storing derived objects in a container of base class objects, object slicing may occur.

  4. Management of pointers: To prevent object slicing and better manage polymorphic objects, we have to use pointers and that might require heap allocation.

Based on the above disadvantages I was thinking of following design to achieve runtime polymorphism.

class B; class C;

class A{
public:
    int common_data1; //common properties
    int common_data2;
    
    int type; //type of child stored by the following variant. e.g. 0 for B, 1 for C. we cal also use enum instead of int
    std::variant<B,C> child;
    void common();
    void default_behavior();
    void specific()
    {
        switch(type){
            case 0:
                std::get<B>(child).specific();
                break;
            case 1:
                std::get<C>(child).specific();
                break;
            default:
                default_behavior();
        }
    }
};

class B : public A{
public:
    int b_data;
    void b_fun();
    void specific();
};

class C : public B{
public:
    int c_data;
    void c_fun();
    void specific();
};

It seems to eliminate the above disadvantages.

  1. No Vtable overhead as there are no virtual functions.
  2. No dynamic cast is required.
  3. No Object slicing as we are storing the child object in std::variant.
  4. Since no slicing occurs, we don't need to use pointers to achieve polymorphism.

Despite the additional overhead associated with employing std::variant, it appears to offer a superior alternative in comparison to the drawbacks mentioned earlier. Can C++ experts please guide me regarding this design and provide their deep insights?

Vivek Mangal
  • 532
  • 1
  • 8
  • 24
  • You do know about [`std::visit`](https://en.cppreference.com/w/cpp/utility/variant/visit), right? – Nelfeal Aug 03 '23 at 07:52
  • 4
    The above technique requires the base class to know about all possible derived classes in advance. This is a major restriction. – Richard Critten Aug 03 '23 at 08:00
  • Your current implementation won't compile, because `std::variant` can't be instantiated with incomplete types. Generally however `std::variant` can be a value type based alternative to inheritance based polymorphism. It does come with it's own problems though. – chrysante Aug 03 '23 at 08:01
  • 1
    Make a proposal that actually works first of all. (Consider the fact that `sizeof(A) > sizeof(std::variant) >= max(sizeof(B), sizeof(C)) >= sizeof(A)` implies `sizeof(A) > sizeof(A)`.) – molbdnilo Aug 03 '23 at 08:14
  • 1
    You may be interested by [this SO thread](https://stackoverflow.com/questions/52296889/what-are-the-advantages-of-using-stdvariant-as-opposed-to-traditional-polymorp). It could give you some answers. – Fareanor Aug 03 '23 at 08:30
  • 3
    It is a myth that vtable calls are slow (dating from late 1990's), in practice it is one or two assembly instructions of indirection (and a memory access, which is where your cache comes in). So unless you really have a proven performance problem just use it. – Pepijn Kramer Aug 03 '23 at 09:02
  • @Vivek - The example only has a single "virtual" function and two derived classes. Try again with 5 functions and 10 derived classes. And then add class D, that derives from B but only overrides 2 of the functions. – BoP Aug 03 '23 at 10:32

3 Answers3

3
  1. The vtable is in the class, not in the object. The object stores some kind of reference to the vtable, which can be a pointer.
  2. Dynamic cast should be avoided. polymorphism is about calling member functions which will be dispatched to the appropriate object.
  3. Yes, you have to be aware of that
  4. Smart-pointers alleviate that problem

The advantage of the vtable-approach is that it is standard and should be known by C++ programmers and that it avoids things like type and switch-cases in your classes.

stefaanv
  • 14,072
  • 2
  • 31
  • 53
2

Your current implementation won't compile, because std::variant can't be instantiated with incomplete types. Generally however std::variant can be a value type based alternative to inheritance based polymorphism. It does come with it's own problems though.

If you write the code like this it should compile.

#include <variant>

struct Common {
    int common_data1; // Common properties
    int common_data2;
    
    void common();
};

struct B: Common {
    int b_data;

    void specific();
};

struct C: Common {
    int c_data;

    void specific();
};

struct A: std::variant<B, C> {
    using Base = std::variant<B, C>;
   
    using Base::Base;
   
    void common() {  
        std::visit([](auto& self) { self.common(); }, *this);
    }

    void specific() {
        std::visit([](auto& self) { self.specific(); }, *this);
    }
};

Downsides compared to inheritance based polymorphism:

  • It is more boilerplate, because you need to write every function twice, once in the concrete classes and once in the variant class.
  • It is not open to extension. You always need to modify A if you want to add another implementation.
  • You lose the code insulation, because all users of A need to know all possible implementations (B, C, ...) to instantiate the variant.

Advantages compared to inheritance based polymorphism:

  • You gain value semantics (without the risk of any object slicing).
  • Perhaps performance improvements (if measurable), because you don't need to dynamically allocate memory and switch-based dispatch of std::variant/std::visit could be faster then through function pointers hidden behind a vtable pointer.
chrysante
  • 2,328
  • 4
  • 24
2

Your approach is basically just vtables with a mustache. There is one vtable per class. When calling a virtual function, we get some value that the compiler secretly stored in the class that represents the runtime type of the instance. This value is likely just a pointer/number, exactly like your example. We then "dispatch" on this value by finding the correct table (vtable) that corresponds to our runtime type. And then look up the correct function in the table. This equates to about 2 pointer indirections in terms of performance. Your switch statement is basically just a vtable for a single function.

Your approach has similar overhead (memory and performance wise). And the same problem with "object slicing" in that you've abstracted away the type by storing different types of things in the same vector. The only benefit is you don't need to use a pointer for the polymorphism.

A better approach IMO would be to just store different types of things in different lists/vectors. No more dispatching, no more "object slicing", no more pointers. It's also way more cache efficient, and easier to read.