Menu
Coddy logo textTech

optional, variant, any

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 89 of 104.

Modern C++ provides three vocabulary types in the <optional>, <variant>, and <any> headers that help you handle values that might be absent, hold one of several types, or store any type at all.

std::optional<T> represents a value that may or may not exist - perfect for functions that might fail without throwing an exception:

#include <iostream>
#include <optional>

std::optional<int> findIndex(const std::string& str, char c) {
    for (size_t i = 0; i < str.size(); ++i) {
        if (str[i] == c) return i;
    }
    return std::nullopt;  // No value
}

int main() {
    auto result = findIndex("hello", 'l');
    if (result.has_value()) {
        std::cout << "Found at: " << *result << "\n";  // 2
    }
}

std::variant<Types...> is a type-safe union that holds exactly one of the specified types at any time:

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

int main() {
    std::variant<int, double, std::string> data;
    
    data = 42;
    std::cout << std::get<int>(data) << "\n";
    
    data = "hello";
    if (std::holds_alternative<std::string>(data)) {
        std::cout << std::get<std::string>(data) << "\n";
    }
}

std::any can hold a value of any type, determined at runtime. Use std::any_cast to retrieve the value:

#include <iostream>
#include <any>

int main() {
    std::any value = 10;
    std::cout << std::any_cast<int>(value) << "\n";
    
    value = std::string("text");
    std::cout << std::any_cast<std::string>(value) << "\n";
}

Choose std::optional for nullable values, std::variant when you know the possible types at compile time, and std::any only when you truly need runtime type flexibility.

challenge icon

Challenge

Easy

Let's build a configuration system that demonstrates the power of modern C++ vocabulary types. You'll create a flexible settings manager that handles values which might be absent, can hold different types, or need runtime type flexibility.

You'll organize your code across three files:

  • ConfigTypes.h: Define your configuration value types and helper functions.

    Create a function called parseValue that takes a string and attempts to parse it as an integer. If the string represents a valid integer, return the integer wrapped in std::optional. If parsing fails (the string contains non-numeric characters), return std::nullopt. For simplicity, consider a string valid if it only contains digits (and optionally a leading minus sign).

    Also create a type alias called Setting using std::variant that can hold either an int, a double, or a std::string.

    Finally, create a function called describeSetting that takes a Setting and returns a string describing what type it holds and its value in this format:

    • For int: Integer: [value]
    • For double: Double: [value]
    • For string: String: [value]
  • DynamicStore.h: Create a simple key-value store using std::any.

    Define a DynamicStore class that can store values of any type. It should have:

    • A method set that takes a string key and a std::any value, storing them internally (use a std::map)
    • A method get that takes a key and returns the std::any value (return an empty std::any if the key doesn't exist)
    • A method hasKey that returns true if the key exists
  • main.cpp: Read three inputs:
    1. A string that might be a number (for testing std::optional)
    2. A type indicator: int, double, or string
    3. A value corresponding to that type

    Demonstrate all three vocabulary types:

    First, use your parseValue function with the first input. If it contains a value, print Parsed: [value]. If it's empty, print Parse failed.

    Second, create a Setting variant based on the type indicator. If the type is int, store the value as an integer. If double, store it as a double. If string, store it as a string. Then call describeSetting and print the result.

    Third, create a DynamicStore, store the string "config_loaded" under the key "status", then retrieve it and print: Status: [value]. Use std::any_cast<std::string> to extract the value.

For example, with inputs 42, int, and 100:

Parsed: 42
Integer: 100
Status: config_loaded

With inputs hello, double, and 3.14:

Parse failed
Double: 3.14
Status: config_loaded

With inputs -15, string, and username:

Parsed: -15
String: username
Status: config_loaded

Remember to include the appropriate headers: <optional>, <variant>, <any>, <string>, and <map> where needed. Use std::holds_alternative and std::get to work with your variant, and has_value() or direct boolean conversion to check your optional.

Cheat sheet

Modern C++ provides three vocabulary types for handling flexible values:

std::optional<T>

Represents a value that may or may not exist. Include <optional>:

std::optional<int> findIndex(const std::string& str, char c) {
    for (size_t i = 0; i < str.size(); ++i) {
        if (str[i] == c) return i;
    }
    return std::nullopt;  // No value
}

auto result = findIndex("hello", 'l');
if (result.has_value()) {
    std::cout << *result;  // Dereference to get value
}

std::variant<Types...>

A type-safe union that holds exactly one of the specified types. Include <variant>:

std::variant<int, double, std::string> data;

data = 42;
std::cout << std::get<int>(data);

data = "hello";
if (std::holds_alternative<std::string>(data)) {
    std::cout << std::get<std::string>(data);
}

std::any

Can hold a value of any type, determined at runtime. Include <any>:

std::any value = 10;
std::cout << std::any_cast<int>(value);

value = std::string("text");
std::cout << std::any_cast<std::string>(value);

When to use: std::optional for nullable values, std::variant when possible types are known at compile time, std::any for runtime type flexibility.

Try it yourself

#include <iostream>
#include <string>
#include <any>
#include "ConfigTypes.h"
#include "DynamicStore.h"

int main() {
    // Read three inputs
    std::string input1;  // String that might be a number (for testing std::optional)
    std::string typeIndicator;  // Type indicator: int, double, or string
    std::string value;  // Value corresponding to that type
    
    std::cin >> input1;
    std::cin >> typeIndicator;
    std::cin >> value;
    
    // TODO: Part 1 - Test std::optional with parseValue
    // Use parseValue function with input1
    // If it contains a value, print "Parsed: [value]"
    // If it's empty, print "Parse failed"
    
    
    // TODO: Part 2 - Test std::variant with Setting
    // Create a Setting variant based on typeIndicator
    // If type is "int", store value as integer
    // If type is "double", store value as double
    // If type is "string", store value as string
    // Then call describeSetting and print the result
    
    
    // TODO: Part 3 - Test std::any with DynamicStore
    // Create a DynamicStore
    // Store the string "config_loaded" under the key "status"
    // Retrieve it and print: "Status: [value]"
    // Use std::any_cast<std::string> to extract the value
    
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming