std::optional
A function that looks something up may find it or not, and an int has no value that means "nothing". std::optional<T> holds either a T or nothing, and it says so in the type, so a caller has to decide what to do about the empty case.
That is the difference from a sentinel such as -1 or an empty string: the compiler can see whether the value is checked, and the function signature tells the truth. This lesson builds optionals, moves them into and out of functions, and ends with the default-value mistake the type prevents.
An optional is a value plus a bool in most builds, padded to the alignment of the value. The cost is small enough that an interface returns one instead of an output parameter and a separate success flag.
A value that may be absent
An optional is empty by default or holds a value constructed from an argument. has_value() asks which, value() reads the value and value_or() returns the value or a fallback.
The program creates an empty optional, prints its state and the fallback, then assigns a value and prints it. The two states are visible in the output without a sentinel.
value() on an empty optional throws std::bad_optional_access, which lesson 077 covers. Until a program handles that, has_value() or value_or() is the way to read one.
#include <iostream>
#include <optional>
int main()
{
std::optional<int> value;
std::cout << value.has_value() << ' ' << value.value_or(-1) << '\n';
value = 42;
std::cout << value.has_value() << ' ' << value.value() << '\n';
}
g++ -std=c++23 -Wall -Wextra first.cpp -o first
./first
0 -1
1 42
A lookup that can fail
A function that searches a container returns optional<T>: an empty optional means not found, and the caller cannot forget the case because there is no T to use without asking for it.
The program looks for 3 and for 9 in a vector of ints. The first call returns the value, the second an empty optional, and the two checks print different messages.
The alternative is a sentinel or an exception. The optional makes the absence part of the return type, and the exception stays for something that really is exceptional.
#include <iostream>
#include <optional>
#include <vector>
std::optional<int> find_value(const std::vector<int>& values, int wanted)
{
for (int value : values) {
if (value == wanted) {
return value;
}
}
return std::nullopt;
}
int main()
{
std::vector<int> values{1, 2, 3};
if (auto found = find_value(values, 3)) {
std::cout << "found " << *found << '\n';
}
if (!find_value(values, 9)) {
std::cout << "9 is missing\n";
}
}
g++ -std=c++23 -Wall -Wextra lookup.cpp -o lookup
./lookup
found 3
9 is missing
emplace and reset
emplace constructs the value inside the optional, and reset destroys it and leaves the optional empty. Both avoid a temporary and make the state change explicit.
The program emplaces a string into an optional, prints it, resets it and prints the state again. A string is a good example because constructing one may allocate, and emplace does it once.
Assigning std::nullopt is the same as reset. The optional's destructor destroys the value when the optional dies, which is the RAII rule of lesson 027 applied to an optional member.
#include <iostream>
#include <optional>
#include <string>
int main()
{
std::optional<std::string> name;
name.emplace("ada");
std::cout << *name << ' ' << name->size() << '\n';
name.reset();
std::cout << name.has_value() << '\n';
}
g++ -std=c++23 -Wall -Wextra emplace.cpp -o emplace
./emplace
ada 3
0
An optional member
A class can have an optional member for data that is not always set, such as a middle name or a deadline. The member is empty until something assigns to it, and the class does not need a separate bool.
The program's person has an optional nickname that is set later. The output shows the empty state first and the value after the assignment.
An optional member costs one bool plus the storage of T, and it is the natural place for a value that arrives in a second step, such as a row of a table that may or may not have a column filled in.
#include <iostream>
#include <optional>
#include <string>
class person
{
public:
explicit person(std::string name) : name_(std::move(name)) {}
void set_nickname(const std::string& nickname)
{
nickname_ = nickname;
}
const std::optional<std::string>& nickname() const
{
return nickname_;
}
private:
std::string name_;
std::optional<std::string> nickname_;
};
int main()
{
person p{"Ada"};
std::cout << p.nickname().has_value() << ' ';
p.set_nickname("the countess");
std::cout << *p.nickname() << '\n';
}
g++ -std=c++23 -Wall -Wextra member.cpp -o member
./member
0 the countess
A default of the wrong type
value_or takes a default of a type that converts to T. For an optional string the default must be a string, not an int, and the compiler checks the conversion with a static assertion.
The program passes 0 as the default of an optional<string>. The error comes from inside <optional> and says the static assertion on is_convertible failed, with the type of the default and the type of the value spelled out.
The fix is std::string{} or "" as the default. When there is no sensible default, value_or is the wrong function and the caller should test has_value instead.
#include <optional>
#include <string>
int main()
{
std::optional<std::string> name;
return static_cast<int>(name.value_or(0).size());
}
g++ -std=c++23 -Wall -Wextra wrong_default.cpp -o wrong_default
In file included from wrong_default.cpp:1:
/usr/include/c++/15.2.0/optional: In instantiation of 'constexpr _Tp std::optional<_Tp>::value_or(_Up&&) const & [with _Up = int; _Tp = std::__cxx11::basic_string<char>]':
wrong_default.cpp:7:42: required from here
7 | return static_cast<int>(name.value_or(0).size());
| ~~~~~~~~~~~~~^~~
/usr/include/c++/15.2.0/optional:1247:25: error: static assertion failed
1247 | static_assert(is_convertible_v<_Up&&, _Tp>);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/optional:1247:25: note: 'std::is_convertible_v<int&&, std::__cxx11::basic_string<char> >' evaluates to false
/usr/include/c++/15.2.0/optional:1252:20: error: no matching function for call to 'std::__cxx11::basic_string<char>::basic_string(int)'
1252 | return static_cast<_Tp>(std::forward<_Up>(__u));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/optional:1252:20: note: there are 18 candidates
In file included from /usr/include/c++/15.2.0/string:56,
from wrong_default.cpp:2:
/usr/include/c++/15.2.0/bits/basic_string.h:887:9: note: candidate 1: 'template<class _Tp, class> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _Tp&, const _Alloc&) [with <template-parameter-2-2> = _Tp; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
887 | basic_string(const _Tp& __t, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:887:9: note: template argument deduction/substitution failed:
In file included from /usr/include/c++/15.2.0/optional:44:
/usr/include/c++/15.2.0/type_traits: In substitution of 'template<bool _Cond, class _Tp> using std::enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]':
/usr/include/c++/15.2.0/bits/basic_string.h:160:8: required by substitution of 'template<class _CharT, class _Traits, class _Alloc> template<class _Tp, class _Res> using std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::_If_sv = std::enable_if_t<((bool)std::__and_<std::is_convertible<const _Tp&, std::basic_string_view<_CharT, _Traits> >, std::__not_<std::is_convertible<const _Tp*, const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>*> >, std::__not_<std::is_convertible<const _Tp&, const _CharT*> > >::value), _Res> [with _Tp = int; _Res = void; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
160 | using _If_sv = enable_if_t<
| ^~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:884:30: required from 'constexpr _Tp std::optional<_Tp>::value_or(_Up&&) const & [with _Up = int; _Tp = std::__cxx11::basic_string<char>]'
884 | template<typename _Tp, typename = _If_sv<_Tp, void>>
| ^~~~~~~~
wrong_default.cpp:7:42: required from here
7 | return static_cast<int>(name.value_or(0).size());
| ~~~~~~~~~~~~~^~~
/usr/include/c++/15.2.0/type_traits:2837:11: error: no type named 'type' in 'struct std::enable_if<false, void>'
2837 | using enable_if_t = typename enable_if<_Cond, _Tp>::type;
| ^~~~~~~~~~~
/usr/include/c++/15.2.0/optional: In instantiation of 'constexpr _Tp std::optional<_Tp>::value_or(_Up&&) const & [with _Up = int; _Tp = std::__cxx11::basic_string<char>]':
wrong_default.cpp:7:42: required from here
7 | return static_cast<int>(name.value_or(0).size());
| ~~~~~~~~~~~~~^~~
/usr/include/c++/15.2.0/bits/basic_string.h:875:9: note: candidate 2: 'template<class _Tp, class> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _Tp&, size_type, size_type, const _Alloc&) [with <template-parameter-2-2> = _Tp; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
875 | basic_string(const _Tp& __t, size_type __pos, size_type __n,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:875:9: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:852:9: note: candidate 3: 'template<class _InputIterator, class> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(_InputIterator, _InputIterator, const _Alloc&) [with <template-parameter-2-2> = _InputIterator; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
852 | basic_string(_InputIterator __beg, _InputIterator __end,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:852:9: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:772:9: note: candidate 4: 'template<class _Rg> requires __container_compatible_range<_Rg, _CharT> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::from_range_t, _Rg&&, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
772 | basic_string(from_range_t, _Rg&& __rg, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:772:9: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:729:7: note: candidate 5: 'template<class> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(size_type, _CharT, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
729 | basic_string(size_type __n, _CharT __c, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:729:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:706:7: note: candidate 6: 'template<class> constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
706 | basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:706:7: note: template argument deduction/substitution failed:
/usr/include/c++/15.2.0/optional:1252:20: note: cannot convert 'std::forward<int>((* & __u))' (type 'int') to type 'const char*'
1252 | return static_cast<_Tp>(std::forward<_Up>(__u));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:808:7: note: candidate 7: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&&, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
808 | basic_string(basic_string&& __str, const _Alloc& __a)
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:808:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:803:7: note: candidate 8: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
803 | basic_string(const basic_string& __str, const _Alloc& __a)
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:803:7: note: candidate expects 2 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:798:7: note: candidate 9: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::initializer_list<_Tp>, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
798 | basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:798:45: note: no known conversion for argument 1 from 'int' to 'std::initializer_list<char>'
798 | basic_string(initializer_list<_CharT> __l, const _Alloc& __a = _Alloc())
| ~~~~~~~~~~~~~~~~~~~~~~~~~^~~
/usr/include/c++/15.2.0/bits/basic_string.h:742:7: note: candidate 10: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
742 | basic_string(basic_string&& __str) noexcept
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:742:35: note: no known conversion for argument 1 from 'int' to 'std::__cxx11::basic_string<char>&&'
742 | basic_string(basic_string&& __str) noexcept
| ~~~~~~~~~~~~~~~^~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:684:7: note: candidate 11: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; size_type = long unsigned int]'
684 | basic_string(const _CharT* __s, size_type __n,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:684:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:664:7: note: candidate 12: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, size_type, size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; size_type = long unsigned int]'
664 | basic_string(const basic_string& __str, size_type __pos,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:664:7: note: candidate expects 4 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:646:7: note: candidate 13: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, size_type, size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; size_type = long unsigned int]'
646 | basic_string(const basic_string& __str, size_type __pos,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:646:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:629:7: note: candidate 14: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&, size_type, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; size_type = long unsigned int]'
629 | basic_string(const basic_string& __str, size_type __pos,
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:629:7: note: candidate expects 3 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:613:7: note: candidate 15: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
613 | basic_string(const basic_string& __str)
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:613:40: note: no known conversion for argument 1 from 'int' to 'const std::__cxx11::basic_string<char>&'
613 | basic_string(const basic_string& __str)
| ~~~~~~~~~~~~~~~~~~~~^~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:601:7: note: candidate 16: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
601 | basic_string(const _Alloc& __a) _GLIBCXX_NOEXCEPT
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:601:34: note: no known conversion for argument 1 from 'int' to 'const std::allocator<char>&'
601 | basic_string(const _Alloc& __a) _GLIBCXX_NOEXCEPT
| ~~~~~~~~~~~~~~^~~
/usr/include/c++/15.2.0/bits/basic_string.h:585:7: note: candidate 17: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string() requires is_default_constructible_v<_Alloc> [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
585 | basic_string()
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:585:7: note: candidate expects 0 arguments, 1 provided
/usr/include/c++/15.2.0/bits/basic_string.h:192:7: note: candidate 18: 'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(__sv_wrapper, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
192 | basic_string(__sv_wrapper __svw, const _Alloc& __a)
| ^~~~~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:192:7: note: candidate expects 2 arguments, 1 provided
Exercises
-
Write a function that returns the middle character of a string as an optional
and call it with an even and an odd length. - Add an optional
score to person and print whether it is set. - Replace value_or(-1) in first.cpp with a has_value check and print a different message for the empty case.
- Add an optional
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
std::optional<T>hold?Answer
Either a T or nothing
The type says in the signature that the value may be absent.
-
What does
value_or(x)return?Answer
The stored value, or x when there is none
The call supplies the fallback and never throws.
-
What does
emplacedo?Answer
It constructs a value in the optional
The arguments go to the constructor of T, which is built in place.
-
Why is
optionalbetter than a sentinel such as -1?Answer
Every value of the type stays available and the absence is explicit
A sentinel steals one value and the caller has to remember it.
-
What does this program print?
#include <iostream> #include <optional> int main() { std::optional<int> value; std::cout << value.has_value() << ' ' << value.value_or(-1) << '\n'; value = 42; std::cout << value.has_value() << ' ' << *value; }Answer
0 -1 / 1 42
The empty optional reports false and the fallback, then the stored 42 is read.
-
What does this program print?
#include <iostream> #include <optional> #include <string> std::optional<std::string> find(int key) { if (key == 3) return std::string{"three"}; return std::nullopt; } int main() { if (auto value = find(3)) std::cout << "found " << *value << '\n'; std::cout << (find(9).has_value() ? "found" : "9 is missing"); }Answer
found three / 9 is missing
The first lookup returns a value, and the second returns an empty optional.
-
What does this program print?
#include <iostream> #include <optional> #include <string> int main() { std::optional<std::string> name; name.emplace(3, 'a'); std::cout << *name << ' ' << name->size() << '\n'; name.reset(); std::cout << name.has_value(); }Answer
aaa 3 / 0
emplace builds the string from three copies of a, and reset empties the optional.
-
What does this program print?
#include <iostream> #include <optional> #include <string> struct person { std::string name; std::optional<int> age; }; int main() { person p{"ada", std::nullopt}; std::cout << p.age.value_or(0) << ' ' << p.name; }Answer
0 ada
The missing age falls back to zero, and the name is printed next to it.
-
What does the compiler report for this line?
#include <optional> #include <string> int main() { std::optional<std::string> name; return static_cast<int>(name.value_or(0).size()); }Answer
no matching function for call to 'std::optional<std::string>::value_or(int)'
The fallback must have the stored type, and an int is not a string.
-
When is
value()the right read?Answer
When the caller has already checked has_value()
value() throws on an empty optional, so it belongs after the check.
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