Use Modern C++ std::any in your projects

🕳️ Say goodbye to void* once and for all.


Use Modern C++ std::any in your projects


std::any is a feature of the C++ standard library that was introduced in C++17.

This component belongs to the set of type-safe container classes, providing a safe means to store and manipulate values of any type.

It is especially useful when you need to deal with situations where the type of the variable can vary! 😃

Then you say:

- Oh man! Good. For these cases I use void *.

Yes, you’re really right, but have you seen how the new generation is in relation to memory safety???

Not to mention that void* is really dangerous!

If you do this, it works:

void * some_data; // Bad idea std::string str = "Hi"; int x = 3; decltype(x) y = 6; some_data = &str; std::cout << *(std::string*)some_data << '\n'; some_data = &x; std::cout << *(int*)some_data << '\n'; some_data = &y; std::cout << "Type of y: " << typeid(y).name() << '\n'; // include typeinfo

But, the chance of this giving s%1t is great! At the end of using these variables, some_data will continue to exist, that is, an indefinite lifetime!

And it is to replace void* that std::any was created in Modern C++ which, of course, is totally Safe!

In other words, it is a wrapper that encapsulates your variable to a shared_ptr(smart pointers) of life! Yes, and there is even a std::make_any!!!


How to use std::any

First you need to include its header:

Logically, it only works from C++17 as was said at the beginning!

#include <any>

And now the same code that was presented above, but using std::any:

#include <iostream> #include <any>  int main(){ std::any some_data; std::string str = "Hi"; int x = 3; auto y = std::make_any<decltype(x)>(6); some_data = str; std::cout << std::any_cast<std::string>(some_data) << '\n'; some_data = x; std::cout << std::any_cast<int>(some_data) << '\n'; some_data = y; std::cout << "Type of y: " << some_data.type().name() << '\n'; }

In the code above we saw that:

  • std::any some_data; - Declares the variable;
  • std::any_cast<T>(some_data) - Converts to the desired type;
  • std::make_any<T> - Another way to create objects;
  • some_data.type().name() - Gets the data type without needing typeinfo.

And you can use it for absolutely everything: std::vector, Lambda and all existing data types!

And the guy asks something else:

- OK! What if I want to end the lifetime of std::any manually?

Just use the reset union structure or even the initialization operator:

some_data.reset(); // Or some_data = {};

— And to check if std::any is empty?

Use has_value():

std::cout << (some_data.has_value() ? "Full!" : "Empty.") << '\n';

The unionless type() with name() can be used to compare types:

std::cout << (some_data.type() == typeid(void)) << '\n'; // 0 to false std::cout << (some_data.type() == typeid(int)) << '\n'; // 1 to true

To use Boolean names use: std::cout << std::boolalpha << (some_data.type() == typeid(int)) << '\n';.

To throw exceptions you must use std::bad_any_cast:

try { std::any any_str("Hiii"); auto my_any{ std::make_any<std::string>(any_str.type().name()) }; std::cout << std::any_cast<std::string>(my_any) << '\n'; }catch (const std::bad_any_cast& e) { std::cerr << "Error: " << e.what() << std::endl; }

To check whether everything really complies, never forget to use the flags for your compiler: -Wall -Wextra -pedantic -g -fsanitize=address.


Real life example

Imagine you have code that needs to concatenate several types and return a string. However, one of the types can be: int, double or std::string.

If you use std::any_cast<T> in the return like this:

Example:

#include <iostream> #include <any> #include <sstream>  enum class Message { SUCCESS, WARNING, ERROR, UNKNOW }; std::string add_info(Message, const std::string&, std::any, int); int main(){ std::any obj; obj = std::string("Start"); std::cout << add_info(Message::SUCCESS, " of type string: ", obj, 3) << '\n'; obj = 6; std::cout << add_info(Message::WARNING, " of type int: ", obj, 9) << '\n'; obj = 3.14; std::cout << add_info(Message::ERROR, " of type double: ", obj, 0) << '\n'; obj.reset(); std::cout << add_info(Message::UNKNOW, " no type: ", obj, 9) << '\n'; obj = "CONST_CHAR"; std::cout << add_info(Message::SUCCESS, " no type: ", nullptr, 9) << '\n'; return 0; } std::string add_info(Message msg, const std::string& out, std::any object, int num){ return std::any_cast<std::string>(msg) + out + "'" + std::any_cast<std::string>(object) + "' " + std::to_string(num); }

Compile: g++ -Wall -Wextra -pedantic -g -fsanitize=address main.cpp.

There will be a std::bad_any_cast in the output:

terminate called after throwing an instance of 'std::bad_any_cast' what(): bad any_cast Aborted

Both conversions (from msg and object) are incorrect:

std::any_cast<std::string>(msg) + ... // How much std::any_cast<std::string>(object)

You need to make a switch case for the enumerator and in the case of the object parameter: You will need to use has_value(), store the type() in std::type_info& and use a std:: stringstream to assign return types with union to: str(), like this:

#include <iostream> #include <any> #include <sstream>  enum class Message { SUCCESS, WARNING, ERROR, UNKNOW }; std::string add_info(Message, const std::string&, std::any, int); int main(){ std::any obj; obj = std::string("Start"); std::cout << add_info(Message::SUCCESS, " of type string: ", obj, 3) << '\n'; obj = 6; std::cout << add_info(Message::WARNING, " of type int: ", obj, 9) << '\n'; obj = 3.14; std::cout << add_info(Message::ERROR, " of type double: ", obj, 0) << '\n'; obj.reset(); std::cout << add_info(Message::UNKNOW, " no type: ", obj, 9) << '\n'; obj = "CONST_CHAR"; std::cout << add_info(Message::SUCCESS, " no type: ", nullptr, 9) << '\n'; return 0; } std::string add_info(Message msg, const std::string& out, std::any object, int num){ std::string local_msg {"NOTHING"}; std::stringstream ss; switch (msg){ case Message::SUCCESS: local_msg = "SUCCESS"; break; case Message::WARNING: local_msg = "WARNING"; break; case Message::ERROR: local_msg = "ERROR"; break; case Message::UNKNOW: local_msg = "UNKNOW"; break; } if (object.has_value()) { const std::type_info& type = object.type(); if (type == typeid(std::string)) { ss << std::any_cast<std::string>(object); } else if (type == typeid(int)) { ss << std::any_cast<int>(object); } else if (type == typeid(double)) { ss << std::any_cast<double>(object); } else { ss << "null"; } } else { ss << "[no object]"; } return local_msg + out + "'" + ss.str() + "' " + std::to_string(num); }

Compile: g++ -Wall -Wextra -pedantic -g -fsanitize=address main.cpp and after running ./a.out, the output will be:

SUCCESS of type string: 'Start' 3 WARNING of type int: '6' 9 ERROR of type double: '3.14' 0 UNKNOW without type: '[no object]' 9 SUCCESS without type: 'null' 9

It seems laborious, but this is the correct way to end the lifespan of any type!


In addition to being completely SAFE, std::any is very practical and a great help!

For more information visit: https://en.cppreference.com/w/cpp/utility/any


cpp cppdaily


Share


YouTube channel

Subscribe


Marcos Oliveira

Marcos Oliveira

Software developer
https://github.com/terroo