C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
78: std::any for Type-Erased Storage
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
ComponentStorethat contains astd::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 typeT. If the key doesn't exist or the type is incorrect, it should throw a descriptive exception or return a default-constructedT. - In your
mainfunction, demonstrate the store by adding anintfor "strength", afloatfor "speed", and a customstruct Vec3 { float x, y, z; }for "position". - Try to retrieve the "strength" property as a
std::stringand wrap it in atry-catchblock to handle thestd::bad_any_cast.
There are no comments for now.