**Understanding std::variant: A Modern C++ Polymorphism Replacement**

std::variant is a type-safe union that can hold one of several specified types. Since C++17 it offers a powerful alternative to classical inheritance for representing a value that may be one of a set of alternatives. In this article we will explore why std::variant is useful, how it works, and demonstrate common patterns and pitfalls through code examples.


1. Why Use std::variant?

Traditional Polymorphism std::variant
Requires a common base class No common base needed
Virtual function overhead Zero runtime cost beyond type tagging
Subclass proliferation Simple to add or remove types
Potential for slicing Guarantees correct type on access

When you have a value that can be one of several discrete types, especially when the types are unrelated, std::variant keeps the type system explicit and compile‑time safe. It also eliminates the indirection and dynamic dispatch costs of virtual functions.


2. Basic Syntax

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

using Value = std::variant<int, double, std::string>;

int main() {
    Value v = 42;                 // holds an int
    v = 3.14;                     // holds a double
    v = std::string{"Hello"};     // holds a string

    std::visit([](auto&& arg){
        std::cout << arg << '\n';
    }, v);
}
  • Definition: using Value = std::variant<int, double, std::string>;
  • Construction: Implicit conversion from any alternative type.
  • Access: `std::get (v)` or `std::visit`.

3. Common Operations

Operation Function Example
Check type `std::holds_alternative
(v)|if (std::holds_alternative(v)) {}`
Get value `std::get
(v)|int i = std::get(v);`
Visit std::visit(fn, v) See example above
Index v.index() Returns 0‑based index of current alternative
Swap std::swap(v1, v2) Swaps contents safely

Error Handling
`std::get

(v)` throws `std::bad_variant_access` if the stored type is not `T`. Use `std::get_if(&v)` for a null‑terminated pointer when the type may be absent. — ### 4. Example: A JSON‑Like Value “`cpp #include #include #include #include #include struct JsonValue; using JsonObject = std::map; using JsonArray = std::vector ; struct JsonValue : std::variant { using base = std::variant; using base::base; // inherit constructors }; void print(const JsonValue& v, int indent = 0) { std::string space(indent, ‘ ‘); std::visit([&](auto&& arg){ using T = std::decay_t; if constexpr (std::is_same_v) std::cout << space << "null\n"; else if constexpr (std::is_same_v) std::cout << space << (arg ? "true" : "false") << '\n'; else if constexpr (std::is_same_v) std::cout << space << arg << '\n'; else if constexpr (std::is_same_v) std::cout << space << '"' << arg << "\"\n"; else if constexpr (std::is_same_v) { std::cout << space << "[\n"; for (auto& el : arg) print(el, indent + 2); std::cout << space << "]\n"; } else if constexpr (std::is_same_v) { std::cout << space << "{\n"; for (auto& [k, v] : arg) { std::cout << std::string(indent+2,' ') << '"' << k << "\": "; print(v, indent + 2); } std::cout << space << "}\n"; } }, v); } int main() { JsonValue obj = JsonObject{ {"name", std::string{"ChatGPT"}}, {"active", true}, {"scores", JsonArray{85.5, 90.0, 78.0}}, {"data", JsonObject{{"a", 1}, {"b", 2}}} }; print(obj); } “` This demonstrates how `std::variant` can recursively hold complex, heterogeneous structures, all while maintaining compile‑time safety. — ### 5. Visitor Pattern Revisited The classic visitor pattern often requires a base class and virtual functions. With `std::variant`, you can express the same logic concisely: “`cpp struct PrintVisitor { void operator()(int i) const { std::cout << "int: " << i << '\n'; } void operator()(double d) const { std::cout << "double: " << d << '\n'; } void operator()(const std::string& s) const { std::cout << "string: " << s << '\n'; } }; std::visit(PrintVisitor{}, v); “` Because `std::visit` is overloaded for any callable that can be invoked with each alternative, you avoid the boilerplate of dynamic dispatch. — ### 6. Pitfalls & Best Practices 1. **Unions of Non‑Trivial Types** `std::variant` internally manages construction and destruction. Avoid embedding types with non‑trivial move semantics unless you explicitly move them. 2. **Large Numbers of Alternatives** `std::variant` grows linearly with alternatives in terms of storage and runtime dispatch. For dozens of types, consider `std::variant` only if alternatives are truly orthogonal; otherwise use polymorphism. 3. **Performance Considerations** Visiting a `variant` is typically cheaper than virtual dispatch because the compiler can inline the visitor. However, `std::visit` incurs a small function‑pointer lookup for each alternative. 4. **Thread Safety** `std::variant` instances are not thread‑safe for concurrent reads and writes. Synchronize access if necessary. 5. **Default Value** A `variant` must always contain one of its alternatives. Initialize it with a default value or use `std::optional<std::variant>` if the value can be absent. — ### 7. Conclusion `std::variant` is a versatile tool for representing a value that can take on one of several distinct types. It blends the safety and expressiveness of static typing with the flexibility of dynamic dispatch, all while avoiding the pitfalls of classical polymorphism such as inheritance hierarchies and virtual overhead. By mastering `std::variant` and its companion utilities (`std::get`, `std::holds_alternative`, `std::visit`, etc.), you can write cleaner, safer, and more efficient C++ code for many real‑world scenarios.</std::variant

发表评论