Шрифт:
Интервал:
Закладка:
print_anything(any(in_place_type_t<int_list>{}, {1, 2, 3}));
}
11. Компиляция и запуск программы дадут следующие результаты, они полностью соответствуют нашим ожиданиям:
$ ./any
Nothing.
It's a string: "abc"
It's an integer: 123
It's a list: 1, 2, 3,
It's a list: 1, 2, 3,
Как это работает
Тип std::any в чем-то похож на тип std::optional — он поддерживает метод has_value(), который говорит, содержит ли экземпляр значение. Но, помимо этого, он может содержать все что угодно, вследствие чего с ним работать немного сложнее, нежели с типом optional.
Прежде чем получать доступ к содержимому переменной типа any, нужно определить, какого типа хранящееся в ней значение, а затем преобразовать данные к этому типу.
Определить тип значения можно с помощью следующего сравнения: x.type() == typeid(T). Если оно возвращает результат true, то можно использовать преобразование any_cast, чтобы получить содержимое.
Обратите внимание: any_cast<T>(x) возвращает копию внутреннего значения. Если нужно получить ссылку, чтобы избежать копирования сложных объектов, то следует использовать конструкцию any_cast<T&>(x). Именно это мы и сделали, когда получали доступ к объектам типа string или list<int> в коде данного раздела.
Если мы преобразуем экземпляр к неправильному типу, будет сгенерировано исключение std::bad_any_cast.
Хранение разных типов с применением std::variant
В языке С++ для создания типов можно использовать не только примитивы struct и class. Если нужно выразить, что какие-то переменные могут содержать значения типа А либо значения типа В (или C, или любого другого), то на помощь придут объединения. Проблема с объединениями заключается в том, что они не могут сказать, для хранения каких типов были инициализированы.
Рассмотрим следующий код:
union U {
int a;
char *b;
float c;
};
void func(U u) { std::cout << u.b << 'n'; }
Допустим, мы вызовем функцию func для объединения, которое было инициализировано так, чтобы хранить в нем целое число в члене a. Тогда ничто не помешает нам получить доступ к нему так, как если бы оно было инициализировано способом, позволяющим хранить в нем указатель на строку в члене b. Из подобного кода могут появиться самые разнообразные ошибки. Прежде чем мы поместим в наше объединение вспомогательную переменную, которая скажет нам, для чего оно было инициализировано, можем воспользоваться типом std::variant, появившимся в C++17.
Тип variant, по сути, представляет собой обновленную версию типа union. Он не использует кучу, поэтому настолько же эффективно задействует память и время, как и решение, основанное на объединениях, так что нам нет нужды реализовывать его самостоятельно. Тип может хранить все что угодно, кроме ссылок массивов или объектов типа void.
В этом разделе мы создадим программу, которая задействует тип variant.
Как это делается
В этом примере мы реализуем программу, которая уже знакома с типами cat и dog и сохраняет смешанный список экземпляров обоих типов, не используя полиморфизм.
1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен std:
#include <iostream>
#include <variant>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
2. Далее реализуем два класса, имеющих схожий инструментарий, но не связанных друг с другом, что отличает их от классов, которые, скажем, наследуют от одного интерфейса или похожих интерфейсов. Первый класс — это класс cat. Объект класса cat имеет имя и может сказать «мяу» (meow):
class cat {
string name;
public:
cat(string n) : name{n} {}
void meow() const {
cout << name << " says Meow!n";
}
};
3. Второй класс — это класс dog. Объект класса dog, конечно, может сказать не «мяу», а «гав» (woof):
class dog {
string name;
public:
dog(string n) : name{n} {}
void woof() const {
cout << name << " says Woof!n";
}
};
4. Теперь можно определить тип animal, он будет представлять собой псевдоним типа std::variant<dog, cat>. По сути, он работает как старое доброе объединение, но имеет все дополнительные средства, предоставленные типом variant:
using animal = variant<dog, cat>;
5. Прежде чем писать основную программу, нужно реализовать два вспомогательных элемента. Одним из них является предикат animal. Вызвав is_type<cat>(...) или is_type<dog>(...), можно определить, какого типа данные содержатся в экземпляре типа animal. Реализация просто вызывает функцию holds_alternative, которая, по сути, является обобщенной функцией-предикатом для типа variant:
template <typename T>
bool is_type(const animal &a) {
return holds_alternative<T>(a);
}
6. Вторым вспомогательным элементом является структура, которая ведет себя как объект функции. Это двойной объект функции, поскольку он дважды реализует оператор (). Одна из реализаций — перегруженная версия, принимающая экземпляры типа dog, вторая же принимает экземпляры типа cat. Для этих типов она просто вызывает функции woof или meow:
struct animal_voice
{
void operator()(const dog &d) const { d.woof(); }
void operator()(const cat &c) const { c.meow(); }
};
7. Воспользуемся результатами нашего труда. Сначала определим список переменных типа animal и заполним его экземплярами типов cat и dog:
int main()
{
list<animal> l {cat{"Tuba"}, dog{"Balou"}, cat{"Bobby"}};
8. Теперь трижды выведем на экран содержимое списка, каждый раз новым способом. Один из них заключается в использовании variant::index(). Поскольку animal является псевдонимом для variant<dog, cat>, возвращаемое значение 0 означает, что переменная хранит экземпляр типа dog. Значение индекса 1 говорит о том, что это экземпляр типа cat. Здесь важен порядок типов в специализации variant. В блоке