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

dog
. Если такого экземпляра внутри нет, то указатель равен
null
. Таким образом, мы можем попробовать получать разные типы до тех пор, пока не преуспеем.


  for (const animal &a : l) {

    if (const auto d (get_if(&a)); d) {

        d->woof();

    } else if (const auto c (get_if(&a)); c) {

      c->meow();

    }

  }

  cout << "-----\n";


10. Последний — и самый элегантный вариант — это

variant::visit
. Данная функция принимает объект функции и экземпляр типа
variant
. Объект функции должен реализовывать разные перегруженные версии для всех вероятных типов, которые может хранить
variant
. Ранее мы реализовали структуру, имеющую необходимые перегруженные версии оператора
()
, поэтому можем использовать ее здесь:


  for (const animal &a : l) {

       visit(animal_voice{}, a);

  }

  cout << "-----\n";


11. Наконец подсчитаем количество экземпляров типов

cat
и
dog
в списке. Предикат
is_type
может быть специализирован для типов
cat
и
dog
, а затем использован в комбинации с
std::count_if
, чтобы получить количество экземпляров этого типа:


  cout << "There are "

<< count_if(begin(l), end(l), is_type)

<< " cats and "

<< count_if(begin(l), end(l), is_type)

<< " dogs in the list.\n";

}


12. После компиляции и запуска программы на экране будет список, выведенный три раза. Затем мы увидим, что предикаты

is_type
, объединенные с
count_if
, тоже работают хорошо:


$ ./variant Tuba says Meow!

Balou says Woof!

Bobby says Meow!

-----

Tuba says Meow!

Balou says Woof!

Bobby says Meow!

-----

Tuba says Meow!

Balou says Woof!

Bobby says Meow!

-----

There are 2 cats and 1 dogs in the list.


Как это работает

Тип

std::variant
похож на тип
std::any
, поскольку они оба могут содержать объекты разных типов, и нужно определять во время работы программы, что именно в них хранится, прежде чем получить доступ к их содержимому.

С другой стороны, тип

std::variant
отличается от
std::any
тем, что мы должны объявлять, экземпляры каких типов он может хранить в виде списка шаблонных типов. Экземпляр типа
std::variant
должен хранить один экземпляр типа
A
,
B
или
C
. Нельзя сделать так, чтобы в экземпляре типа
variant
не хранился ни один экземпляр. Это значит, что тип
std::variant
не поддерживает возможность опциональности.

Экземпляр типа

variant
имитирует объединение, которое может выглядеть так:


union U {

  A a;

  B b;

  C c;

};


Проблема с объединениями заключается в том, что нужно создавать собственные механизмы для определения того, экземпляром какого типа оно было инициализировано:

A
,
B
или
C
. Тип
std::variant
может сделать это за нас, не прилагая особых усилий.

В коде, показанном в этом разделе, мы использовали три разных способа работы с содержимым переменной variant.

Первый способ — применение функции

index()
типа
variant
. Для типа
variant
она может вернуть индекс
0
, если экземпляр был инициализирован переменной типа
A
,
1
для типа
B
или
2
для типа
C
, и т.д. для более сложных вариантов.

Следующий способ — использование функции

get_if
. Она принимает адрес объекта типа
variant
и возвращает указатель типа
T
на его содержимое. Если тип
T
указан неправильно, то указатель станет нулевым. Кроме того, можно вызвать метод
get(x)
для переменной типа
variant
, чтобы получить ссылку на ее содержимое, но если это не сработает, то данная функция сгенерирует исключение (перед выполнением таких преобразований можно проверить правильность типа с помощью булева предиката
holds_alternative(x)
).

Последний способ получить доступ к значению, хранящемуся в типе

variant
, — применить функцию
std::visit
. Она принимает объект функции и экземпляр типа
variant
. Функция проверяет, какой тип имеет содержимое экземпляра типа
variant
, а затем вызывает соответствующий перегруженный оператор
()
объекта функции.

Именно для этих целей мы и реализовали тип animal_voice, поскольку он может быть использован в комбинации с

visit
и
variant
:


struct animal_voice

{

  void operator()(const dog &d) const { d.woof(); }

  void operator()(const cat &c) const { c.meow(); }

};


Последний описанный способ получения доступа к экземплярам, хранящимся в экземплярах типа

variant
, считается самым элегантным, поскольку в разделах кода, в которых мы получаем доступ к экземпляру, не нужно жестко кодировать возможные типы. Это позволяет проще расширять код.


 Утверждение о том, что тип

variant
не может не иметь значения, было не совсем верным. Добавив тип
std::monostate
в список вероятных типов, можно указать, что экземпляр не будет хранить значения.

Автоматическое управление ресурсами с помощью std::unique_ptr

Начиная с C++11 в STL появились умные указатели, помогающие отслеживать динамическую память и ее использование. Даже до C++11 существовал класс

auto_ptr
, который мог управлять динамической памятью, но его было легко применить неправильно.

Однако с появлением умных указателей теперь редко приходится самостоятельно использовать ключевые слова

new
и
delete
, и это очень хорошо. Умные указатели — отличный пример автоматического управления памятью. Поддерживая объекты, память для которых выделяется динамически с помощью
unique_ptr
, мы защищены от утечек памяти, поскольку при разрушении объекта данный класс автоматически вызывает поддерживаемый им объект.

Уникальный указатель выражает принадлежность объекта, на который ссылается, и выполняет свою задачу по освобождению его памяти, если та более не используется. Этот класс может навсегда освободить нас от утечек памяти (во всяком случае вместе со своими компаньонами

shared_ptr
и
weak_ptr
, но в этом примере мы концентрируемся только на
unique_ptr
). Самая приятная особенность заключается в том, что он не влияет на производительность и свободное место в сравнении с кодом, содержащим необработанные указатели и предусматривающим ручное управление памятью. (О’кей, он все еще устанавливает значение внутреннего необработанного указателя на