Sylvester
Sylvester

Reputation: 111

std::variant vs pointer to base class for heterogeneous containers in C++

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.

Upvotes: 2

Views: 5217

Answers (5)

Indiana Kernick
Indiana Kernick

Reputation: 5331

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);
} 

Upvotes: 2

anastaciu
anastaciu

Reputation: 23802

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.

Upvotes: 3

pptaszni
pptaszni

Reputation: 8217

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).

Upvotes: 2

Michael Chourdakis
Michael Chourdakis

Reputation: 11158

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.

Upvotes: 1

Anthony Williams
Anthony Williams

Reputation: 68591

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.

Upvotes: 10

Related Questions