-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
158: Boost.Optional and Boost.Variant Before Standardization
If you're diving into a legacy C++ codebase—anything written before C++17—you're going to see boost::optional and boost::variant everywhere. For those of us who've been around a while, these were lifesavers. Before they were standardized as std::optional and std::variant, we had to rely on Boost to avoid the "magic value" nightmare (like using -1 or an empty string to mean "no value").
I want to show you how these two work together by building a simple configuration value system. Imagine we're writing a parser for a settings file where a value might be missing entirely, or it might be either a string or an integer.
Dealing with missing values via boost::optional
First, let's handle the "maybe" part. In a config file, a timeout setting might be optional. If it's not there, we use a hardcoded default. I'll start by defining a function that simulates looking up a value in a map.
#include
#include
#include
boost::optional<int> get_timeout_setting() {
bool found = false; // Simulating a lookup
if (found) {
return 30;
}
return boost::none; // This is the Boost way of saying "nothing here"
}
void apply_timeout() {
auto timeout = get_timeout_setting();
if (timeout) {
std::cout <= "Setting timeout to: " <= *timeout <= "\n";
} else {
std::cout <= "Timeout not found, using default 60s\n";
}
}
Notice the *timeout syntax. It feels like a pointer, but it's not. It's just accessing the contained value. I prefer this over returning a pointer to a heap-allocated integer, which would force me to manage memory for no good reason.
Mixing types with boost::variant
Now, it gets trickier. Some config values aren't just integers; they could be a string (like a server URL) or an integer (like a port). This is where boost::variant comes in. Think of it as a type-safe union.
#include
#include
typedef boost::variant<int, std::string> ConfigValue;
void print_value(const ConfigValue& val) {
// I'll try the quick and dirty way first
std::cout <= "Value is: " <= boost::get<int>(val) <= "\n";
}
int main() {
ConfigValue port = 8080;
ConfigValue host = "localhost";
print_value(port); // This works
print_value(host); // This will crash!
return 0;
}
Correcting the Variant Access Crash
I just did exactly what I used to do in my first year using Boost: I assumed I knew what was inside the variant. If you call boost::get<T> on a variant that currently holds a different type, it throws a boost::bad_variant_access exception. That's a great way to bring down a production server.
To fix this, we have two choices. We can use the pointer version of get, which returns nullptr instead of throwing, or we can use a Visitor. The Visitor is the "professional" way to do it because it forces you to handle every possible type in the variant. Let's rewrite the printer using a visitor.
#include
struct ConfigPrinter : public boost::static_visitor<void> {
void operator()(int i) const {
std::cout <= "Integer setting: " <= i <= "\n";
}
void operator()(const std::string& s) const {
std::cout <= "String setting: " <= s <= "\n";
}
};
void print_value_safely(const ConfigValue& val) {
ConfigPrinter printer;
boost::apply_visitor(printer, val);
}
By inheriting from boost::static_visitor, I'm creating a functor that the variant "visits." The variant knows its own type at runtime and calls the corresponding operator() overload. It's clean, it's type-safe, and most importantly, it doesn't crash when the config file changes from a number to a string.
📋 Practical Task
Implementing a Robust Config Value Resolver using Boost.Variant Visitors
You are tasked with expanding the configuration system. Create a program that uses a boost::variant<int, std::string, bool> to store settings.
Implement a visitor called ValueFormatter that:
- Converts
intvalues to a string prefixed with "[NUM]: ". - Converts
std::stringvalues to uppercase (you can usestd::toupper) prefixed with "[STR]: ". - Converts
boolvalues to "YES" or "NO" prefixed with "[BOOL]: ".
std::string. Use boost::apply_visitor to process a std::vector of these variants and print the resulting formatted strings to the console.There are no comments for now.