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:
| Features | std::any | std::variant | std::optional |
|---|---|---|---|
| Storage type | All types | fixed type | Single type |
| type tracking | runtime | compile time | N/A |
| memory | heap (large object) | stack | stack |
| Performance | slow | Fast | Very fast |
| Type Safe | runtime | compile time | compile time |
| Use | Plugins, Settings | state machine, error | null 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_castis undefined behavior, and unpaireddeleteis immediately UB. - Summary: Although still used for opaque buffers with fixed layouts (e.g. FFI buffers),
std::anyor 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_alternativewill 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_castis 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 matchesT(there are reference·cv qualifier rules). If you try to insertintand take it out withlong, it will fail.type()returnstype_info, so theif (a.type() == typeid(Foo))pattern is possible, but maintenance is better left toany_castat once.- At interface boundaries, it is safe to limit types to
variantor dedicated base classes whenever possible, and to keepanyonly 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:
- Type information:
type_infosearch, internal comparison whenany_cast. - 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.
- Copy/Move: If you frequently copy
anyitself, 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 typein the first place. - In configuration maps, etc., check whether a strongly typed struct** or
toml/jsonparser result type is better than astringkey +anyvalue.
Summary of practical use
| Situation | Why any is suitable |
|---|---|
| Script/plugin passes arbitrary type payload | Difficult 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 bus | Branch to any_cast in handler |
| Test mock object/mock dependency | Used 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:
- “C++17 The Complete Guide” by Nicolai Josuttis
- “Effective Modern C++” by Scott Meyers
- cppreference.com - std::any
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.
Related articles
- 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 |