You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
9
+
In the last lecture we talked about `std::optional` and `std::expected` types that make our life better when we want to handle errors. Both of these types can store multiple values in the same memory. 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.
10
+
11
+
But, probably even more importantly, `std::variant` also happens to be the key to achieving a form of dynamic polymorphism when using templates.
10
12
11
13
<!-- Intro -->
12
14
13
15
## Why use `std::variant`?
14
16
15
17
`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
18
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:
19
+
For instance, if a variable can hold either an integer or a string, we can use `std::variant<int, std::string>` and put any value in it:
20
+
18
21
```cpp
19
22
#include<variant>
20
23
#include<iostream>
@@ -31,8 +34,15 @@ int main() {
31
34
}
32
35
```
33
36
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.
37
+
Do note though, that once we put one type into variant, `get`ting another type is undefined behavior, so don't do that.
38
+
39
+
<!-- TODO: talk about memory? -->
40
+
41
+
<!-- Can we store more than one same type? -->
42
+
43
+
## How `std::variant` is used in practice?
44
+
45
+
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, well, it does. If we need to know the type we want to use at compile time, we could as well just use that type and not bother with `std::variant` at all.
36
46
37
47
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
48
@@ -51,17 +61,23 @@ struct Printer {
51
61
};
52
62
53
63
int main() {
54
-
std::variant<int, std::string> value = "Hello, Variant!";
55
-
std::visit(Printer{}, value);
56
-
value = 42;
57
-
std::visit(Printer{}, value);
64
+
const Printer printer{};
65
+
std::variant<int, std::string> value = "Hello, Variant!";
66
+
std::visit(printer, value);
67
+
value = 42;
68
+
std::visit(printer, value);
58
69
}
59
70
```
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
71
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.
72
+
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, the operator that accepts an integer is called instead.
73
+
74
+
<!-- Talk about it all happening at runtime and the cost -->
75
+
76
+
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_.
77
+
78
+
<!-- TODO: change the below to just drop one operator from `Printer` -->
79
+
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 would not compile even though we only ever actually store a `std::string` in our variant:
63
80
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
81
```cpp
66
82
#include <variant>
67
83
#include <iostream>
@@ -74,25 +90,125 @@ struct LengthPrinter {
74
90
};
75
91
76
92
int main() {
77
-
// ❌ Does not compile!
78
-
std::variant<int, std::string> value = "Hello, Variant!";
79
-
std::visit(LengthPrinter{}, value);
93
+
// ❌ Does not compile!
94
+
std::variant<int, std::string> value = "Hello, Variant!";
95
+
std::visit(LengthPrinter{}, value);
96
+
}
97
+
```
98
+
99
+
This _is_ confusing. It might seem strange that we have to cover a case that we never aim to use. However, the reason why it was designed the way it was designed becomes easier to see if we look at a slightly more complex example.
100
+
101
+
Imagine that our `variant` is part of some class `Foo` that we design. The header file `foo.hpp` contains a declaration of our `Foo` class with a function `Print`:
102
+
103
+
`foo.hpp`
104
+
105
+
```cpp
106
+
#pragma once
107
+
108
+
#include<string>
109
+
#include<variant>
110
+
111
+
// Using struct for simplicity here.
112
+
structFoo {
113
+
void Print() const;
114
+
115
+
std::variant<int, std::string> value{};
116
+
};
117
+
118
+
```
119
+
120
+
We implement this `Print` function in a corresponding `foo.cpp` file and, because we want to print the value stored in a `std::variant` we need to use `std::visit` with, say, `BadPrinter` function object passed to it. We call it "bad" because it does not handle all of the types in our variant.
If we try to compile this code it won't - the compiler wants us to cover all the types of our variant in the `BadPrinter` class. However, let us for a moment assume that it _would_ be allowed by the standard and we would be able to compile this code into a library.
148
+
149
+
In that case, the code that we compile would be generated and stored in our library binary file. Without the code for handling `std::string` in `BadPrinter` that is.
150
+
151
+
Now imagine what would happen if at some point down the line someone would write an executable, link it to our code, try to store a `std::string` in the variant, and call `Print` on our `foo` object!
152
+
153
+
```cpp
154
+
#include"foo.hpp"
155
+
156
+
intmain() {
157
+
Foo foo{};
158
+
foo.value = "Some string";
159
+
foo.Print();
160
+
}
161
+
```
162
+
163
+
The behavior of this code would be undefined as our library's binary file would have no way to print a string stored in the variant inside the `Foo` class.
164
+
165
+
To the degree of my understanding this is the reason why the standard requires a function object passed into `std::visit` to be able to handle all the types that can be stored inside of a given `std::variant` object.
166
+
167
+
## Use it with an `std::vector`
168
+
169
+
Now with that out of the way, I want to talk about arguably the most important thing that `std::visit` allows us to do with `std::variant`. You see, we can now have a **vector of variants**.
170
+
171
+
Think about [dynamic polymorphism](inheritance.md#simple-polymorphic-class-example-following-best-practices) that we talked about before. The coolest thing about it was our ability to put multiple pointers to some interface class into a vector and then work with them without regard of which concrete type we're using. Here, with variant, we can do a very similar thing!
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
196
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.
197
+
We get to store any type from the allowed list of types and then we can process them in a uniform way using the `std::visit` pattern. Note that all of these values are set at runtime! This means that we can set these values from user input or from reading some file etc. And so we achieve dynamic polymorphism while using strong types and templated code.
85
198
86
199
## `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.
200
+
201
+
One last thing to cover about `std::variant` is a caveat that this type has. When we create a variable of this type, by default, a default value of the first type in its type list is stored. This might not always be desirable or, indeed, even possible. Imagine that we store only types that do not have a default constructor in the first place.
202
+
203
+
For this purpose there is a type `std::monostate` in the standard library. This is an empty type that is default constructible and we can define our variant type using `std::monostate` as its first type in the list should we want our variant to hold no value by default:
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
211
95
-
96
212
## **Summary**
97
213
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.
214
+
Overall, `std::variant` is extremely important for modern C++. If we implement our code largely using templates or concepts and need to enable dynamic polymorphism based on some values provided at runtime, there is probably no way around using `std::variant`. Which also means that we probably also need to use `std::visit`. These things might well be confusing from the get go but I hope that the explanations that we've covered just now will suffice to break the ice and using `std::variant` and `std::visit` in actual code will clear up any remaining issues.
0 commit comments