C++17 has granted us with std::variant
. Simply put, it is a type-safe union
. To access the value it stores, you can either request a specific type (using std::get
or something similar) or “visit” the variant, automatically handling only the data-type that is actually there.
Visiting is done using std::visit
, and is fairly straight forward.
|
|
In (1) we define our variant type. In (2) we define a class with an overloaded operator()
. This is needed for the call to std::visit
. In (3) we define a vector of variants. In (4) we visit each variant. We pass in an instance of Print
, and overload resolution ensures that the correct overload will be called for every type.
But this example forces us to write and name an object for the overloaded operator()
. We can do better. In fact, the example for std::visit
on cppreference already does. Here is an example derived from it:
|
|
This is certainly more compact, and we removed the Print
struct. But how does it work? You can see a class-template (1), lambdas passed in as arguments for the construction (3), and something with an arrow and some more template magic (2). Let’s build it step by step.
First, we want to break the print functions out of Print
and compose them later.
|
|
In (1) and (2), we define the same operators as before, but in separate structs. In (3), we are inherit from both of those structs, then explicitly use their operator()
. This results in exactly the same results as before. Next, we convert Print
into a class template. I’ll jump ahead and convert it directly to a variadic template.
|
|
In (1) we define the template. We take an arbitrary number of classes, inherit from them, and use their operator()
. In (2) we instantiate the Print
class-template with PrintCString
and PrintInt
to get their functionality.
Next, we want to use lambdas to do the same. This is possible because lambdas are not functions; they are objects implementing operator()
.
|
|
In (1) we define the lambdas we need. In (2) we instantiate the template with our lambdas. This is ugly. Since lambdas have unique types, we need to define them before using them as template parameters (deducing their types using decltype
). Then, we need to pass the lambdas as arguments for aggregate initialization as lambdas have a delete default constructor. We are close, but not quite there yet.
The <decltype(PrintCString), decltype(PrintInt)>
part is really ugly, and causes repetition. But it is needed as ctors cannot do type-deduction. So in proper C++ style, we will create a function to circumvent that.
|
|
In (1) we define our helper function, to perform type deduction and forward it to the ctor. In (2) we take advantage of our newly found type-deduction to define the lambdas inline. But this is C++17, and we can do better.
C++17 added user-defined deduction guides. Those allow us to instruct the compiler to perform the same actions as our helper function, but without adding another function. Using a suitable deduction guide, the code is as follows.
|
|
In (1) we define a deduction guide which acts as our previous helper function, and in (2) we use the constructor instead of a helper function. Done.
Now we have fully recreated the original example. As Print
is no longer indicative of the template-class’ behavior, overloaded
is probably a better name.
|
|