Everything you need to know about std :: any

Hello, Habr! We present to your attention a translation of the article “Everything You Need to Know About std :: any from C ++ 17” by Bartlomiej Filipek .

image

With the help of std::optionalyou can store one kind of type. With the help of std::variantyou can store several types in one object. And C ++ 17 provides us with another such wrapper type - std::anywhich can store anything while remaining type-safe.

The basics


Prior to this, the C ++ standard did not provide many solutions to the problem of storing several types in one variable. Of course you can use void*, but it is not at all safe.

Theoretically, void*you can wrap it in a class where you can somehow store the type:

class MyAny
{
    void* _value;
    TypeInfo _typeInfo;
};

As you can see, we got a certain basic form std::any, but to ensure type safety MyAnywe need additional checks. That is why it is better to use an option from the standard library than to make your own decision.

And this is what it is std::anyfrom C ++ 17. It allows you to store anything in the object and reports an error (throws an exception) when you try to access by specifying the wrong type.

Little demo:

std::any a(12);

//    :
a = std::string("Hello!");
a = 16;
//   :

//    a  
std::cout << std::any_cast<int>(a) << '\n'; 

//    :
try 
{
    std::cout << std::any_cast<std::string>(a) << '\n';
}
catch(const std::bad_any_cast& e) 
{
    std::cout << e.what() << '\n';
}

//        - :
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

//    any  :
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;

for (auto &[key, val] : m)
{
    if (val.type() == typeid(int))
        std::cout << "int: " << std::any_cast<int>(val) << "\n";
    else if (val.type() == typeid(std::string))
        std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
    else if (val.type() == typeid(float))
        std::cout << "float: " << std::any_cast<float>(val) << "\n";
}

This code will output:

16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World

The example above shows some important things:

  • std::anystd::optional std::variant
  • , .has_value()
  • .reset()
  • std::decay
  • ,
  • std::any_cast, bad_any_cast, «T»
  • .type(), std::type_info

The example above looks impressive - a real type variable in C ++! If you really like JavaScript, you can even make all your type variables std::anyand use C ++ as JavaScript :)

But maybe there are some normal usage examples?

When to use?


While it is void*perceived by me as a very unsafe thing with a very limited range of possible uses, it is std::anycompletely type-safe, so it has some good ways to use it.

For instance:

  • In libraries - when your library needs to store or transfer some data, and you do not know what type this data can be
  • When parsing files - if you really can’t determine what types are supported
  • Messaging
  • Scripting Language Interaction
  • Creating an interpreter for a scripting language
  • User Interface - Fields Can Store Anything

It seems to me that in many of these examples we can highlight a limited list of supported types, so it std::variantmay be a better choice. But of course it is difficult to create libraries without knowing the final products in which it will be used. You just don’t know what types will be stored there.

The demonstration showed some basic things, but in the following sections you will learn more about std::any, so keep reading.

Create std :: any


There are several ways to create an object of type std::any:

  • standard initialization - object is empty
  • direct initialization with value / object
  • directly indicating the type of object - std::in_place_type
  • via std::make_any

For instance:

//  :
std::any a;
assert(!a.has_value());

//   :
std::any a2(10); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
std::any a6 = std::make_any<std::string>("Hello World");

Change value


There std::anyare two ways to change the value that is currently stored in : method emplaceor assignment:

std::any a;

a = MyType(10, 11);
a = std::string("Hello");

a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);

Object Life Cycle


The key to security std::anyis the lack of leakage of resources. To achieve this, it will std::anydestroy any active object before assigning a new value.

std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";

This code will output the following:

MyType::MyType
MyType::~MyType
100

The object is std::anyinitialized with an object of type MyType, but before assigning a new value (100.0f), the destructor is called MyType.

Gaining access to a value


In most cases, you have only one way to access the value in std::any- std::any_cast, it returns the values ​​of the specified type if it is stored in the object.

This feature is very useful, as it has many ways to use it:

  • return a copy of the value and quit std::bad_any_caston error
  • return a link to the value and quit std::bad_any_cast on error
  • return a pointer to a value (constant or not) or nullptr in case of an error

See an example:

struct MyType
{
    int a, b;

    MyType(int x, int y) : a(x), b(y) { }

    void Print() { std::cout << a << ", " << b << "\n"; }
};

int main()
{
    std::any var = std::make_any<MyType>(10, 10);
    try
    {
        std::any_cast<MyType&>(var).Print();
        std::any_cast<MyType&>(var).a = 11; // /
        std::any_cast<MyType&>(var).Print();
        std::any_cast<int>(var); // throw!
    }
    catch(const std::bad_any_cast& e) 
    {
        std::cout << e.what() << '\n';
    }

    int* p = std::any_cast<int>(&var);
    std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");

    MyType* pt = std::any_cast<MyType>(&var);
    if (pt)
    {
        pt->a = 12;
        std::any_cast<MyType&>(var).Print();
    }
}

As you can see, we have two ways to track errors: through exceptions ( std::bad_any_cast) or returning a pointer (or nullptr). The function std::any_castfor returning pointers is overloaded and marked as noexcept.

Performance and memory usage


std::anyIt looks like a powerful tool, and you will most likely use it to store data of different types, but what is the price of it?

The main problem is the extra memory allocation.

std::variant and std::optionaldoes not require any additional memory allocations, but this is because the types of data stored in the object are known in advance. std :: any does not have such information, so it can use additional memory.

Will this happen always or sometimes? Which rules? Will this even happen with simple types like int?

Let's see what the standard says:
Implementations should avoid the use of dynamically allocated memory for a small contained value. Example: where the object constructed is holding only an int. Such small-object optimization shall only be applied to types T for which is_nothrow_move_constructible_v is true
An implementation should avoid using dynamic memory for small-sized stored data. For example, when an object is created storing only int. Such optimization for small objects should only be applied to types T for which is_nothrow_move_constructible_v is true.

As a result, they propose to use Small Buffer Optimization / SBO for implementations. But this also has a price. This makes the type larger - to cover the buffer.

Let's look at the size std::any, here are the results from several compilers:

Compilersizeof (any)
GCC 8.1 (Coliru)sixteen
Clang 7.0.0 (Wandbox)32
MSVC 2017 15.7.0 32-bit40
MSVC 2017 15.7.0 64-bit64

In general, as you can see, std::anythis is not a simple type, and it brings additional costs. It usually takes up a lot of memory, due to SBO, from 16 to 32 bytes (in GCC or clang ... or even 64 bytes in MSVC!).

Migrating from boost :: any


boost::anyIt was introduced somewhere in 2001 (version 1.23.0). In addition, the author boost::any(Kevlin Henney) is also the author of the proposal std::any. Therefore, these two types are closely related, the version from STL is strongly based on its predecessor.

Here are the main changes:

FunctionBoost.Any
(1.67.0)
std :: any
Additional memory allocationYesYes
Small Object OptimizationnoYes
emplacenoYes
in_place_type_t in the constructornoYes


The main difference is that it boost::anydoes not use SBO, so it takes up significantly less memory (in GCC8.1 its size is 8 bytes), but because of this, it dynamically allocates memory even for such small types as int.

Examples of using std :: any


The main plus std::anyis flexibility. In the examples below, you can see some ideas (or specific implementations) where using std::anyit makes the application a little easier.

File parsing


In the examples to std::variant ( you can see them here [eng] ) you could see how you can parse configuration files and store the result in a type variable std::variant. Now you are writing a very general solution, maybe it is part of some library, then you may not be aware of all the possible types of types.

Storing data using std::anyfor parameters is likely to be quite good in terms of performance, and at the same time give you the flexibility of the solution.

Messaging


In Windows Api, which is mainly written in C, there is a messaging system that uses message id with two optional parameters that store message data. Based on this mechanism, you can implement WndProc, which processes the message sent to your window.

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

The fact is that the data is stored in wParamor lParamin different forms. Sometimes you only need to use a couple of bytes wParam.

What if we change this system so that the message can pass anything to the processing method?

For instance:

class Message
{
public:
    enum class Type 
    {
        Init,
        Closing,
        ShowWindow,        
        DrawWindow
    };

public:
    explicit Message(Type type, std::any param) :
        mType(type),
        mParam(param)
    {   }
    explicit Message(Type type) :
        mType(type)
    {   }

    Type mType;
    std::any mParam;
};

class Window
{
public:
    virtual void HandleMessage(const Message& msg) = 0;
};

For example, you can send a message to the window:

Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);

A window can reply to a message like this:

switch (msg.mType) {
// ...
case Message::Type::ShowWindow:
    {
    auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
    std::cout << "ShowWidow: "
              << pos.first << ", " 
              << pos.second << "\n";
    break;
    }
}

Of course, you have to determine how the data type is stored in messages, but now you can use real types instead of different tricks with numbers.

Properties


The original document that any represents for C ++ (N1939) shows an example of a property object:

struct property
{
    property();
    property(const std::string &, const std::any &);

    std::string name;
    std::any value;
};

typedef std::vector<property> properties;

This object looks very useful because it can store many different types. The first one that comes to my mind is an example of using it in a user interface manager or in a game editor.

We pass through the borders


In r / cpp there was a stream about std :: any. And there was at least one great comment that summarizes when a type should be used.

From this comment :
The bottom line is that std :: any allows you to transfer rights to arbitrary data across borders that do not know about its type.
Everything I talked about before is close to this idea:

  • in the library for the interface: you do not know what types the client wants to use there
  • messaging: the same idea - give the customer flexibility
  • file parsing: to support any type

Total


In this article, we learned a lot about std::any!

Here are some things to keep in mind:

  • std::any not a template class
  • std::any uses optimization of small objects, so it will not dynamically allocate memory for simple types such as int or double, and for larger types, additional memory will be used
  • std::any can be called "heavy", but it offers safety and greater flexibility
  • Access to the data std::anycan be obtained with the help of any_castwhich offers several "modes". For example, in case of an error, it may throw an exception or just return nullptr
  • use it when you don’t know exactly what data types are possible, otherwise consider using std::variant

All Articles