DMarlon Studio

Lesson 15 · 15 of 20

optional, variant and visit · C++

Model values that may be missing with std::optional, and values of several types with std::variant and std::visit, in C++17: ports and cells.

C++ · optional, variant and visit in DMarlon Studio: the example's code and its output in the terminal

`std::optional<int>` is an int that may not be there. findPort returns the port if the service is in the configuration and `std::nullopt` otherwise, with no special values such as -1. The return type itself tells callers they have to check the result.

An optional is true in a condition when it holds a value, and *port reads it. `value_or(25)` gives the value or, if it is missing, the one you pass: mail is not configured, so port 25 is used.

`std::variant<int, double, std::string>` holds exactly one of those types at a time, like a spreadsheet cell that may contain a whole number, a decimal or some text. using Cell = ... gives it a short name, and row mixes all three types in a single vector.

`std::visit` calls the right function for whatever type each cell holds. CellPrinter is a visitor: a struct with one operator() per type. If one were missing and no conversion fitted, the program would not compile, so no case can be forgotten.

To ask about one particular type there is `std::holds_alternative<double>`, and `std::get<std::string>` takes the value out; if the type does not match, it throws std::bad_variant_access.

Code · main.cpp

#include <iostream>#include <map>#include <optional>#include <string>#include <variant>#include <vector>// optional: a value that may be missing, with no magic numbers such as -1.std::optional<int> findPort(const std::map<std::string, int>& config, const std::string& service) {    auto it = config.find(service);    if (it == config.end()) return std::nullopt;    return it->second;}using Cell = std::variant<int, double, std::string>;   // a spreadsheet cell: one of thesestruct CellPrinter {                 // a visitor: one operator() per type    void operator()(int n) const { std::cout << "int    " << n << "\n"; }    void operator()(double d) const { std::cout << "double " << d << "\n"; }    void operator()(const std::string& s) const { std::cout << "text   \"" << s << "\"\n"; }};int main() {    std::map<std::string, int> config = {{"http", 8080}, {"db", 5432}};    for (std::string service : {"db", "mail"}) {        std::optional<int> port = findPort(config, service);        if (port) std::cout << service << " port: " << *port << "\n";        else std::cout << service << " port: not set, default " << port.value_or(25) << "\n";    }    std::vector<Cell> row = {42, 3.75, std::string("Total"), 7};    for (const Cell& c : row) std::visit(CellPrinter{}, c);    std::cout << "Cell 1 is a double? " << (std::holds_alternative<double>(row[1]) ? "yes" : "no")              << ", cell 2 says " << std::get<std::string>(row[2]) << "\n";}

Output · c++ -std=c++17 main.cpp -o main && ./main

db port: 5432
mail port: not set, default 25
int    42
double 3.75
text   "Total"
int    7
Cell 1 is a double? yes, cell 2 says Total

Key points

  • std::optional<T>: a T or nothing (std::nullopt).
  • Check an optional with if before reading it with *, or use value_or.
  • std::variant<A, B, C> holds one of those types at a time.
  • std::visit with a visitor handles every type in the variant.
  • std::get<T> throws if the variant does not hold a T.

Exercise

Add bool to the types of Cell and a true cell to the row. It prints as int 1, because the bool is converted to int to call the visitor. Add an operator()(bool) to CellPrinter that prints bool yes.