Skip to Content
Course content

78: std::any for Type-Erased Storage

Click on the "Edit" button in the top corner of the screen to edit your slide content.

At some point in your career, you'll run into a requirement where you need to store "something," but you have absolutely no idea what that "something" is at compile time. Maybe you're building a property system for a game engine, or a plugin architecture where third parties provide their own data structures. In a perfect world, we'd use a template, but templates require the type to be known when the code is compiled. When the type is only known at runtime, we have to talk about type erasure.

The void* Gamble

If you look at older C++ code—or C code—the instinctive move is to use void*. It's the ultimate "I don't care" pointer. You cast your object to void* to store it, and then you cast it back to the original type when you need to use it. I've seen this in countless legacy systems, and it's a nightmare to maintain.

struct Property {
    void* value;
    int typeId; // Manually tracking what this is
};

// To use it:
if (prop.typeId == TYPE_INT) {
    int val = *static_cast<int*>(prop.value);
}

The problem here is that void* is blind. It doesn't know how to call a destructor, it doesn't know the size of the object it's pointing to, and it certainly won't stop you from casting a float to a std::string and crashing your entire application. You're essentially telling the compiler, "Trust me, I know what I'm doing," and in a large codebase, you eventually stop trusting yourself.

Type-Safe Erasure with std::any

This is where std::any comes in. Introduced in C++17, it gives us a way to store any CopyConstructible type while maintaining a level of type safety that void* completely ignores. Think of it as a type-safe container that remembers what it's holding.

Let's look at how we'd implement that same property system using std::any. Notice how we no longer need a manual typeId; the container handles the metadata for us.

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

struct GameObject {
    std::map<std::string, std::any> properties;
};

void printProperty(const std::string& name, const std::any& value) {
    if (value.type() == typeid(int)) {
        std::cout < name < ": " < std::any_cast<int>(value) < "\n";
    } else if (value.type() == typeid(std::string)) {
        std::cout < name < ": " < std::any_cast<std::string>(value) < "\n";
    } else {
        std::cout < name < ": [Unknown Type]\n";
    }
}

int main() {
    GameObject player;
    player.properties["health"] = 100; 
    player.properties["name"] = std::string("Hero");

    printProperty("health", player.properties["health"]);
    printProperty("name", player.properties["name"]);
}

The magic here is std::any_cast. If you try to cast the "health" property (an int) to a std::string, std::any_cast won't just give you garbage data; it will throw a std::bad_any_cast exception. I prefer this a thousand times over a random segmentation fault because it tells me exactly where my logic failed.

The Cost of Flexibility

Now, before you start replacing every std::variant or base class pointer with std::any, we need to talk about the trade-offs. std::any isn't free. Because it can hold absolutely anything, it usually has to allocate memory on the heap to store the object (though most implementations have a "small object optimization" for types like int or bool).

You're also trading compile-time checks for runtime checks. With a std::variant, the compiler forces you to handle all possible types. With std::any, you're basically guessing at runtime. I generally follow this rule of thumb: if you know the 5 or 6 possible types your variable could be, use std::variant. If you are building a system where you literally cannot know the types until a user loads a plugin or a config file, std::any is your best friend.




📋 Practical Task

Implementing a Dynamic Component Property Store

You are tasked with creating a ComponentStore class that allows different game components to store arbitrary settings. Instead of using a rigid class hierarchy, you will use std::any to allow for maximum flexibility.

Requirements:

  • Create a class ComponentStore that contains a std::map<std::string, std::any>.
  • Implement a method void setProperty(const std::string& key, std::any value) to add or update properties.
  • Implement a template method template <typename T> T getProperty(const std::string& key). This method should attempt to return the value cast to type T. If the key doesn't exist or the type is incorrect, it should throw a descriptive exception or return a default-constructed T.
  • In your main function, demonstrate the store by adding an int for "strength", a float for "speed", and a custom struct Vec3 { float x, y, z; } for "position".
  • Try to retrieve the "strength" property as a std::string and wrap it in a try-catch block to handle the std::bad_any_cast.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.