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
EasyLet'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
parseValuethat takes a string and attempts to parse it as an integer. If the string represents a valid integer, return the integer wrapped instd::optional. If parsing fails (the string contains non-numeric characters), returnstd::nullopt. For simplicity, consider a string valid if it only contains digits (and optionally a leading minus sign).Also create a type alias called
Settingusingstd::variantthat can hold either anint, adouble, or astd::string.Finally, create a function called
describeSettingthat takes aSettingand 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]
- For int:
DynamicStore.h: Create a simple key-value store usingstd::any.Define a
DynamicStoreclass that can store values of any type. It should have:- A method
setthat takes a string key and astd::anyvalue, storing them internally (use astd::map) - A method
getthat takes a key and returns thestd::anyvalue (return an emptystd::anyif the key doesn't exist) - A method
hasKeythat returns true if the key exists
- A method
main.cpp: Read three inputs:- A string that might be a number (for testing
std::optional) - A type indicator:
int,double, orstring - A value corresponding to that type
Demonstrate all three vocabulary types:
First, use your
parseValuefunction with the first input. If it contains a value, printParsed: [value]. If it's empty, printParse failed.Second, create a
Settingvariant based on the type indicator. If the type isint, store the value as an integer. Ifdouble, store it as a double. Ifstring, store it as a string. Then calldescribeSettingand print the result.Third, create a
DynamicStore, store the string"config_loaded"under the key"status", then retrieve it and print:Status: [value]. Usestd::any_cast<std::string>to extract the value.- A string that might be a number (for testing
For example, with inputs 42, int, and 100:
Parsed: 42
Integer: 100
Status: config_loadedWith inputs hello, double, and 3.14:
Parse failed
Double: 3.14
Status: config_loadedWith inputs -15, string, and username:
Parsed: -15
String: username
Status: config_loadedRemember 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;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container12Modern C++ Features
Move Semantics & RvaluesPerfect ForwardingLambda Expressions In Depthstd::function & std::bindconstexpr and constevalStructured Bindingsoptional, variant, any