1

Let's assume this class hierarchy below.

class BaseClass {
public:
  int x;
}

class SubClass1 : public BaseClass {
public:
  double y;
}

class SubClass2 : public BaseClass {
public:
  float z;
}
...

I want to make a heterogeneous container of these classes. Since the subclasses are derived from the base class I can make something like this:

std::vector<BaseClass*> container1;

But since C++17 I can also use std::variant like this:

std::vector<std::variant<SubClass1, SubClass2, ...>> container2;

What are the advantages/disadvantages of using one or the other? I am interested in the performance too.

Take into consideration that I am going to sort the container by x, and I also need to be able to find out the exact type of the elements. I am going to

  1. Fill the container,
  2. Sort it by x,
  3. Iterate through all the elements, find out the type, use it accordingly,
  4. Clear the container, then the cycle starts over again.
Sylvester
  • 91
  • 3
  • 11
  • container1 - to iterate over the container and have subclass behavior, have at least one virtual function( maybe destructor) in the base class. container2- more of like type safe unions, where in one object is visible. – Srini Jan 17 '20 at 09:30
  • They are not equivalent. You can't inherit `std::string` from `BaseClass`, but you can have a `std::variant` – Caleth Jan 17 '20 at 09:35
  • 2
    `std::visit` of `std::variant` would do call similar to virtual dispatch. – Jarod42 Jan 17 '20 at 09:35
  • it would be much more helpful if you could have provided a minimum reproducible example for for a few use case so we can know what the usage pattern is, as far as performance is concerned if code is performance critical then macro benchmark few use cases. – Gaurav Dhiman Jan 17 '20 at 09:38
  • @Gaurav Dhiman I am going to fill the container, sort it by x, iterate through all the elements and use them, clear the container, then the cycle starts over again. – Sylvester Jan 17 '20 at 09:49
  • 1
    @Sylvester How do you use them? I mean, are you calling virtual functions on each element? The best solution depends on the use case. Please update the question with an example. – Indiana Kernick Jan 17 '20 at 10:01
  • 1
    "find out the type, use it accordingly" You could declare virtual functions in the base class. That would probably be much cleaner but it's hard to say what would be cleaner without more information. – Sneaky Turtle Jan 17 '20 at 10:16
  • @Sneaky Turtle I send the data to a peer in a TCP connection. I just need to access the elements unique member variables to do that. – Sylvester Jan 17 '20 at 10:16
  • 2
    You could declare a virtual function `sendData` in the base and override it for each subclass. There's no need to "find out the type" explicitly. – Indiana Kernick Jan 17 '20 at 10:17

5 Answers5

8

std::variant<A,B,C> holds one of a closed set of types. You can check whether it holds a given type with std::holds_alternative, or use std::visit to pass a visitor object with an overloaded operator(). There is likely no dynamic memory allocation, however, it is hard to extend: the class with the std::variant and any visitor classes will need to know the list of possible types.

On the other hand, BaseClass* holds an unbounded set of derived class types. You ought to be holding std::unique_ptr<BaseClass> or std::shared_ptr<BaseClass> to avoid the potential for memory leaks. To determine whether an instance of a specific type is stored, you must use dynamic_cast or a virtual function. This option requires dynamic memory allocation, but if all processing is via virtual functions, then the code that holds the container does not need to know the full list of types that could be stored.

Anthony Williams
  • 66,628
  • 14
  • 133
  • 155
  • sorry, for going a bit offtopic, but is it really stricly necessary to use dynamic memory allocation? It could be pointers to stack allocated objects (no smart pointers in this case). Perhaps this is just a rare case and if the vector is supposed to own the instances there is no way around dynamic allocating them – 463035818_is_not_an_ai Jan 17 '20 at 10:02
  • Yes, you could have pointers to existing objects allocated on the stack or as globals, but that would be a really rare case. – Anthony Williams Jan 17 '20 at 10:09
3

A problem with std::variant is that you need to specify a list of allowed types; if you add a future derived class you would have to add it to the type list. If you need a more dynamic implementation, you can look at std::any; I believe it can serve the purpose.

I also need to be able to find out the exact type of the elements.

For type recognition you can create a instanceof-like template as seen in C++ equivalent of instanceof. It is also said that the need to use such a mechanism sometimes reveals poor code design.

The performance issue is not something that can be detected ahead of time, because it depends on the usage: it's a matter of testing different implementations and see witch one is faster.

Take into consideration that, I am going to sort the container by x

In this case you declare the variable public so sorting is no problem at all; you may want to consider declaring the variable protected or implementing a sorting mechanism in the base class.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
anastaciu
  • 23,467
  • 7
  • 28
  • 53
  • I have no problem specifying the allowed types. It's going to be 4 SubClasses, and this will not change in the foreseeable future. – Sylvester Jan 17 '20 at 09:51
  • 1
    In that case it's a good option, but it's the general case that code will need to be altered/improved in the future and is probably going to be altered by someone other that the original designers, so creating a simpler design is allways a good thing. There is also the memory management issue pointed out by @AntonyWilliams. – anastaciu Jan 17 '20 at 10:00
2

What are the advantages/disadvantages of using one or the other?

The same as advantages/disadvantages of using pointers for runtime type resolution and templates for compile time type resolution. There are many things that you might compare. For example:

  • with pointers you might have memory violations if you misuse them
  • runtime resolution has additional overhead (but also depends how would you use this classes exactly, if it is virtual function call, or just common member field access)

but

  • pointers have fixed size, and are probably smaller than the object of your class will be, so it might be better if you plan to copy your container often

I am interested in the performance too.

Then just measure the performance of your application and then decide. It is not a good practice to speculate which approach might be faster, because it strongly depends on the use case.

Take into consideration that, I am going to sort the container by x and I also need to be able to find out the exact type of the elements.

In both cases you can find out the type. dynamic_cast in case of pointers, holds_alternative in case of std::variant. With std::variant all possible types must be explicitly specified. Accessing member field x will be almost the same in both cases (with the pointer it is pointer dereference + member access, with variant it is get + member access).

pptaszni
  • 5,591
  • 5
  • 27
  • 43
2

Sending data over a TCP connection was mentioned in the comments. In this case, it would probably make the most sense to use virtual dispatch.

class BaseClass {
public:
  int x;

  virtual void sendTo(Socket socket) const {
    socket.send(x);
  }
};

class SubClass1 final : public BaseClass {
public:
  double y;

  void sendTo(Socket socket) const override {
    BaseClass::sendTo(socket);
    socket.send(y);
  }
};

class SubClass2 final : public BaseClass {
public:
  float z;

  void sendTo(Socket socket) const override {
    BaseClass::sendTo(socket);
    socket.send(z);
  }
};

Then you can store pointers to the base class in a container, and manipulate the objects through the base class.

std::vector<std::unique_ptr<BaseClass>> container;

// fill the container
auto a = std::make_unique<SubClass1>();
a->x = 5;
a->y = 17.0;
container.push_back(a);
auto b = std::make_unique<SubClass2>();
b->x = 1;
b->z = 14.5;
container.push_back(b);

// sort by x
std::sort(container.begin(), container.end(), [](auto &lhs, auto &rhs) {
  return lhs->x < rhs->x;
});

// send the data over the connection
for (auto &ptr : container) {
  ptr->sendTo(socket);
} 
Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50
1

It's not the same. std::variant is like a union with type safety. No more than one member can be visible at the same time.

// C++ 17
std::variant<int,float,char> x;
x = 5; // now contains int
int i = std::get<int>(v); // i = 5;
std::get<float>(v); // Throws

The other option is based on inheritance. All members are visible depending on which pointer you have.

Your selection will depend on if you want all the variables to be visible and what error reporting you want.

Related: don't use a vector of pointers. Use a vector of shared_ptr.

Unrelated: I'm somewhat not of a supporter of the new union variant. The point of the older C-style union was to be able to access all the members it had at the same memory place.

Michael Chourdakis
  • 10,345
  • 3
  • 42
  • 78