|
| 1 | + |
| 2 | +`std::variant` in Modern C++ |
| 3 | +-- |
| 4 | + |
| 5 | +<p align="center"> |
| 6 | + <a href="https://youtu.be/dummy_link"><img src="https://img.youtube.com/vi/dummy_link/maxresdefault.jpg" alt="Video Thumbnail" align="right" width=50% style="margin: 0.5rem"></a> |
| 7 | +</p> |
| 8 | + |
| 9 | +In the last lecture we talked about `std::optional` and `std::expected` types that make our life better. It might be useful to understand _how_ they can store two values of different types in the same memory. We can get a glimpse into this by understanding how `std::variant` works. Furthermore, we can store many more types than two in it. This, incidentally also happens to be the key to mimicking dynamic polymorphism when using templates. |
| 10 | + |
| 11 | +<!-- Intro --> |
| 12 | + |
| 13 | +## Why use `std::variant`? |
| 14 | + |
| 15 | +`std::variant` is a type-safe `union` type introduced in C++17. It allows a variable to hold one value out of a defined set of types. |
| 16 | + |
| 17 | +For instance, if a variable can hold either an integer or a string, you can use `std::variant<int, std::string>` and put any value in it: |
| 18 | +```cpp |
| 19 | +#include <variant> |
| 20 | +#include <iostream> |
| 21 | +#include <string> |
| 22 | + |
| 23 | +int main() { |
| 24 | + // This compiles |
| 25 | + std::variant<int, std::string> value; |
| 26 | + value = 42; // value holds an int. |
| 27 | + std::cout << "Integer: " << std::get<int>(value) << '\n'; |
| 28 | + value = "42" // value now holds a string. |
| 29 | + std::cout << "String: " << std::get<std::string>(value) << '\n'; |
| 30 | + return 0; |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +### How `std::variant` is used in practice? |
| 35 | +While cool already, the current tiny example might feel quite limited. Think about it, we somehow have to _know_ which type our `std::variant` holds to use it. Which almost feels like it defeats the purpose. And to a degree it does. |
| 36 | + |
| 37 | +But we should not despair, this is C++ after all, there are options for us to use to make sure that we can work with _any_ type that the variant holds. This option is to use a visitor pattern through the use of the `std::visit` function: |
| 38 | + |
| 39 | +```cpp |
| 40 | +#include <variant> |
| 41 | +#include <iostream> |
| 42 | +#include <string> |
| 43 | + |
| 44 | +struct Printer { |
| 45 | + void operator(int value) const { |
| 46 | + std::cout << "Integer: " << value << '\n'; |
| 47 | + } |
| 48 | + void operator(const std::string& value) const { |
| 49 | + std::cout << "String: " << value << '\n'; |
| 50 | + } |
| 51 | +}; |
| 52 | + |
| 53 | +int main() { |
| 54 | + std::variant<int, std::string> value = "Hello, Variant!"; |
| 55 | + std::visit(Printer{}, value); |
| 56 | + value = 42; |
| 57 | + std::visit(Printer{}, value); |
| 58 | +} |
| 59 | +``` |
| 60 | +Here, `std::visit` applies a [function object](lambdas.md#before-lambdas-we-had-function-objects-or-functors) to the value contained in the variant. Should our variant hold a string, the operator that accepts a string is called and should it hold an integer instead, the operator that accepts an integer is called instead. |
| 61 | +
|
| 62 | +Note, that a typical pitfall that beginners make is to forget that all of the checks for this code happen at compile time without taking into account the runtime logic of our code. |
| 63 | +
|
| 64 | +If, for example, we would change our `Printer` function object to a `LengthPrinter` function object that only knows how to print length of objects, our code will not compile even though we only ever actually store an `std::string` in our variant: |
| 65 | +```cpp |
| 66 | +#include <variant> |
| 67 | +#include <iostream> |
| 68 | +#include <string> |
| 69 | +
|
| 70 | +struct LengthPrinter { |
| 71 | + void operator(const std::string& value) const { |
| 72 | + std::cout << "String length: " << value.size() << '\n'; |
| 73 | + } |
| 74 | +}; |
| 75 | +
|
| 76 | +int main() { |
| 77 | + // ❌ Does not compile! |
| 78 | + std::variant<int, std::string> value = "Hello, Variant!"; |
| 79 | + std::visit(LengthPrinter{}, value); |
| 80 | +} |
| 81 | +``` |
| 82 | +This happens because the compiler must guarantee that all the code paths compile because it does not know which other code might be called. This might happen if some dynamic library gets linked to our code after it gets compiled. If that dynamic library actually stores an `int` in our variant the compiled code must know how to deal with it. |
| 83 | + |
| 84 | +Many people find this confusing and get burned by this at least a couple of times until it becomes very intuitive and please remember that it just takes time. |
| 85 | + |
| 86 | +## `std::monostate` |
| 87 | +Whenever we create a new `std::variant` object we actually initialize it to storing some uninitialized value of the type that is first in the list of types that the variant can store. Sometimes it might be undesirable and we want the variant to be initialized in an "empty" state. For this purpose there is a type `std::monostate` in the standard library and we can define our variant type using `std::monostate` as its first type in the list. |
| 88 | +```cpp |
| 89 | +std::variant<std::monostate, SomeType, SomeOtherType> value{}; |
| 90 | +// value holds an instance of std::monostate now. |
| 91 | +``` |
| 92 | + |
| 93 | +Note that it probably means that we'll need to differentiate between our variant holding the `std::monostate` value or some other value in the `std::visit` that we will inevitably use at a later point in time. |
| 94 | + |
| 95 | + |
| 96 | +## **Summary** |
| 97 | + |
| 98 | +Overall, `std::variant` is extremely important for modern C++. If we implement our code largely using templates or concepts and need to enable polymorphic behavior based on some values provided at runtime, there is probably no way for us to avoid using it. Which also means that we probably also will need to use `std::visit`. These things might well be confusing from the get go but after we've looked into how function objects and lambdas work we should have no issues using all of this machinery. |
0 commit comments