- PVSM.RU - https://www.pvsm.ru -
Привет! Представляем вашему вниманию перевод статьи «Everything You Need to Know About std::any from C++17» [1] автора Bartlomiej Filipek [2].

С помощью std::optional вы можете хранить один какой-то тип. С помощью std::variant вы можете хранить несколько типов в одном объекте. И С++17 предоставляет нам еще один такой оберточный тип — std::any, который может хранить что угодно, оставаясь при этом типобезопасным.
До этого стандарт С++ не предоставлял много вариантов решения проблемы хранения нескольких типов в одной переменной. Конечно можно использовать void*, однако это совсем не безопасно.
Теоретически void* можно обернуть в класс, где как-то хранить тип:
class MyAny
{
void* _value;
TypeInfo _typeInfo;
};
Как вы видите, мы получили некую базовую форму std::any, но для обеспечения типобезопасности MyAny нам требуются дополнительные проверки. Именно поэтому лучше использовать вариант из стандартной библиотеки, чем делать свое решение.
И это то, чем является std::any из C++17. Он позволяет хранить что угодно в объекте и сообщает об ошибке (бросает исключение), когда вы пытаетесь получить доступ указав не тот тип.
Маленькая демонстрация:
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";
}
Этот код выведет:
16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World
Пример выше показывает некоторые важные вещи:
std::any — не шаблонный класс как std::optional или std::variant.has_value().reset()std::decaystd::any_cast, который выбросит исключение bad_any_cast, если сейчас переменная хранить знаечние не типа «T».type(), который возвращает std::type_info этого типа
Пример выше выглядит впечатляюще — настоящая переменная типа в С++! Если вы очень любите JavaScript, то можете даже сделать все ваши переменные типа std::any и использовать С++ как JavaScript :)
Но может есть какие-то нормальные примеры использования?
В то время как void* воспринимается мной как очень небезопасная вещь с очень ограниченным кругом возможных использований, std::any полностью типобезопасный, поэтому у него есть некоторые хорошие способы использования.
Например:
Мне кажется, во многих из этих примерах мы можем выделить ограниченный список поддерживаемых типов, поэтому std::variant может быть более верным выбором. Но конечно сложно создавать библиотеки не зная конечных продуктов, в которых она будет использована. Ты просто не знаешь какие типы там будут храниться.
Демонстрация показала некоторые базовые вещи, но в следующих секциях вы узнаете больше деталей о std::any, поэтому продолжайте читать.
Есть несколько способов создать объект типа std::any:
std::in_place_typestd::make_anyНапример:
// стандартная инициализация:
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");
Изменить значение, которое в данный момент хранится в std::any, можно двумя способами: методом emplace или присваиванием:
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);
Ключевым для безопасности std::any является отсутствие утечки ресурсов. Для достижения этой цели std::any уничтожит любой активный объект перед тем, как присваивать новое значение.
std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "n";
Этот код выведет следующее:
MyType::MyType
MyType::~MyType
100
Объект std::any инициализируется объектом типа MyType, но перед присваиванием нового значения (100.0f) вызывается деструктор MyType.
В большинстве случаев у вас есть только один способ получения доступа к значению в std::any — std::any_cast, она возвращает значения заданного типа, если оно хранится в объекте.
Эта функция очень полезная, так как у нее есть много способов использования:
std::bad_any_cast при ошибкеstd::bad_any_cast при ошибкеПосмотрите пример:
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();
}
}
Как вы можете видеть, у нас есть два способа отслеживания ошибок: через исключения (std::bad_any_cast) или возвращая указатель (или nullptr). Функция std::any_cast для возврата указателей перегружена и помечена как noexcept.
std::any выглядит мощным инструментом, и вы скорее всего будете использовать его, чтобы хранить данные разных типов, но какова цена этого?
Главная проблема — дополнительное выделение памяти.
std::variant и std::optional не требует никаких дополнительных выделений памяти, но это вызвано тем, что типы хранимых в объекте данных заранее известны. std::any не имеет такой информации, поэтому может использовать дополнительную память.
Это будет происходить всегда или иногда? Какие правила? Это будет происходить даже с такими простыми типами, как int?
Давайте посмотрим, что сказано в стандарте:
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
Реализация должна избегать использование динамической памяти для хранимых данных малого размера. Например, когда объект создается храня только int. Такая оптимизация для малых объектов должна применяется только для типов T, для которых is_nothrow_move_constructible_v является true.
В итоге, для реализаций предлагают использовать оптимизацию малых объектов (Small Buffer Optimization/SBO). Но у этого есть и цена. Это делает тип больше — чтобы покрыть буфер.
Давайте посмотрим на размер std::any, вот результаты с нескольких компиляторов:
| Compiler | sizeof(any) |
|---|---|
| GCC 8.1 (Coliru) | 16 |
| Clang 7.0.0 (Wandbox) | 32 |
| MSVC 2017 15.7.0 32-bit | 40 |
| MSVC 2017 15.7.0 64-bit | 64 |
В общем, как вы видите, std::any это не простой тип, и он приносит дополнительные расходы. Он обычно занимает не мало памяти, из-за SBO, от 16 до 32 байтов (в GCC или clang… или даже 64 байта в MSVC!).
boost::any был представлен где-то в 2001 году (версия 1.23.0). Кроме того, автор boost::any (Kevlin Henney) так же является автором предложения std::any. Поэтому эти два типа тесно связаны, версия из STL сильно основана на предшественнике.
Вот главные изменения:
| Функция | Boost.Any (1.67.0) [3] |
std::any |
|---|---|---|
| Дополнительное выделение памяти | да | да |
| Оптимизация малых объектов | нет | да |
| emplace | нет | да |
| in_place_type_t в конструкторе | нет | да |
Главное отличие в том, что boost::any не использует SBO, поэтому он занимает значительно меньше памяти (в GCC8.1 его размер — 8 байт), но из-за этого он динамически выделяет памяти даже для таких маленьких типов как int.
Главным плюсом std::any является гибкость. В примерах ниже вы можете увидеть некоторые идеи (или конкретные реализации), где использование std::any позволяет сделать приложение немного проще.
В примерах к std::variant (посмотреть их можно тут [англ] [4]) вы могли видеть, как можно парсить конфигурационные файлы и хранить результат в переменной типа std::variant. Теперь вы пишите очень общее решение, может быть это часть какой-то библиотеки, тогда вам могут быть не известны все возможные варианты типов.
Хранение данных с помощью std::any для параметров скорее всего будет достаточно хорошим в плане производительности, и в то же время даст вам гибкость решения.
В Windows Api, которое в основном написанно на С, есть система передачи сообщений, которая использует id сообщения с двумя опциональными параметрами, которые хранят данные сообщения. Основываясь на этом механизме ты можешь реализовать WndProc, который обрабатывает переданное в ваше окно сообщение.
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
Дело в том: что данные хранятся в wParam или lParam в разных формах. Иногда тебе надо использовать только пару байтов wParam.
Что если мы изменим эту систему так, что сообщение может передавать методу обработки что угодно?
Например:
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;
};
Например, вы можете отправить сообщение окну:
Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);
Окно может ответить на сообщение так:
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;
}
}
Конечно, вам приходиться определять как хранится тип данных в сообщениях, но теперь можно использовать настоящие типы вместо разных трюков с числами.
Оригинальный документ, который представляет any для С++ (N1939) показывает пример объекта свойства:
struct property
{
property();
property(const std::string &, const std::any &);
std::string name;
std::any value;
};
typedef std::vector<property> properties;
Этот объект выглядит очень полезным, так как может хранить много разных типов. Первым мне приходит в голову пример использования его в менеджере пользовательского интерфейса или в игровом редакторе.
В r/cpp [5] был поток о std::any. И там было как минимум один отличный комментарий, который обобщает когда тип должен использоваться.
Из этого комментария [6]:
Суть в том, что std::any позволяет передавать права на произвольные данные через границы, которые не знают о его типе.
Все, о чем я говорил перед этим, близко к этой идее:
В этой статье мы много узнали о std::any!
Вот вещи, которые надо помнить:
std::any не является шаблонным классомstd::any использует оптимизацию малых объектов, поэтому он не будет динамически выделять памяти для таких простых типов как int или double, а для бо́льших типов будет использоваться дополнительная памятьstd::any можно назвать «тяжелым», но он предлагает типобезопастность и большую гибкостьstd::any можно получить с помощью any_cast, который предлагает несколько «режимов». Например, в случае ошибки он может бросить исключение или просто вернуть nullptrstd::variantАвтор: Nikita Serba
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/353315
Ссылки в тексте:
[1] «Everything You Need to Know About std::any from C++17»: https://www.bfilipek.com/2018/06/any.html
[2] Bartlomiej Filipek: https://twitter.com/intent/user?screen_name=fenbf
[3] (1.67.0): https://www.boost.org/doc/libs/1_67_0/doc/html/any.html
[4] посмотреть их можно тут [англ]: https://www.bfilipek.com/2018/06/variant.html#examples-of-stdvariant
[5] r/cpp: https://www.reddit.com/r/cpp/comments/7l3i19/why_was_stdany_added_to_c17/
[6] комментария: https://www.reddit.com/r/cpp/comments/7l3i19/why_was_stdany_added_to_c17/drjaep3
[7] Источник: https://habr.com/ru/post/503556/?utm_source=habrahabr&utm_medium=rss&utm_campaign=503556
Нажмите здесь для печати.