optional and variant
Model a maybe-value with std::optional and a fixed set of alternatives with std::variant β no sentinels, no unions, no out-params.
std::optional<T> says "a T or nothing" and std::variant<A, B> says "exactly one of these types"; together they cover the two cases a raw pointer or tagged union used to fumble.
Two C++17 vocabulary types replace a pile of old idioms. std::optional<T> holds either a value of type T or nothing, so a function that might not return a result no longer needs a magic sentinel (-1, an empty string) or an output parameter plus a bool. std::variant<A, B, C> holds exactly one value drawn from a fixed list of types β a type-safe tagged union that always knows which alternative it currently stores.
std::optional β a value or nothing
Construct an optional from a value and it is engaged; default-construct it (or assign std::nullopt) and it is empty. Test it with has_value() or directly in a boolean context (if (opt)), and read the contained value with *opt or opt->member. The everyday convenience is value_or(fallback): it returns the contained value if engaged, otherwise the fallback you pass β collapsing the whole "is it there? if not use a default" dance into one call. Dereferencing an empty optional is undefined behaviour, so guard with has_value() or reach for value_or when a default will do.
std::variant β one of a fixed set
A std::variant is always valid and always holds exactly one of its alternatives; assigning a new value of a different alternative type switches which one is active. Ask which alternative is live with std::holds_alternative<T>(v), and pull the value out with std::get<T>(v) (or std::get_if<T>(&v) for a pointer that is null on mismatch). index() gives the zero-based position of the active alternative. A std::get for the wrong type throws std::bad_variant_access rather than reading garbage β the safety a hand-rolled union can't give you.
std::visit β dispatch on the active type
std::visit(visitor, v) calls the visitor with whatever the variant currently holds, picking the right overload at the call site. Pair it with an overload set built from lambdas (the "overloaded" pattern) and you get a compile-time-checked switch over every alternative β forget a case and it won't compile. This is the idiomatic way to consume a variant: exhaustive, type-safe, and free of manual holds_alternative ladders.
Try it 5 examples
optional return + value_or
C++introa=8080 shows value_or returning the engaged optional's parsed value, while b=80 shows it falling back because parsePort("") returned std::nullopt β one call replaces the whole "is it there? else default" branch.