optional
.Прежде чем получать доступ к содержимому переменной типа
any
, нужно определить, какого типа хранящееся в ней значение, а затем преобразовать данные к этому типу.Определить тип значения можно с помощью следующего сравнения:
x.type() == typeid(T)
. Если оно возвращает результат true
, то можно использовать преобразование any_cast
, чтобы получить содержимое.Обратите внимание:
any_cast(x)
возвращает копию внутреннего значения. Если нужно получить ссылку, чтобы избежать копирования сложных объектов, то следует использовать конструкцию any_cast(x)
. Именно это мы и сделали, когда получали доступ к объектам типа string
или list
в коде данного раздела.
Если мы преобразуем экземпляр к неправильному типу, будет сгенерировано исключение
std::bad_any_cas
t.Хранение разных типов с применением 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
#include
#include
#include
#include
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
. По сути, он работает как старое доброе объединение, но имеет все дополнительные средства, предоставленные типом variant
:
using animal = variant;
5. Прежде чем писать основную программу, нужно реализовать два вспомогательных элемента. Одним из них является предикат
animal
. Вызвав is_type(...)
или is_type(...)
, можно определить, какого типа данные содержатся в экземпляре типа animal
. Реализация просто вызывает функцию holds_alternative
, которая, по сути, является обобщенной функцией-предикатом для типа variant
:
template
bool is_type(const animal &a) {
return holds_alternative(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 l {cat{"Tuba"}, dog{"Balou"}, cat{"Bobby"}};
8. Теперь трижды выведем на экран содержимое списка, каждый раз новым способом. Один из них заключается в использовании
variant::index()
. Поскольку animal
является псевдонимом для variant
, возвращаемое значение 0
означает, что переменная хранит экземпляр типа dog
. Значение индекса 1
говорит о том, что это экземпляр типа cat
. Здесь важен порядок типов в специализации variant
. В блоке switch case
мы получаем доступ к variant
с помощью вызова get
для получения экземпляра типа cat
или dog
, хранящегося внутри:
for (const animal &a : l) {
switch (a.index()) {
case 0:
get(a).woof();
break;
case 1:
get(a).meow();
break;
}
}
cout << "-----\n";
9. Вместо того чтобы использовать численный индекс типа, можно также явно запросить каждый тип. Вызов
get_if
возвращает указатель на объект типа do
на внутренний экземпляр типа