Skip to content

Expert · Modern C++: lambdas, C++17 and C++20 essentials · topic 32 of 42 · 30 min

std::optional, std::variant and std::string_view

Explanation

std::optional<T> holds either a T or nothing, which is a clear way to say "no result" without magic values like -1. std::variant<A, B> holds exactly one of several types, safely. std::string_view is a cheap, read-only view of text owned by someone else. All three arrived in C++17.

The problem with magic values

Old code often returns -1 for "not found", an empty string for "no name", or a null pointer for "no object". Readers must remember each convention, and the caller can forget to check. C++17 added "vocabulary types" that say what you mean directly in the type.

std::optional: a value or nothing

cpp
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> findRoll(const std::vector<std::string>& names, const std::string& who) {
    for (std::size_t i = 0; i < names.size(); ++i) {
        if (names[i] == who) return static_cast<int>(i) + 1;
    }
    return std::nullopt;                    // "no value"
}

int main() {
    std::vector<std::string> cls = {"Asha", "Ravi", "Meena"};
    if (auto roll = findRoll(cls, "Ravi")) {   // true if it has a value
        std::cout << "Roll " << *roll << '\n';   // Roll 2
    }
    auto missing = findRoll(cls, "Om");
    std::cout << missing.value_or(-1) << '\n';   // -1
    std::cout << missing.has_value() << '\n';    // 0
}
*opt gives the value (undefined if empty); opt.value() throws std::bad_optional_access if empty; value_or gives a fallback.
Think of it like this: std::optional is like a parcel locker that may or may not have your parcel in it. The locker always exists; you check whether it is full before taking anything out. A function returning optional is honest that "nothing" is a possible answer.

std::variant: one of several types

cpp
#include <iostream>
#include <string>
#include <variant>

using PaymentResult = std::variant<std::string, int>;   // a transaction id OR an error code

PaymentResult pay(int amount) {
    if (amount <= 0) return 400;                          // error code
    return std::string("TXN") + std::to_string(amount);   // transaction id
}

int main() {
    auto r = pay(500);
    if (std::holds_alternative<std::string>(r)) {
        std::cout << "Paid: " << std::get<std::string>(r) << '\n';   // Paid: TXN500
    }
    std::visit([](const auto& v) { std::cout << "Result: " << v << '\n'; }, pay(0));   // Result: 400
}
A variant always holds exactly one alternative. std::get<T> throws std::bad_variant_access if it holds a different type; std::visit calls a function with whatever it holds.

variant is a type-safe replacement for C unions. It is great for modelling "this OR that": a JSON value (number, string, bool, list), a parse result, or a shape that is a circle or a rectangle, without needing inheritance and heap allocation.

std::string_view: look, do not own

cpp
#include <string_view>

bool isUpiId(std::string_view s) {                 // accepts std::string, literals, substrings
    auto at = s.find('@');
    return at != std::string_view::npos && at > 0 && at + 1 < s.size();
}

std::string id = "asha@okbank";
isUpiId(id);              // no copy
isUpiId("ravi@upi");      // no std::string created
std::string_view domain = std::string_view(id).substr(5);   // "okbank", no allocation
string_view is just a pointer and a length. Taking substrings is free because nothing is copied.
Careful: A string_view does not own its characters. If the string it looks at is destroyed or changed, the view dangles. Never return a string_view to a local std::string, never store a view of a temporary (std::string_view v = std::string("x") + "y"; dangles at the semicolon), and remember a string_view is not guaranteed to end with '\0', so do not pass .data() to C functions that expect a C string.

When to use which

SituationType
A function may have no resultstd::optional<T>
A value can be one of a fixed set of typesstd::variant<A, B, ...>
A function only reads text it does not keepstd::string_view parameter
You need to store or own textstd::string
Any type at all, decided at run time (rare)std::any
Tip: C++23 adds std::expected<T, E>, which holds either a value or an error, like Rust's Result. If your compiler supports it, it is often better than optional when you need to say WHY something failed.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

std::optional, std::variant and std::string_view · 1/6

Vocabulary types

C plus plus seventeen added types that say exactly what you mean: optional, variant and string view.

Prefer a video by a teacher? Find free videos on “c++17 std::optional std::variant std::string_view tutorial” on YouTube ↗

In real life: Parsing a PIN code from a form

A delivery app reads the PIN code typed in an address form. Instead of returning 0 or -1 on bad input, the parser returns std::optional<int>; the caller cannot forget that "invalid" is possible. It takes a std::string_view so it can parse part of a longer address line without copying.

cpp
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>

std::optional<int> parsePin(std::string_view s) {
    if (s.size() != 6 || s[0] == '0') return std::nullopt;
    int value = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
    if (ec != std::errc() || ptr != s.data() + s.size()) return std::nullopt;
    return value;
}

int main() {
    std::cout << parsePin("411001").value_or(-1) << '\n';   // 411001
    std::cout << parsePin("41A001").value_or(-1) << '\n';   // -1
    std::cout << parsePin("12345").has_value() << '\n';     // 0
}
std::from_chars (C++17, <charconv>) is a fast number parser that never throws. Indian PIN codes have 6 digits and do not start with 0.

Practice (0/4 done)

Task 1 · Question

std::optional<int> x; What is x.has_value()?

Task 2 · Question

What does this print? std::optional<int> a = 5, b; std::cout << a.value_or(0) + b.value_or(10);

Task 3 · Question

Which line creates a dangling string_view?

Task 4 · Practice

Write std::optional<double> safeDivide(double a, double b) that returns nothing when b is 0. Then define using Shape = std::variant<Circle, Rect>; (two small structs) and a function double area(const Shape&) that uses std::visit. Print the area of one circle and one rectangle.

Quick quiz

Question 1

1.What is std::nullopt?

Question 2

2.What happens with std::get<int>(v) when the variant v holds a std::string?

Question 3

3.Why is std::string_view a good parameter type for read-only text?

Question 4

4.Which C++ standard introduced optional, variant and string_view?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is std::nullopt?

Answer: The value that means "this optional is empty"

Returning std::nullopt from a function returning std::optional<T> gives an empty optional.

What happens with std::get<int>(v) when the variant v holds a std::string?

Answer: Throws std::bad_variant_access

std::get checks the active alternative and throws on a mismatch.

Why is std::string_view a good parameter type for read-only text?

Answer: It accepts std::string, literals and substrings without copying

A view is just a pointer and a length.

Which C++ standard introduced optional, variant and string_view?

Answer: C++17

All three came in C++17.

std::string a = "apple", b = "Apple"; Is a == b?Related

Answer: No, comparison is case sensitive

Characters are compared exactly; 'a' and 'A' are different.

From Text: C strings and std::string

What is std::span<const int> good for?Related

Answer: Passing any contiguous array (vector, std::array, C array) to a function without copying

span is a non-owning view of contiguous elements.

From C++20 ranges, views and other C++20 essentials

What is the main advantage of std::vector over a C-style array?Related

Answer: Its size can change at run time and it knows its size

vector grows with push_back and reports size(); it also manages its own memory.

From Arrays, std::array and a first look at std::vector

What can an enum variant hold in Rust?Related

Answer: Any data: none, a tuple of values, or named fields

Rust variants can be unit-like, tuple-like or struct-like.

From Enums and Option

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Modern C++: lambdas, C++17 and C++20 essentials” before its exam: C++20 ranges, views and other C++20 essentials.
  2. Module 8 exam: Modern C++ · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Exceptions, concurrency and building projects with CMake. 42 topics left in the course before the final exam.

← Previous: auto, structured bindings and other C++17 conveniences