C++ any | "Type Erasing" Guide

C++ any | "Type Erasing" Guide

이 글의 핵심

We summarize the cast, safety, performance of type-elimination any, and selection criteria for variants from a practical perspective.

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 on the stack and large objects are stored on the heap (Small Object Optimization).

// 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
memoryheap (large object)stackstack
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;

any vs variant vs void* In-depth

void*

  • Advantages: Any address can be stored, and integration with the C API seems simple.
  • Disadvantage: Ownership/Lifetime/Actual Type depends only on code contract. An invalid static_cast is undefined behavior, and unpaired delete is immediately UB.
  • Summary: Although still used for opaque buffers with fixed layouts (e.g. FFI buffers), std::any or smart pointer+interfaces are safe for holding owned objects.

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.
  • Memory is usually placed in fixed size buffers and often operates without heap allocation (implementation dependent).

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.
  • 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.

any_cast Completely organized

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

Types that cannot be copied or can only be moved are entered with std::make_any<T>(...), and when taken out, use the std::any_cast<T&>(a) or any_cast<T>(std::move(a)) pattern according to the type.

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.
  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.

Summary of practical use

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.

Default use

#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;

Practical example

Example 1: 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;
        }
    }
}

Example 2: 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");
}

Example 3: 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);
}

Example 4: 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;
    }
}

Accessing 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);

Frequently occurring problems

Problem 1: 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);
}

Issue 2: 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

Issue 3: 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;

Problem 4: Copy

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

// ❌ Non-copyable type
// std::any a = NonCopyable{};  // error

// ✅ Move
std::any a = std::make_any<NonCopyable>();

any vs 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)

Practice pattern

Pattern 1: 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
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;
    }
};

Pattern 2: 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"});

Pattern 3: 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 on the stack and large objects are stored on the heap.

// Small objects: stack (usually no more than 16-32 bytes)
std::any a1 = 42;  // stack

// Large objects: heap
std::any a2 = std::vector<int>(1000);  // hip

Q8: Any learning resources?

A:

Related posts: variant, optional, type_erasure.

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


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ Type Erasure | “Type erasure” pattern guide
  • C++ optional | “Optional Values” Guide
  • C++ variant | “Type-safe union” guide

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, any, type-erasure, runtime, C++17, etc.


  • C++ std::any vs void* |
  • C++ Type Erasure |
  • Modern C++ (C++11~C++20) Core Grammar Cheat Sheet | A glance at frequently used items in the workplace
  • C++ CTAD |
  • C++ string vs string_view |