C++17 std::any: any_cast Forms, Type Safety, and When variant Is the Better Choice

Key takeaways

A guide that summarizes std::any and variant·void* comparison, type safety, any_cast, practical examples, and performance overhead.

What is any?

std::any is a type erasure container introduced in C++17. You can store values of any type, and you can check the type and extract the value at runtime. A type-safe alternative to void*.

#include <any>

std::any a = 42;           // int
a = 3.14;                  // double
a = std::string{"hello"};  // string

// Access after checking the type
if (a.type() == typeid(std::string)) {
    std::cout << std::any_cast<std::string>(a) << std::endl;
}

Why do you need it?:

  • Type Flexibility: When the type is unknown at compile time.
  • Type safety: Safer type erasure than void*.
  • Automatic Management: Automatic life cycle management
  • Exception safety: Exception when accessing the wrong type.
// ❌ void*: type unsafe, manual management
void* ptr = new int(42);
int x = *static_cast<int*>(ptr);  // No type checking
delete ptr;  // manual deletion

// ✅ std::any: type safe, automatically managed
std::any a = 42;
if (a.type() == typeid(int)) {
    int x = std::any_cast<int>(a);  // Check type
}
// automatic destruction

How any works:

std::any stores type information and value together internally. Small objects are stored in a buffer inside the any object itself and large objects are stored on the heap (Small Object Optimization). “Inside the object” means wherever the any lives — on the stack for a local variable, inside a vector’s heap buffer for a std::vector<std::any>.

The sketch below is simplified: it always allocates, and it omits copying. A real std::any also needs a way to copy the stored value without knowing its type at the call site, so implementations keep a small table of type-specific operations (copy, move, destroy, get type) — typically a single function pointer or a pointer to a static “manager” per stored type. That table is the essence of type erasure: the type is known when the value is stored, the code for handling it is generated then, and afterwards the any only carries a pointer to that code.

// Conceptual Implementation
class any {
    void* data_;
    const std::type_info* type_;
    void (*deleter_)(void*);
    
public:
    template<typename T>
    any(T value) {
        data_ = new T(std::move(value));
        type_ = &typeid(T);
        deleter_ = [](void* p) { delete static_cast<T*>(p); };
    }
    
    ~any() {
        if (data_) {
            deleter_(data_);
        }
    }
    
    const std::type_info& type() const {
        return *type_;
    }
};

any vs variant vs optional:

Featuresstd::anystd::variantstd::optional
Storage typeAll typesfixed typeSingle type
type trackingruntimecompile timeN/A
memoryinline buffer or heap (large objects)inline, never allocatesinline, never allocates
PerformanceslowFastVery fast
Type Saferuntimecompile timecompile time
UsePlugins, Settingsstate machine, errornull alternative
// any: Any type possible
std::any a = 42;
a = std::string{"hello"};
a = std::vector<int>{1, 2, 3};

// variant: only certain types
std::variant<int, std::string> v = 42;
v = std::string{"hello"};
// v = std::vector<int>{};  // error

// optional: single type
std::optional<int> opt = 42;
opt = std::nullopt;

Choosing Among any, variant, and void*

void*

  • Stores an address and nothing else: no ownership, no type, no check. A wrong static_cast back is undefined behavior.
  • Still the right tool for opaque C callback context and fixed-layout FFI buffers, where the pointee’s lifetime is managed elsewhere.
  • The failure modes (the multiple-inheritance offset bug, the string-literal trap in any_cast) and the C callback pattern are worked through in std::any vs void* in C++; this article stays with std::any itself.

std::variant

  • If a closed set (e.g. int | double | string) is fixed at compile time, visit·holds_alternative will allow most mistakes to be caught at the compile stage.
  • The value is stored inside the variant itself — its size is roughly the largest alternative plus a discriminator — and the standard forbids it from allocating memory for the alternatives. That guarantee is a real advantage over any in allocation-sensitive code.

std::any

  • Suitable when a set of storage types is open and unknown types can enter, such as plugins.
  • In return, type checking goes to runtime, and bad any_cast is an exception (bad_any_cast).

Selection guide one line: variant if the type candidate is known, void* (or uintptr_t) if only a heap pointer is passed and the contract is clear, and any if “value ownership + arbitrary type” in between.

Type safety

The safety of std::any is less about “Does it prevent bad casts?” and more about “Can bad casts be detected at runtime?”

  • any_cast<T>(a) must have an internal storage type that exactly matches T (there are reference·cv qualifier rules). If you try to insert int and take it out with long, it will fail.

The exact-match rule is the most common source of bad_any_cast in practice, because the stored type is whatever the expression’s type was at the point of assignment, after decay. std::any a = "hello"; stores a const char*, not a std::string, so any_cast<std::string>(a) throws. std::any a = 42; stores int, so reading it back as size_t, long or double throws, even though those conversions are implicit everywhere else in C++. No conversion, promotion or base-class lookup happens: storing a Derived and asking for Base also fails, which surprises people who expect any to behave like a pointer to a base class. The std::bad_any_cast exception also does not say which types were involved (what() typically just returns “bad any_cast”), so when I debug one, logging a.type().name() next to the failing cast is the quickest way to see what was actually stored.

  • type() returns type_info, so the if (a.type() == typeid(Foo)) pattern is possible, but maintenance is better left to any_cast at once.
  • At interface boundaries, it is safe to limit types to variant or dedicated base classes whenever possible, and to keep any only in layers that really need heterogeneity.

Every any_cast Form

Copy by value

std::any a = std::string{"hi"};
std::string s = std::any_cast<std::string>(a);  // copy

Edited with reference

std::any a = 10;
std::any_cast<int&>(a) = 20;

Pointer overload (nullptr on failure)

std::any a = 3.14;
if (double* p = std::any_cast<double>(&a)) {
    *p = 2.71;
}

any_cast throws bad_any_cast on value/reference overloads if the stored type and the requested type do not match. Pointer overloads return nullptr instead of an exception, so the hot path has the option of branching to pointer form.

Move-only type

std::any cannot hold move-only types at all — not even through std::make_any or emplace — because copying an any must copy its content (see “Non-copyable types” below). For copyable types that are expensive to copy, construct in place with std::make_any<T>(args...) or a.emplace<T>(args...), read through std::any_cast<T&>(a) to avoid a copy, and use std::any_cast<T>(std::move(a)) to move the value out when the any is no longer needed.

Performance overhead

Approximate cost factors are as follows:

  1. Type information: type_info search, internal comparison when any_cast.
  2. Storage method: Depending on the implementation, small objects are placed in an inline buffer with Small Object Optimization, but large objects or complex types may be subject to heap allocation. The buffer is small — one pointer in libstdc++, a few pointers in libc++ and MSVC — and a type only qualifies if its move constructor is noexcept (the standard encourages this so that moving an any can never throw). A std::string fits in some implementations and not others, so the same code allocates on one platform and not on another.
  3. Copy/Move: If you frequently copy any itself, the cost of copying the stored value is also incurred.

mitigation

  • If the candidate type is determined, move it to std::variant.
  • If you repeatedly cast from the same scope, either cast only once and get a reference, or get it as a variant/concrete type in the first place.
  • In configuration maps, etc., check whether a strongly typed struct or toml/json parser result type is better than a string key + any value.

Where any Fits and Where It Does Not

SituationWhy any is suitable
Script/plugin passes arbitrary type payloadDifficult to enumerate types in advance
Configuration file values are mixed, such as int/string/bool, etc.Convenient as a simple key-value store (however, if the schema grows, consider a dedicated type)
Various payloads on event busBranch to any_cast in handler
Test mock object/mock dependencyUsed only to a limited extent

Conversely, if the type is fixed, such as numerical hot loop, real-time audio sample processing, or internal API, it is better to avoid any and use variant or a direct type.

Creating, Resetting, and Checking an any

#include <any>

// generation
std::any a1 = 42;
std::any a2 = std::string{"hello"};
std::any a3;  // empty any

// check
if (a1.has_value()) {
std::cout << "has value" << std::endl;
}

// type
std::cout << a1.type().name() << std::endl;

type().name() returns an implementation-defined string: MSVC prints a readable int, while GCC and Clang print the mangled name i (and something like NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE for std::string). It is fine for debug logs — c++filt -t demangles it — but should never be used as a stable key or shown to users. For an empty any, type() returns typeid(void). To empty an existing any, call a.reset(); assigning {} also works.

Heterogeneous Containers, Config Stores, and Event Systems

Heterogeneous containers

#include <any>
#include <vector>
#include <string>

int main() {
    std::vector<std::any> data;
    
    data.push_back(42);
    data.push_back(3.14);
    data.push_back(std::string{"hello"});
    
    for (const auto& item : data) {
        if (item.type() == typeid(int)) {
            std::cout << "int: " << std::any_cast<int>(item) << std::endl;
        } else if (item.type() == typeid(double)) {
            std::cout << "double: " << std::any_cast<double>(item) << std::endl;
        } else if (item.type() == typeid(std::string)) {
            std::cout << "string: " << std::any_cast<std::string>(item) << std::endl;
        }
    }
}

This works, but notice what the loop has become: a hand-written chain of typeid comparisons, one per supported type, which silently skips anything not listed. Adding a new type means finding every such chain. When the set of types is known — as it is here — std::vector<std::variant<int, double, std::string>> with std::visit gives the same flexibility, and the compiler reports every visitor that does not handle a newly added alternative. any pays off only when the code that stores values and the code that reads them are written independently, and the reader genuinely cannot know all types.

Configuration repository

#include <any>
#include <map>
#include <string>

class Config {
    std::map<std::string, std::any> settings;
    
public:
    template<typename T>
    void set(const std::string& key, const T& value) {
        settings[key] = value;
    }
    
    template<typename T>
    T get(const std::string& key) const {
        auto it = settings.find(key);
        if (it != settings.end()) {
            return std::any_cast<T>(it->second);
        }
throw std::runtime_error("No key");
    }
};

int main() {
    Config config;
    
    config.set("port", 8080);
    config.set("host", std::string{"localhost"});
    config.set("timeout", 30.0);
    
    int port = config.get<int>("port");
    std::string host = config.get<std::string>("host");
    double timeout = config.get<double>("timeout");
}

The caller of get<T> must repeat the exact type used in set, and nothing checks that at compile time. This is where the const char* trap bites hardest: config.set("host", "localhost") deduces T as char[10], stores a const char* pointing at the string literal, and a later get<std::string>("host") throws bad_any_cast. The code above avoids it by wrapping the literal in std::string{...}, but every caller has to remember. Similarly set("timeout", 30) followed by get<double> fails. For configuration specifically, a typed struct loaded once from the file, or a std::variant of the few value types a config format supports, catches these mistakes much earlier.

Event system

#include <any>
#include <functional>
#include <map>
#include <string>

class EventBus {
    std::map<std::string, std::vector<std::function<void(std::any)>>> handlers;
    
public:
    void on(const std::string& event, std::function<void(std::any)> handler) {
        handlers[event].push_back(handler);
    }
    
    void emit(const std::string& event, std::any data) {
        if (auto it = handlers.find(event); it != handlers.end()) {
            for (auto& handler : it->second) {
                handler(data);
            }
        }
    }
};

int main() {
    EventBus bus;
    
    bus.on("message", [](std::any data) {
        auto msg = std::any_cast<std::string>(data);
std::cout << "Message: " << msg << std::endl;
    });
    
    bus.on("count", [](std::any data) {
        auto count = std::any_cast<int>(data);
std::cout << "Count: " << count << std::endl;
    });
    
    bus.emit("message", std::string{"Hello"});
    bus.emit("count", 42);
}

Each handler receives std::any by value, so every emitted event is copied once per handler — cheap for an int, noticeable for a large payload with many subscribers. Taking const std::any& in the handler signature and reading with std::any_cast<const std::string&> avoids those copies. The bigger risk is that the event name and the payload type are linked only by convention: bus.emit("count", 42u) compiles and throws inside the handler at run time. The typed MessageBus further down addresses this by checking the type before calling the handler.

Type-safe wrapper

#include <any>

class SafeAny {
    std::any data;
    
public:
    template<typename T>
    void set(const T& value) {
        data = value;
    }
    
    template<typename T>
    std::optional<T> get() const {
        try {
            return std::any_cast<T>(data);
        } catch (const std::bad_any_cast&) {
            return std::nullopt;
        }
    }
    
    bool empty() const {
        return !data.has_value();
    }
};

int main() {
    SafeAny sa;
    sa.set(42);
    
    if (auto val = sa.get<int>()) {
std::cout << "Value: " << *val << std::endl;
    }
    
    if (auto val = sa.get<double>()) {
        std::cout << "double" << std::endl;
    } else {
std::cout << "Type mismatch" << std::endl;
    }
}

Using try/catch for an expected outcome — “this might be another type” — is the expensive way to write this. The pointer form of any_cast expresses the same thing without exceptions: if (auto* p = std::any_cast<T>(&data)) return *p; return std::nullopt;. It is also usable in code built with -fno-exceptions, where the throwing overloads are not an option.

Reading the Stored Value

std::any a = 42;

// any_cast: value
int x = std::any_cast<int>(a);

// any_cast: pointer
if (int* ptr = std::any_cast<int>(&a)) {
    std::cout << *ptr << std::endl;
}

// any_cast: reference
int& ref = std::any_cast<int&>(a);

Type Mismatches and Non-Copyable Types

Type mismatch

std::any a = 42;

// ❌ Wrong type
try {
    double d = std::any_cast<double>(a);  // std::bad_any_cast
} catch (const std::bad_any_cast&) {
std::cout << "Type mismatch" << std::endl;
}

// ✅ Access after confirmation
if (a.type() == typeid(int)) {
    int x = std::any_cast<int>(a);
}

References

std::any a = 42;

// ❌ Copy
int x = std::any_cast<int>(a);

// ✅ See also
int& ref = std::any_cast<int&>(a);
ref = 100;

std::cout << std::any_cast<int>(a) << std::endl;  // 100

Performance

// any has overhead
// - Save type information
// - Dynamic allocation (large objects)
// - Type check

// ✅ Alternative: variant (if type is known)
std::variant<int, double, std::string> v;

Non-copyable types

struct NonCopyable {
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
};

// ❌ Non-copyable type: std::any requires a copy-constructible type,
//    so neither of these compiles (moving or make_any doesn't help)
// std::any a = NonCopyable{};
// std::any b = std::make_any<NonCopyable>();

// ✅ Store it behind a copyable handle instead
std::any c = std::make_shared<NonCopyable>();
auto p = std::any_cast<std::shared_ptr<NonCopyable>>(c);

std::any copies its contents when the any itself is copied, so the standard requires the stored type to be copy-constructible — even std::make_any and emplace are ill-formed for a move-only type. If you need type-erased storage for move-only objects, keep them behind a std::shared_ptr (as above) or use a std::variant of the concrete types.

The Same Value in any and variant

// any: any type (runtime)
std::any a = 42;
a = std::string{"hello"};

// variant: defined type (compile time)
std::variant<int, std::string> v = 42;
v = std::string{"hello"};

// variant recommended (if type is known)

Plugin Systems, Message Buses, and Dynamic Properties

Plugin system

class Plugin {
public:
    virtual ~Plugin() = default;
    virtual std::string getName() const = 0;
    virtual std::any execute(const std::any& input) = 0;
};

class PluginManager {
    std::map<std::string, std::unique_ptr<Plugin>> plugins_;
    
public:
    void registerPlugin(std::unique_ptr<Plugin> plugin) {
        plugins_[plugin->getName()] = std::move(plugin);
    }
    
    std::any execute(const std::string& name, const std::any& input) {
        if (auto it = plugins_.find(name); it != plugins_.end()) {
            return it->second->execute(input);
        }
throw std::runtime_error("No plugin found");
    }
};

// Plugin implementation
// Note: std::any must not cross a shared-library boundary between modules built
// with different compilers or standard libraries (type identity and layout differ).
class CalculatorPlugin : public Plugin {
public:
    std::string getName() const override {
        return "calculator";
    }
    
    std::any execute(const std::any& input) override {
        auto values = std::any_cast<std::vector<int>>(input);
        int sum = 0;
        for (int v : values) {
            sum += v;
        }
        return sum;
    }
};

The plugin interface is where any looks most attractive and where its limits show. Within one executable it works well. Across dynamically loaded plugins, any_cast relies on comparing type_info objects, and whether two modules agree that their std::vector<int> is “the same type” depends on symbol visibility and platform: on Linux it usually works when both are built with the same toolchain, while with DLLs on Windows or with -fvisibility=hidden a cast can fail even though the types are spelled identically. Plugin systems that must work across compilers typically use a C ABI with plain data or a serialization format instead.

Type-safe message bus

class MessageBus {
    struct Handler {
        std::function<void(const std::any&)> callback;
        std::type_index expectedType;
    };
    
    std::map<std::string, std::vector<Handler>> handlers_;
    
public:
    template<typename T>
    void subscribe(const std::string& topic, std::function<void(const T&)> callback) {
        handlers_[topic].push_back({
            [callback](const std::any& data) {
                callback(std::any_cast<const T&>(data));
            },
            std::type_index(typeid(T))
        });
    }
    
    template<typename T>
    void publish(const std::string& topic, const T& data) {
        if (auto it = handlers_.find(topic); it != handlers_.end()) {
            for (auto& handler : it->second) {
                if (handler.expectedType == std::type_index(typeid(T))) {
                    try {
                        handler.callback(std::any(data));
                    } catch (const std::bad_any_cast& e) {
std::cerr << "Type mismatch: " << e.what() << '\n';
                    }
                }
            }
        }
    }
};

// use
MessageBus bus;
bus.subscribe<std::string>("log", [](const std::string& msg) {
std::cout << "log: " << msg << '\n';
});
bus.publish("log", std::string{"Hello"});

Storing std::type_index with each handler turns a mismatch into a skipped handler instead of an exception, and subscribe<T> makes the expected type explicit at the subscription site. The remaining weakness is that the check is still at run time and still exact: bus.publish("log", "Hello") publishes a const char*, matches no handler, and the message silently disappears. In a system I would build on this, publishing to a topic with no matching handler would at least log a warning in debug builds, because silent drops are much harder to diagnose than exceptions.

Dynamic property system

class Entity {
    std::map<std::string, std::any> properties_;
    
public:
    template<typename T>
    void setProperty(const std::string& name, const T& value) {
        properties_[name] = value;
    }
    
    template<typename T>
    std::optional<T> getProperty(const std::string& name) const {
        auto it = properties_.find(name);
        if (it == properties_.end()) {
            return std::nullopt;
        }
        
        try {
            return std::any_cast<T>(it->second);
        } catch (const std::bad_any_cast&) {
            return std::nullopt;
        }
    }
    
    bool hasProperty(const std::string& name) const {
        return properties_.count(name) > 0;
    }
};

// use
Entity player;
player.setProperty("health", 100);
player.setProperty("name", std::string{"Hero"});
player.setProperty("position", std::vector<double>{10.0, 20.0});

if (auto health = player.getProperty<int>("health")) {
std::cout << "Health: " << *health << '\n';
}

FAQ

Q1: What is any?

A: A type erasure container in C++17 that can store values of any type. Check the type and extract the value at runtime.

std::any a = 42;
a = 3.14;
a = std::string{"hello"};

Q2: Where is any used?

A:

  • Heterogeneous containers: Store various types in one container
  • Plugin system: Passing data between plugins
  • Settings Storage: Stores various types of setting values
  • Event System: Various types of event data
std::vector<std::any> data;
data.push_back(42);
data.push_back(3.14);
data.push_back(std::string{"hello"});

Q3: How do I access the values?

A: Use std::any_cast. In case of type mismatch, an exception is thrown.

std::any a = 42;

// Extract value (exceptions possible)
int x = std::any_cast<int>(a);

// Pointer extraction (safe)
if (int* ptr = std::any_cast<int>(&a)) {
    std::cout << *ptr << '\n';
}

// Reference extraction
int& ref = std::any_cast<int&>(a);

Q4: What is the performance of any?

A: There is overhead. There are costs of storing type information, dynamic allocation (large objects), and type checking.

// any: overhead
std::any a = 42;  // Type information + value storage

// variant: faster (if type is known)
std::variant<int, double, std::string> v = 42;

Recommended: Use variant if type is known in advance

Q5: What is the difference from variant?

A:

  • any: Any type possible, runtime type check, slow
  • variant: only defined types, compile-time type check, fast
// any: any type
std::any a = 42;
a = std::vector<int>{1, 2, 3};  // OK

// variant: only certain types
std::variant<int, double> v = 42;
// v = std::vector<int>{};  // error

Selection criteria:

  • If the type is known in advance: variant
  • If the type is not known in advance: any

Q6: Can any store a reference?

A: Not possible directly, but you can use std::reference_wrapper.

int x = 42;

// ❌ Cannot save reference
// std::any a{x};  // copied

// ✅ Use reference_wrapper
std::any a = std::ref(x);
std::reference_wrapper<int> ref = std::any_cast<std::reference_wrapper<int>>(a);
ref.get() = 100;
std::cout << x << '\n';  // 100

Q7: What is the memory allocation for any?

A: Uses Small Object Optimization (SOO). Small objects are stored inside the any object and large objects are stored on the heap.

// Small objects: inline buffer (size is implementation-defined, one to a few pointers)
std::any a1 = 42;  // no allocation

// Large objects: heap
std::any a2 = std::vector<int>(1000);  // the vector object may be stored inline or on the heap;
                                       // its 1000 elements are on the heap either way

Q8: Any learning resources?

A:

std::any is a C++17 type-erasing container that can store any type.