C++17 STL Стандартная библиотека шаблонов — страница 85 из 119

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
на внутренний экземпляр типа