Appearance
RTTI, std any, and std variant
RTTI, std::any, and std::variant
C++ provides several mechanisms for working with types at runtime and handling heterogeneous data safely. Let's explore three key tools: RTTI for runtime type identification, std::any for type-safe type erasure, and std::variant for discriminated unions.
Runtime Type Information (RTTI)
RTTI allows you to inspect object types at runtime using typeid and dynamic_cast. It's enabled by default but can be disabled with -fno-rtti for performance-critical code.
cpp
#include <typeinfo>
#include <iostream>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
void inspectType(const Base& obj) {
// Get type name (implementation-defined format)
std::cout << "Type: " << typeid(obj).name() << std::endl;
// Check if it's a specific type
if (typeid(obj) == typeid(Derived)) {
std::cout << "It's a Derived object" << std::endl;
}
}
// Dynamic casting with runtime checks
void safeCast(Base* ptr) {
if (Derived* d = dynamic_cast<Derived*>(ptr)) {
// Safe to use d as Derived*
std::cout << "Successfully cast to Derived" << std::endl;
} else {
std::cout << "Cast failed" << std::endl;
}
}When to use RTTI:
- Polymorphic hierarchies where you need runtime type checking
- Debugging and logging
- Generic algorithms that need type-specific behavior
Performance considerations:
typeidis relatively fastdynamic_castcan be expensive in deep hierarchies- Consider alternatives like virtual functions for performance-critical code
std::any: Type-Safe Type Erasure
std::any can hold values of any copyable type while preserving type safety. It's useful when you need to store heterogeneous data without knowing the types at compile time.
cpp
#include <any>
#include <string>
#include <vector>
std::any createValue(int choice) {
switch (choice) {
case 0: return 42;
case 1: return std::string("hello");
case 2: return 3.14;
default: return std::vector<int>{1, 2, 3};
}
}
void processAny(const std::any& value) {
if (value.type() == typeid(int)) {
int i = std::any_cast<int>(value);
std::cout << "Integer: " << i << std::endl;
} else if (value.type() == typeid(std::string)) {
std::string s = std::any_cast<std::string>(value);
std::cout << "String: " << s << std::endl;
} else if (value.type() == typeid(double)) {
double d = std::any_cast<double>(value);
std::cout << "Double: " << d << std::endl;
} else if (value.type() == typeid(std::vector<int>)) {
const auto& vec = std::any_cast<const std::vector<int>&>(value);
std::cout << "Vector size: " << vec.size() << std::endl;
}
}
// Safe access with error handling
void safeAccess(const std::any& value) {
try {
int i = std::any_cast<int>(value);
std::cout << "Successfully extracted: " << i << std::endl;
} catch (const std::bad_any_cast& e) {
std::cout << "Cast failed: " << e.what() << std::endl;
}
}Use cases for std::any:
- Configuration systems with mixed value types
- Plugin architectures
- Generic data containers
- Interfacing with dynamic languages
Alternatives to consider:
std::variantwhen you know the set of possible types- Virtual base classes for polymorphic behavior
- Template specialization for compile-time type handling
std::variant: Discriminated Unions
std::variant represents a type-safe union that can hold one value from a fixed set of types. It's more efficient than std::any when you know the possible types in advance.
cpp
#include <variant>
#include <string>
#include <iostream>
// Define a variant that can hold int, string, or double
using Value = std::variant<int, std::string, double>;
void processValue(const Value& value) {
// Method 1: std::visit with lambda (C++17)
std::visit((const auto& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "Integer: " << v << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "String: " << v << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "Double: " << v << std::endl;
}
}, value);
// Method 2: std::get with error handling
if (std::holds_alternative<int>(value)) {
int i = std::get<int>(value);
std::cout << "Extracted int: " << i << std::endl;
} else if (std::holds_alternative<std::string>(value)) {
const std::string& s = std::get<std::string>(value);
std::cout << "Extracted string: " << s << std::endl;
}
}
// Safe access with std::get_if
void safeVariantAccess(const Value& value) {
if (auto* i = std::get_if<int>(&value)) {
std::cout << "Integer: " << *i << std::endl;
} else if (auto* s = std::get_if<std::string>(&value)) {
std::cout << "String: " << *s << std::endl;
} else if (auto* d = std::get_if<double>(&value)) {
std::cout << "Double: " << *d << std::endl;
}
}
// Variant with custom types
struct Point { int x, y; };
struct Circle { double radius; };
struct Rectangle { double width, height; };
using Shape = std::variant<Point, Circle, Rectangle>;
double area(const Shape& shape) {
return std::visit((const auto& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Point>) {
return 0.0; // Points have no area
} else if constexpr (std::is_same_v<T, Circle>) {
return 3.14159 * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
}
}, shape);
}Advantages of std::variant:
- Type-safe: no runtime type errors
- Efficient: no dynamic allocation
- Compile-time type checking
- Pattern matching with
std::visit
When to use std::variant:
- Parsing and AST representation
- Configuration with known value types
- State machines
- Error handling (success/error variants)
Choosing the Right Tool
Use RTTI when:
- You need runtime type checking in polymorphic hierarchies
- Debugging and logging type information
- Generic algorithms requiring type-specific behavior
Use std::any when:
- You don't know the types at compile time
- Building generic data containers
- Interfacing with dynamic systems
Use std::variant when:
- You know the set of possible types
- Performance is critical
- You want compile-time type safety
- Building state machines or parsers
Performance Considerations
- RTTI: Moderate overhead, can be disabled
- std::any: Dynamic allocation, type checking overhead
- std::variant: No allocation, compile-time type checking
For performance-critical code, prefer std::variant over std::any, and consider disabling RTTI if you don't need it.
Questions
Q: What is the main purpose of RTTI in C++?
RTTI (Runtime Type Information) allows you to inspect object types at runtime using typeid and dynamic_cast, enabling runtime type checking and safe polymorphic casting.
Q: When should you prefer std::variant over std::any?
Use std::variant when you know the set of possible types in advance. It's more efficient than std::any (no dynamic allocation) and provides compile-time type safety.
Q: What happens when you try to std::any_cast to the wrong type?
std::any_cast throws std::bad_any_cast exception when you try to cast to the wrong type. This provides type safety and allows you to handle casting errors gracefully.
Q: Which of the following is NOT a valid way to access std::variant values?
std::any_cast is used with std::any, not std::variant. For std::variant, use std::visit, std::get, or std::get_if to access values safely.
Q: What is the performance impact of using RTTI?
typeid is relatively fast, but dynamic_cast can be expensive in deep inheritance hierarchies. For performance-critical code, consider alternatives like virtual functions or disabling RTTI with -fno-rtti.