Many functions can't always produce an answer. Searching for a user by email might find nobody. Reading a number from text might fail. Older C++ code handled this with special values: return -1 for "not found", an empty string for "no name", or a null pointer. Callers had to remember which special value meant what, and forgetting led to quiet bugs.
C++17 added small vocabulary types that say what you mean. std::optional<T> holds either a value of type T or nothing at all. std::variant<A, B, C> holds exactly one value, which can be any of the listed types, and always knows which one it holds.
In this lesson you'll return optional values from functions and read them safely, store and handle different types with variant and std::visit, and return several values neatly with structured bindings. These features make function signatures self-explanatory and push you to handle every case.
std::optional
#include <optional>
optional<int> findAge(const string& name) {
if (name == "Ana") return 34;
return nullopt; // no value
}optional<int>means "an int, or nothing". The return type itself documents that the answer may be missing.- Return a value normally, or
nulloptfor "nothing".return {};also works. - Check with
if (result)orresult.has_value(), then read with*resultorresult.value(). result.value_or(0)gives the value, or the fallback if it's empty.
Reading optionals safely
| Way to read | If the optional is empty |
|---|---|
*opt | Undefined behaviour: never do this unchecked |
opt.value() | Throws bad_optional_access |
opt.value_or(fallback) | Returns the fallback |
if (opt) { use(*opt); } | The block is skipped |
A common, readable pattern puts the call in the if itself: if (auto age = findAge(name)) { cout << *age; }. The variable age only exists inside the if, where it's guaranteed to hold a value.
std::variant
A variant holds one value from a fixed list of types, and it remembers which:
#include <variant>
variant<int, double, string> setting = 42;
setting = "dark"; // now it holds a stringholds_alternative<string>(setting)checks the current type.get<string>(setting)reads it, throwingbad_variant_accessif the type is wrong.get_if<string>(&setting)returns a pointer to the value, ornullptrif it's another type.setting.index()gives the position of the current type in the list: 0, 1 or 2.
Unlike a C-style union, a variant is type-safe: you can't accidentally read an int as a string.
std::visit and structured bindings
std::visit calls a function with whatever the variant currently holds. Passing a generic lambda with an auto parameter handles every type in one go; if constexpr inside it can treat types differently. This replaces long chains of if (holds_alternative<...>), and the compiler checks that every type is handled.
Structured bindings unpack a struct, pair or tuple into named variables in one line:
auto [minScore, maxScore] = minmax({72, 45, 91});
for (const auto& [name, score] : scores) { ... } // over a mapThey make functions that return several values pleasant to use.
C++ optional and variant Syntax
optional and variant
#include <optional>
#include <variant>
optional<T> f() { return value; /* or */ return nullopt; }
if (auto x = f()) { use(*x); }
f().value_or(fallback);
variant<int, string> v = 5;
holds_alternative<int>(v); get<int>(v); get_if<string>(&v);
visit([](const auto& x) { cout << x; }, v);
auto [a, b] = makePair();C++ optional and variant Summary
optional<T>holds a T or nothing; returnnulloptfor "no value".- Check with
if (opt)before using*opt;value_orgives a fallback. variant<A, B, C>holds exactly one value of one of the listed types.- Read variants with holds_alternative, get, get_if or std::visit.
- Structured bindings unpack pairs, tuples and structs into named variables.
C++ optional and variant Examples
Each example shows the code, the output it produces and what each part does.
Searching with optional
#include <iostream>
#include <map>
#include <optional>
#include <string>
using namespace std;
map<string, double> prices = {{"tea", 2.50}, {"cake", 3.75}};
optional<double> findPrice(const string& item) {
auto it = prices.find(item);
if (it == prices.end()) return nullopt;
return it->second;
}
int main() {
for (string item : {"cake", "soup"}) {
if (auto price = findPrice(item)) {
cout << item << ": " << *price << endl;
} else {
cout << item << ": not on the menu" << endl;
}
}
cout << "Soup or 0: " << findPrice("soup").value_or(0) << endl;
return 0;
}cake: 3.75
soup: not on the menu
Soup or 0: 0Line-by-line explanation
- Line 9The return type tells every caller that a price might not exist.
- Line 11nullopt means "no value", instead of a magic number like -1.
- Line 17The if creates price and checks it in one step. Inside, *price is safe.
- Line 23value_or supplies a fallback when the optional is empty.
A variant for settings of different types
#include <iostream>
#include <map>
#include <string>
#include <variant>
using namespace std;
using Setting = variant<bool, int, string>;
int main() {
map<string, Setting> settings = {
{"darkMode", true},
{"fontSize", 16},
{"language", string("en-GB")}
};
for (const auto& [name, value] : settings) {
cout << name << " = ";
visit([](const auto& v) { cout << boolalpha << v; }, value);
cout << " (type " << value.index() << ")" << endl;
}
if (auto size = get_if<int>(&settings["fontSize"])) {
cout << "Bigger font: " << *size + 2 << endl;
}
return 0;
}darkMode = true (type 0)
fontSize = 16 (type 1)
language = en-GB (type 2)
Bigger font: 18Line-by-line explanation
- Line 7A type alias keeps the variant's long name readable.
- Line 13string("en-GB") is written explicitly. Some older compilers would convert a plain "en-GB", which is a const char*, into a bool instead.
- Line 16Structured bindings split each map entry into name and value.
- Line 18visit calls the lambda with whichever type the variant holds.
- Lines 22 to 24get_if returns a pointer only if the setting really is an int.
Parse results: a value or an error
#include <iostream>
#include <string>
#include <variant>
#include <vector>
using namespace std;
variant<int, string> parseAge(const string& text) {
try {
int age = stoi(text);
if (age < 0 || age > 130) return string("Age out of range: " + text);
return age;
} catch (const exception&) {
return string("Not a number: " + text);
}
}
int main() {
vector<string> inputs = {"34", "abc", "200"};
for (const string& in : inputs) {
auto result = parseAge(in);
if (holds_alternative<int>(result)) {
cout << "Valid age " << get<int>(result) << endl;
} else {
cout << "Error: " << get<string>(result) << endl;
}
}
return 0;
}Valid age 34
Error: Not a number: abc
Error: Age out of range: 200Line-by-line explanation
- Line 7The function returns either an int or an error message.
- Lines 10 to 11Return the error or the value; the variant records which.
- Lines 12 to 14stoi throws for "abc", which is turned into an error message.
- Line 21holds_alternative checks which type came back before reading it.
Common C++ optional and variant Mistakes
Dereferencing an empty optional
auto price = findPrice("soup");
cout << *price; // undefined behaviour: it's emptyauto price = findPrice("soup");
if (price) cout << *price;Magic values instead of optional
int findIndex(const vector<string>& v, const string& x); // returns -1 if missingoptional<size_t> findIndex(const vector<string>& v, const string& x);get with the wrong type
variant<int, string> v = 5;
string s = get<string>(v); // throws bad_variant_accessif (auto s = get_if<string>(&v)) cout << *s;C++ optional and variant in Real Projects
Vocabulary types in practice
Modern C++ code bases use optional for lookups and configuration values that may be absent, and variant for messages that can be one of several kinds, tokens in a parser, or "result or error" returns. C++23 even adds std::expected<T, E>, a type built specifically for holding a value or an error. Learning optional and variant now prepares you for all of these.
C++ optional and variant FAQ
Can std::optional hold a reference?
No: optional<int&> doesn't compile in C++17, C++20 or C++23. To return "maybe a reference to an existing object", return a pointer that can be nullptr, or an optional<reference_wrapper<T>>. Often the simplest answer is to return an optional<T> holding a copy, if the object is small.
What is std::monostate?
std::monostate is an empty type you put first in a variant to mean "nothing yet": variant<monostate, int, string> v;. A default-constructed variant holds its first alternative, so without monostate it would start out holding a default int (0 here). It's also useful when the first real type can't be default-constructed.
What is std::expected in C++23?
std::expected<T, E> holds either a successful value of type T or an error of type E, which makes it ideal for a function like "parse this number or tell me why not". You return std::unexpected("empty") for the error case, and callers test it like an optional and read .error() when it failed. It needs <expected> and -std=c++23; g++ 13 supports it.
What is std::any and how is it different from variant?
std::any can hold a value of any copyable type, decided at run time, and you get it back with any_cast<int>(a), which throws bad_any_cast if the type is wrong. A variant lists its possible types up front, so the compiler can check that you handle each one with visit. Prefer variant whenever you know the possible types.
Does std::optional allocate memory on the heap?
No. An optional<T> stores the value inside itself, next to a flag that says whether a value is present, so no heap memory is involved. That makes it slightly bigger than T alone: with g++, optional<int> is 8 bytes while an int is 4. Creating and returning optionals is therefore cheap.
C++ optional and variant Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line so the function returns "no value".
optional<int> find(int id) {
return ;
}
What does optional<string> represent?
What does this print?
optional<int> a;
optional<int> b = 7;
cout << a.value_or(1) + b.value_or(1) << endl;
Which safely reads a string from variant<int, string> v without throwing?
When is optional a better choice than an exception?
C++ optional and variant Quiz
5 questions to check what you learned.
Which header provides std::optional?
- <optional>
- <vector>
- <utility>
- <variant>
What happens if you call
.value()on an empty optional?- It returns 0
- It returns nullptr
- It throws bad_optional_access
- Nothing
What does std::visit do?
- Prints a variant
- Calls a function with the value the variant currently holds
- Converts a variant to a string
- Lists the variant's types
What does
auto [a, b] = p;do for a pair p?- Creates an array
- Unpacks p's two members into variables a and b
- Compares a and b
- Swaps the pair
How is a variant safer than a C-style union?
- It always knows which type it holds and stops you reading the wrong one
- It's smaller
- It can hold several values at once
- It doesn't need a header