C++17 optional, variant and any: Practical Patterns, Pitfalls and a Worked Example

Learn when C++17 std::optional, std::variant and std::any fit, then trace safe retrieval and fallback choices through one practical configuration example.

KnowledgeGate Team

Exam prep & CS education

Updated 1 Nov 20266 min read
C++17 optional, variant and any: Practical Patterns, Pitfalls and a Worked Example

std::optional, std::variant and std::any can all look like ways to store a value whose shape is uncertain. They actually model three different kinds of uncertainty. Choosing the wrong one hides useful compile-time information or makes ordinary absence difficult to express. Use the configuration values "8080", 1500, true, "safe", "request-42" and 27 to compare how each type is chosen, retrieved and explained in an interview or code review.

C++17 optional, variant and any answer three different questions

Type

Meaning

Normal retrieval

std::optional<T>

Zero or one T

Check presence, then use value(), *, or value_or()

std::variant<Ts...>

Exactly one active value from a closed compile-time list

Use std::visit, std::get_if, or checked std::get

std::any

Empty, or one value whose concrete type is discovered at runtime

Use exact-type std::any_cast

All three are value-owning vocabulary types, not interchangeable nullable containers.

Ask three questions in order:

  1. Can one known type simply be absent? Choose optional.

  2. Must the value be one of a known set, such as int, bool, or std::string? Choose variant.

  3. Must infrastructure carry an open-ended payload whose producer and consumer agree on an exact runtime type? Consider any, with weaker compile-time guarantees.

These vocabulary types belong naturally in the broader Coding & DSA route. If the language foundations need revision first, follow the C++ Tutorial learning sequence before returning to C++17 library types.

A three-branch C++17 decision tree compares optional port values 8080 and absent 80x, variant values 1500, true, and safe, and any payloads request-42 and 27.

std::optional represents one known type that may be absent

This parser returns a port only when the whole input is a decimal int in the accepted range:

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

std::optional<int> parse_port(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = first + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);

    if (ec != std::errc{} || ptr != last || value < 0 || value > 65535) {
        return std::nullopt;
    }
    return value;
}

ec rejects inputs that cannot be converted or overflow int. ptr != last rejects a partial parse, while the final comparisons reject ports outside 0 to 65535.

The trace is exact. parse_port("8080") contains 8080. parse_port("80x") is disengaged because parsing stops before x, and parse_port("70000") is disengaged because 70000 > 65535. Their value_or(443) results are therefore 8080, 443 and 443.

Disengagement means only “no value”. It cannot distinguish invalid characters from an out-of-range number. Use a richer result type when that reason matters. Calling value() on a disengaged optional throws std::bad_optional_access; dereferencing it violates the access precondition. Check presence first.

std::variant keeps a closed set of alternatives type-safe

Use an explicit first state for an unset setting:

cpp
#include <string>
#include <variant>

using Setting = std::variant<std::monostate, int, bool, std::string>;

template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

std::string describe(const Setting& setting) {
    return std::visit(overloaded{
        [](std::monostate) { return std::string{"unset"}; },
        [](int value) { return std::string{"int:"} + std::to_string(value); },
        [](bool value) { return value ? std::string{"bool:true"}
                                     : std::string{"bool:false"}; },
        [](const std::string& value) { return std::string{"text:"} + value; }
    }, setting);
}

Setting unset = std::monostate{};, Setting timeout = 1500;, Setting tls = true; and Setting mode = std::string{"safe"}; have indices 0, 1, 2 and 3. The visitor returns "unset", "int:1500", "bool:true" and "text:safe". Do not make indices durable application tags because reordering the alternatives changes them.

The variant tracks its active alternative, so no manual type tag is needed. std::get_if<int>(&mode) returns nullptr. std::get<int>(mode) is valid to compile because int is listed, but throws std::bad_variant_access because mode holds a std::string.

std::any trades exhaustiveness for an open runtime type

std::any retrieves only the exact stored type:

cpp
#include <any>
#include <cstdint>
#include <string>
#include <typeinfo>

std::any trace = std::string{"request-42"};
bool present = trace.has_value();
bool is_text = trace.type() == typeid(std::string);
auto* text = std::any_cast<std::string>(&trace);
auto* not_an_int = std::any_cast<int>(&trace);

std::any count = std::int64_t{27};
std::int64_t exact = std::any_cast<std::int64_t>(count);
auto* still_not_int = std::any_cast<int>(&count);

Here present and is_text are true; text points to "request-42"; and both int pointers are nullptr. exact is 27, but any_cast never converts std::int64_t to int. The value form std::any_cast<int>(count) throws std::bad_any_cast.

This fits opaque plugin metadata or a heterogeneous property bag where code outside the container defines the type contract. It supplies no exhaustive visitor, common operation, comparison, or general serialization. For a known set such as int, bool and std::string, variant preserves more information.

Optional, variant and any in a worked configuration pipeline

For the successful record, start with port_text = "8080", Setting setting = std::string{"safe"} and std::any trace = std::string{"request-42"}.

  1. parse_port produces engaged optional<int>{8080}, so the port is 8080.

  2. The visitor selects the string lambda and produces "text:safe".

  3. Pointer-form any_cast<std::string> succeeds and produces "request-42".

The rendered record is exactly {port: 8080, setting: "text:safe", trace: "request-42"}.

For the fallback record, use port_text = "80x", Setting setting = std::monostate{} and std::any trace = std::int64_t{27}. Parsing yields std::nullopt, so the caller's value_or(443) policy selects 443. Visiting produces "unset". The text cast returns nullptr, so caller policy writes "no-text-trace". The result is {port: 443, setting: "unset", trace: "no-text-trace"}. Casting that same trace to std::int64_t would return 27.

A two-row pipeline shows successful and fallback handling for optional, variant, and any values.

The three fields carry different uncertainty. Port is one type plus possible absence, setting is one of four declared alternatives, and trace crosses an open runtime boundary. Safe retrieval does not choose business policy. The caller still decides fallbacks and error reporting.

C++17 vocabulary-type traps come from confusing presence, alternatives and type identity

Trap

What actually happens

Safer move

std::optional<bool>{false} in a condition

The condition is true because the optional is engaged; its contained value is false

Test *value only after checking presence

Calling value() unchecked

A disengaged optional throws

Check has_value() or use an explicit fallback

Treating variant::index() as a business tag

Reordering alternatives changes the index

Visit by type or define a stable domain enum

Calling std::get<T> for the wrong active type

It throws std::bad_variant_access

Use a visitor or std::get_if when mismatch is expected

Casting std::int64_t from any as int

Exact type matching fails

Cast the stored type, then convert explicitly

Using any for a known closed set

Compile-time exhaustiveness is lost

Use variant

A variant can rarely become valueless_by_exception() if an exception interrupts a type-changing operation. Ordinary default construction is not valueless here: Setting{} constructs its first std::monostate. Access must follow the program's actual exception policy.

variant is strong when all value alternatives are known and visitors express the operations. A virtual base-class interface suits an extensible family of objects sharing behaviour. Object Oriented Technology Explained develops that runtime-polymorphism contrast. any is opaque storage, not a behavioural interface.

C++17 interview and exam-style questions test exact state and exact type

Representative questions ask you to choose a wrapper, predict has_value(), index(), or a cast, separate compile-time rejection from a runtime mismatch, and repair unchecked retrieval.

  • With std::optional<bool> enabled = false;, static_cast<bool>(enabled) is true because the optional is engaged, while *enabled is false.

  • With std::variant<int, std::string> value = 7; value = std::string{"7"};, the indices are first 0, then 1. Afterwards, std::get_if<int>(&value) is nullptr.

  • With std::any number = std::int64_t{27};, std::any_cast<int>(&number) == nullptr is true, although the stored numerical value is 27.

There is also an important compile-time distinction. std::get<double>(value) is ill-formed because double is not in the alternative list. std::get<int>(value) is well-formed, but throws std::bad_variant_access when the active alternative is std::string{"7"}. Explain both outcomes using “alternative list” and “active alternative”.

C++17 optional, variant and any: the short version and next step

  • optional<T> means perhaps one T.

  • variant<Ts...> means one value from a declared closed set.

  • any means an opaque value requiring an exact runtime type match.

The retrieval rule is equally compact: check optional presence, visit or probe the variant's active alternative, and prefer pointer-form any_cast when mismatch is expected.

Without looking back, derive 8080 versus fallback 443, the visitor results "unset", "int:1500", "bool:true" and "text:safe", and why an int cast cannot retrieve std::int64_t{27}. For a focused next step in language concepts and coding practice, use C++ Programming. For broader practice across placement languages, use Coding For Placements. Both are optional routes after you can explain the three uncertainty models in your own words.