Reputation: 111
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
x
,Upvotes: 2
Views: 5217
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
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
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:
but
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
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
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