Skip to content

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:

  • typeid is relatively fast
  • dynamic_cast can 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::variant when 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.