Visiting a std::variant safely
I assume you all know C++17's type-safe replacement for union
s: std::variant
. Here you look at a great replacement for union
s as it knows the active type and destructs that object on a new assignment or when the std::variant
itself gets destroyed. Achieving all these things with plain union
s requires effort.
The basics
Let's have a look at how you can visit a std::variant
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
What you see here is a common approach used with std::visit
. In A, I start creating a class template that derives from all its parameters. The idea is that overload
gets instantiated with a couple of lambdas. Thanks to inheritance, they all provide a call operator, which is also part of overload
. B ensures that the call operators from the base class(es) are available in the derived class.
This help is completed by C, the deduction guide, which tells CTAD how to instantiate the class template overload
.
Next, in D, I create a simple variant consisting of int
and bool
initialized with true
. So far, so good.
Finally, in E, I use overload
with std::visit
and the variant from D to find and print the value of the active member, bool
, in this case.
Type-safe they say
Well, you often hear, even from me, that a std::variant
is type-safe and superior to a union
. Let's test that. What happens if you forget to provide a lambda or, better, an overload for one of the cases? Let's say we forget the bool
case. Maybe it is less forgetting and more a new type was added during a refactoring, making our code look the following:
1 2 3 4 5 6 7 |
|
What do you think happens? I'm not asking what you want to happen ;-)
Well, ... the code compiles :-/ and it runs ... but, the output is:
1 |
|
Granted, the output is very close to true
, given that its equivalent is 1
. Yet, the output is wrong.
Thanks to implicit integer conversions, this code compiles fine. Only at run-time, the only available visitor int val
is picked. For me, type-safe means that this code should not have compiled. But we have to live with the implicit integer conversions. The question is, is there something you can do to avoid this situation? Yes, of course, write the correct code the first time. I'm talking about something more realistic.
Create a safety-net
There is something you can do. In the overload
class template, you can catch all cases which would lead to implicit conversions by providing a function template that catches them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
I first need a helper, which you can see in A. With C++23, we finally have static_assert(false)
, making the helper obsolete. But I decided to use C++17 code for this post.
Now, the safety net is in B. We provide a function template that is the best match for all types not caught by the lambdas. The functions body then uses a static_assert
for communicating the error, effectively preventing unwanted implicit conversions.
If you run the code without the lambda taking a bool
, you will get a compile error. One positive side-effect of all this is that you also improved the error message for an unhandled type that isn't convertible.
How about C++23?
For the full picture below, you will find the C++23 version, which doesn't need that always_false
helper nor CTAD and uses C++20's consteval
for our mistake-catching overload.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
[2023-09-11: Update of the example thanks to a comment from Bartlomiej Filipek using auto
spares the template-head.]
Regardless of which standard you're using, please remember the pattern above when writing type-safe code!
Andreas