본문으로 건너뛰기 C++ std::any vs void* Complete Comparison

C++ std::any vs void* Complete Comparison

C++ std::any vs void* Complete Comparison

이 글의 핵심

Master C++ type erasure: std::any (type-safe, runtime checks) vs void* (unsafe, manual casting). Complete comparison with use cases, performance, and when to choose each.

Overview

Both std::any and void* provide type erasure—storing values of any type—but with vastly different safety guarantees.

// std::any (C++17, type-safe)
std::any value = 42;
int x = std::any_cast<int>(value);  // Runtime type check
// void* (C, unsafe)
void* ptr = new int(42);
int y = *(int*)ptr;  // No type check, dangerous!

Key difference: std::any remembers the stored type and checks it at runtime; void* forgets the type entirely.

std::any

Basic Usage

#include <any>
#include <iostream>
int main() {
    std::any value;
    
    // Store int
    value = 42;
    cout << std::any_cast<int>(value) << endl;  // 42
    
    // Store string
    value = std::string("hello");
    cout << std::any_cast<std::string>(value) << endl;  // hello
    
    // Store custom type
    struct Point { int x, y; };
    value = Point{10, 20};
    auto p = std::any_cast<Point>(value);
    cout << p.x << ", " << p.y << endl;  // 10, 20
}

Output:

42
hello
10, 20

Type Checking

std::any value = 42;
// Check type
if (value.type() == typeid(int)) {
    cout << "It's an int" << endl;
}
// has_value()
if (value.has_value()) {
    cout << "Has value" << endl;
}
// Wrong type cast → exception
try {
    auto s = std::any_cast<std::string>(value);  // Throws!
} catch (const std::bad_any_cast& e) {
    cerr << "Bad cast: " << e.what() << endl;
}

Output:

It's an int
Has value
Bad cast: bad any_cast

Safe Casting

std::any value = 42;
// Pointer cast (returns nullptr on failure)
int* ptr = std::any_cast<int>(&value);
if (ptr) {
    cout << *ptr << endl;  // 42
}
// Wrong type → nullptr
std::string* strPtr = std::any_cast<std::string>(&value);
if (!strPtr) {
    cout << "Not a string" << endl;
}

Output:

42
Not a string

void*

Basic Usage

void* ptr = new int(42);
// Cast back to int*
int* intPtr = (int*)ptr;
cout << *intPtr << endl;  // 42
delete intPtr;

Warning: No type safety—you must remember the original type.

Dangerous Scenarios

// ❌ Wrong type cast (undefined behavior)
void* ptr = new int(42);
double* dblPtr = (double*)ptr;  // Wrong type!
cout << *dblPtr << endl;  // Garbage or crash
// ❌ Forgot to delete (memory leak)
void* ptr = new int(42);
// ... ptr goes out of scope, never deleted
// ❌ Double delete
void* ptr = new int(42);
delete (int*)ptr;
delete (int*)ptr;  // Crash!

C API Example

// C API using void*
void processData(void* userData, void (*callback)(void*)) {
    // ... processing ...
    callback(userData);
}
// Usage
struct Context {
    int id;
    std::string name;
};
void myCallback(void* ptr) {
    Context* ctx = (Context*)ptr;  // Manual cast
    cout << ctx->id << ", " << ctx->name << endl;
}
int main() {
    Context ctx{1, "Alice"};
    processData(&ctx, myCallback);
}

Output:

1, Alice

Comparison

Feature Comparison

Featurestd::anyvoid*
Type safety✅ Runtime checks❌ No checks
Exception on wrong cast✅ Yes❌ No (UB)
Type info stored✅ Yes❌ No
Memory management✅ Automatic❌ Manual
Small object optimization✅ Yes❌ No
C compatibility❌ No✅ Yes
PerformanceGoodSlightly faster

Safety Comparison

// std::any: Safe
std::any value = 42;
try {
    auto s = std::any_cast<std::string>(value);  // Throws bad_any_cast
} catch (const std::bad_any_cast&) {
    cout << "Caught wrong type" << endl;
}
// void*: Unsafe
void* ptr = new int(42);
std::string* s = (std::string*)ptr;  // Compiles, but UB!
cout << *s << endl;  // Crash or garbage

Performance Comparison

// Benchmark: 1,000,000 operations
// std::any: 15ms (type info overhead)
// void*: 10ms (no overhead)

Key: void* is slightly faster, but the difference is negligible in most applications.

When to Use Each

Use std::any When:

  • ✅ Type safety is important
  • ✅ Storing heterogeneous data in containers
  • ✅ Building modern C++ APIs
  • ✅ Runtime type checks needed
// Event system
struct Event {
    std::string type;
    std::any data;
};
void handleEvent(const Event& e) {
    if (e.type == "click") {
        auto pos = std::any_cast<Point>(e.data);
        // ...
    }
}

Use void* When:

  • ✅ Interfacing with C APIs
  • ✅ Legacy code maintenance
  • ✅ Extreme performance requirements
  • ✅ Custom memory management
// C API callback
void setCallback(void* userData, void (*callback)(void*)) {
    // ...
}

Common Mistakes

Mistake 1: Forgetting Type with void*

// ❌ Lost type information
void* ptr = new std::string("hello");
// ... later ...
int* intPtr = (int*)ptr;  // Wrong type! UB

Fix: Use std::any or document type carefully.

Mistake 2: Memory Leak with void*

// ❌ Memory leak
void* ptr = new int(42);
// ... forgot to delete
// ✅ Use smart pointer
std::unique_ptr<void, void(*)(void*)> ptr(
    new int(42),
    [](void* p) { delete (int*)p; }
);

Fix: Use RAII or std::any.

Mistake 3: Wrong any_cast

// ❌ Value cast throws on failure
std::any value = 42;
auto s = std::any_cast<std::string>(value);  // Throws!
// ✅ Pointer cast returns nullptr on failure
std::string* s = std::any_cast<std::string>(&value);
if (!s) {
    cout << "Not a string" << endl;
}

Practical Examples

Example 1: Heterogeneous Container

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

Output:

int: 42
string: hello
double: 3.14

Example 2: Plugin System

// Plugin interface
class Plugin {
public:
    virtual ~Plugin() = default;
    virtual void execute(std::any context) = 0;
};
class LogPlugin : public Plugin {
public:
    void execute(std::any context) override {
        if (context.type() == typeid(std::string)) {
            auto msg = std::any_cast<std::string>(context);
            cout << "Log: " << msg << endl;
        }
    }
};
int main() {
    LogPlugin plugin;
    plugin.execute(std::string("Hello"));
}

Output:

Log: Hello

Example 3: C API Wrapper

// C API
extern "C" {
    void c_callback(void* userData);
}
// C++ wrapper
class CallbackWrapper {
    std::function<void()> func;
    
public:
    CallbackWrapper(std::function<void()> f) : func(f) {}
    
    static void invoke(void* ptr) {
        auto* wrapper = (CallbackWrapper*)ptr;
        wrapper->func();
    }
    
    void* getUserData() {
        return this;
    }
};
void registerCallback() {
    CallbackWrapper wrapper([]() {
        cout << "Callback invoked" << endl;
    });
    
    c_callback(wrapper.getUserData());
}

Production Patterns

Pattern 1: Event System with std::any

class EventBus {
    unordered_map<string, vector<function<void(std::any)>>> handlers;
    
public:
    void subscribe(const string& eventType, function<void(std::any)> handler) {
        handlers[eventType].push_back(handler);
    }
    
    void publish(const string& eventType, std::any data) {
        for (auto& handler : handlers[eventType]) {
            handler(data);
        }
    }
};
struct ClickEvent { int x, y; };
int main() {
    EventBus bus;
    
    bus.subscribe("click", [](std::any data) {
        auto evt = std::any_cast<ClickEvent>(data);
        cout << "Click at " << evt.x << ", " << evt.y << endl;
    });
    
    bus.publish("click", ClickEvent{100, 200});
}

Output:

Click at 100, 200

Pattern 2: Config System

class Config {
    unordered_map<string, std::any> values;
    
public:
    template<typename T>
    void set(const string& key, const T& value) {
        values[key] = value;
    }
    
    template<typename T>
    T get(const string& key, const T& defaultValue = T{}) {
        auto it = values.find(key);
        if (it == values.end()) {
            return defaultValue;
        }
        
        try {
            return std::any_cast<T>(it->second);
        } catch (const std::bad_any_cast&) {
            return defaultValue;
        }
    }
};
int main() {
    Config cfg;
    cfg.set("port", 8080);
    cfg.set("host", std::string("localhost"));
    cfg.set("debug", true);
    
    cout << cfg.get<int>("port") << endl;        // 8080
    cout << cfg.get<string>("host") << endl;     // localhost
    cout << cfg.get<bool>("debug") << endl;      // 1
    cout << cfg.get<int>("missing", 9000) << endl;  // 9000 (default)
}

Output:

8080
localhost
1
9000

Summary

Key Points

  1. std::any: Type-safe, runtime checks, exceptions
  2. void*: Unsafe, manual casting, no checks
  3. Use std::any: For modern C++ code
  4. Use void*: For C API interop, legacy code
  5. Performance: void* slightly faster, but std::any overhead is minimal
  6. Safety: std::any prevents undefined behavior

Decision Matrix

ScenarioRecommendation
Modern C++ APIstd::any
C API interopvoid*
Type safety criticalstd::any
Legacy codebasevoid* (migrate to std::any)
Extreme performancevoid* (measure first)
Heterogeneous containersstd::any or std::variant

Migration Path

// Old (void*)
void* data = new int(42);
int value = *(int*)data;
delete (int*)data;
// New (std::any)
std::any data = 42;
int value = std::any_cast<int>(data);
// Automatic cleanup

Keywords

C++ std::any, void pointer, type erasure, type safety, runtime type checking, std::any_cast, bad_any_cast One-line summary: std::any provides type-safe type erasure with runtime checks, while void* offers unsafe manual casting—prefer std::any for modern C++ code.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Master C++ type erasure: std::any (type-safe, runtime checks, exceptions) vs void* (unsafe, manual casting, legacy). Com…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, std::any, void pointer, type erasure, type safety, C++17.