std::variant

A variant holds exactly one value of a fixed list of types, and it remembers which one. It is a union with a tag, written so that the compiler checks the access and the alternatives are part of the type.

That makes it the tool for a value that changes shape: a setting that is a number or a word, a token that is an identifier or a literal, a result that is a value or an error. Reading it is either get, which checks at run time, or visit, which handles every alternative.

A variant is as large as its largest alternative plus the index, and it is aligned for all of them; the storage is a union, so no alternative is constructed until it becomes active.

One value, several types

std::variant<int, std::string> is either an int or a string. index() returns which alternative is active, counted from zero, and holds_alternative<T>() answers whether T is the one.

The program creates a variant from an int, prints the index and the checks, then assigns a string and prints them again. The assignment changes the active alternative and destroys the old value.

The first alternative is the default-constructed one, so a variant<int, std::string> with no argument holds an int of value 0. A variant whose first alternative has no default constructor cannot be default-constructed.

#include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{7}; std::cout << value.index() << ' ' << std::holds_alternative<int>(value) << '\n'; value = std::string{"text"}; std::cout << value.index() << ' ' << std::holds_alternative<std::string>(value) << '\n'; }

g++ -std=c++23 -Wall -Wextra first.cpp -o first
./first

0 1
1 1

get and get_if

std::get<T>(value) returns a reference to the value when T is the active alternative, and throws std::bad_variant_access otherwise. std::get<Index> does the same by position.

get_if takes a pointer to the variant and returns a pointer to the value or nullptr, so it never throws. The program uses it to check the active alternative before reading.

get_if is the tool for a function that branches on the alternative: one check, then a pointer that is either valid or null. get is for the case where the alternative is already known.

#include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{42}; if (auto* number = std::get_if<int>(&value)) { std::cout << "int " << *number << '\n'; } if (auto* text = std::get_if<std::string>(&value)) { std::cout << "string " << *text << '\n'; } else { std::cout << "not a string\n"; } std::cout << std::get<int>(value) << '\n'; }

g++ -std=c++23 -Wall -Wextra get.cpp -o get_demo
./get_demo

int 42
not a string
42

visit

std::visit calls a visitor with the active value. A generic lambda with an auto parameter handles every alternative in one function, and the return type is deduced from the branches.

The program visits a variant that holds an int and then a string, and the visitor prints a line for each type. The alternative is checked once, by the library, and the lambda sees the real type.

A visitor can also be a set of overloaded functions or a function object with several operator() overloads, which is the subject of lesson 126. The generic lambda is the shortest form when the branches are similar.

#include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{7}; auto show = [](const auto& item) { std::cout << item << '\n'; }; std::visit(show, value); value = std::string{"seven"}; std::visit(show, value); }

g++ -std=c++23 -Wall -Wextra visit.cpp -o visit_demo
./visit_demo

7
seven

A member that changes type

A class can store a variant for data whose type depends on the input: a configuration value that is a number in one file and a word in another. The class exposes a visit-based accessor and keeps the variant private.

The program's setting holds an int or a string, and set_text switches the alternative. The printed values show both states, and the class does not need a second member to remember which one is active.

The variant is the storage; the visitor is the interface. Callers never see get or get_if, which keeps the alternative list free to change.

#include <iostream> #include <string> #include <variant> class setting { public: void set_number(int number) { value_ = number; } void set_text(const std::string& text) { value_ = text; } void print() const { std::visit([](const auto& item) { std::cout << item << '\n'; }, value_); } private: std::variant<int, std::string> value_{0}; }; int main() { setting s; s.print(); s.set_text("auto"); s.print(); s.set_number(3); s.print(); }

g++ -std=c++23 -Wall -Wextra member.cpp -o member_variant
./member_variant

0
auto
3

A type that is not an alternative

A variant only accepts the types in its list. Assigning a const char* to a variant<int, double> has no matching alternative, and the compiler reports that no assignment operator fits the operand types.

The program assigns "text" to a variant of int and double. The message names the variant and the argument type, and the note lists the candidates, all of which need a type the variant can hold.

The fix is to add std::string to the alternative list or to convert the argument first. A variant with a string alternative would accept the literal by converting it.

#include <string> #include <variant> int main() { std::variant<int, double> value{1}; value = "text"; return 0; }

g++ -std=c++23 -Wall -Wextra wrong_type.cpp -o wrong_type

wrong_type.cpp: In function 'int main()':
wrong_type.cpp:7:13: error: no match for 'operator=' (operand types are 'std::variant<int, double>' and 'const char [5]')
7 | value = "text";
| ^~~~~~
wrong_type.cpp:7:13: note: there are 3 candidates
In file included from wrong_type.cpp:2:
/usr/include/c++/15.2.0/variant:1550:9: note: candidate 1: 'template<class _Tp> constexpr std::enable_if_t<((bool)((__exactly_once<std::variant<_Types>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type> > && is_constructible_v<std::variant<_Types>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type>, _Tp>) && is_assignable_v<std::variant<_Types>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type>&, _Tp>)), std::variant<_Types>&> std::variant<_Types>::operator=(_Tp&&) [with _Types = {int, double}]'
1550 | operator=(_Tp&& __rhs)
| ^~~~~~~~
/usr/include/c++/15.2.0/variant:1550:9: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/variant: In substitution of 'template<class ... _Types> template<class _Tp, class> using std::variant<_Types>::__accepted_type = std::variant<_Types>::__to_type<((long unsigned int)__accepted_index<_Tp>)> [with _Tp = const char (&)[5]; <template-parameter-2-2> = void; _Types = {int, double}]':
/usr/include/c++/15.2.0/variant:1546:14: required by substitution of 'template<class _Tp> constexpr std::enable_if_t<((bool)((__exactly_once<std::variant<int, double>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type> > && is_constructible_v<std::variant<int, double>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type>, _Tp>) && is_assignable_v<std::variant<int, double>::__accepted_type<_Tp&&, typename std::enable_if<__not_self<_Tp&&>, void>::type>&, _Tp>)), std::variant<int, double>&> std::variant<int, double>::operator=(_Tp&&) [with _Tp = const char (&)[5]]'
1546 | enable_if_t<__exactly_once<__accepted_type<_Tp&&>>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
wrong_type.cpp:7:13: required from here
7 | value = "text";
| ^~~~~~
/usr/include/c++/15.2.0/variant:1462:15: error: no type named 'type' in 'struct std::enable_if<false, void>'
1462 | using __accepted_type = __to_type<__accepted_index<_Tp>>;
| ^~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1488:16: note: candidate 2: 'constexpr std::variant<_Types>& std::variant<_Types>::operator=(const std::variant<_Types>&) [with _Types = {int, double}]'
1488 | variant& operator=(const variant&) = default;
| ^~~~~~~~
/usr/include/c++/15.2.0/variant:1488:26: note: no known conversion for argument 1 from 'const char [5]' to 'const std::variant<int, double>&'
1488 | variant& operator=(const variant&) = default;
| ^~~~~~~~~~~~~~
/usr/include/c++/15.2.0/variant:1489:16: note: candidate 3: 'constexpr std::variant<_Types>& std::variant<_Types>::operator=(std::variant<_Types>&&) [with _Types = {int, double}]'
1489 | variant& operator=(variant&&) = default;
| ^~~~~~~~
/usr/include/c++/15.2.0/variant:1489:26: note: no known conversion for argument 1 from 'const char [5]' to 'std::variant<int, double>&&'
1489 | variant& operator=(variant&&) = default;
| ^~~~~~~~~

Exercises

  1. Add a bool alternative to the variant in first.cpp and print the index for each of the three types.
  2. Write a visitor that returns a string describing the active alternative and print it for both states.
  3. Store a variant in a vector and print every element with std::visit.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a std::variant hold?

    Answer

    Exactly one value of its listed types

    The variant is a tagged union written safely, and it knows which alternative is active.

  2. What does std::get_if<T> return when T is not the active alternative?

    Answer

    nullptr

    The pointer form turns the mismatch into a test.

  3. What does std::visit do?

    Answer

    It calls a callable with the active alternative

    The visitor is instantiated for every alternative and the active one is passed.

  4. What happens if std::get<T> is called for the wrong type?

    Answer

    It throws std::bad_variant_access

    The exception reports the mismatch, while get_if returns a pointer.

  5. What does this program print? #include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{42}; std::cout << value.index() << ' ' << std::get<int>(value) << '\n'; value = std::string{"hello"}; std::cout << value.index() << ' ' << std::get<std::string>(value); }

    Answer

    0 42 / 1 hello

    The index is the position of the active alternative, and assignment changes it.

  6. What does this program print? #include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{42}; if (auto* number = std::get_if<int>(&value)) std::cout << "int " << *number << '\n'; std::cout << (std::get_if<std::string>(&value) ? "not a string" : "not a string") << '\n'; std::cout << std::get<int>(value); }

    Answer

    int 42 / not a string / 42

    get_if succeeds for the active int, fails for the string, and the direct get reads 42.

  7. What does this program print? #include <iostream> #include <string> #include <variant> int main() { std::variant<int, std::string> value{7}; std::cout << std::visit([](const auto& item) -> int { return static_cast<int>(std::string{}.size()); }, value) << '\n'; value = std::string{"seven"}; std::visit([](const auto& item) { std::cout << item; }, value); }

    Answer

    0 / seven

    The first visit returns zero, and the second prints the active string.

  8. What does this program print? #include <iostream> #include <string> #include <variant> struct number { std::variant<int, std::string> data; }; int main() { number value{1}; std::cout << value.data.index() << '\n'; value.data = std::string{"auto"}; std::cout << std::get<std::string>(value.data) << '\n'; std::cout << std::get<std::string>(value.data).size(); }

    Answer

    0 / auto / 4

    The member holds an int, then a string, and the string has four characters.

  9. What does the compiler report for this assignment? #include <string> #include <variant> int main() { std::variant<int, double> value{1}; value = "text"; }

    Answer

    no match for 'operator=' (operand types are 'std::variant<int, double>' and 'const char [5]')

    A string literal is not one of the alternatives, and no conversion selects one.

  10. What is the advantage of a variant over a class with a tag and a union?

    Answer

    The compiler checks the access and the alternatives are part of the type

    The tag and the union are written by the library with the correct destruction and checks.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev