2
, 3
, а кортеж b
— значения 'a'
, 'b'
, 'c'
.В данном случае вызов
apply(z, a)
приведет к вызову z(1, 2, 3)
. Он вернет объект функции, который захватит значения 1
, 2
, 3
в набор параметров xs
. В момент вызова с помощью apply(z(1,2,3),b)
этот объект получит значения 'a'
, 'b'
, 'c'
, помещенные в набор параметров ys
. По сути, действие аналогично прямому вызову z(1,2,3)('a', 'b', 'c')
.О’кей, что произойдет теперь, когда у нас есть значения
xs = (1, 2, 3)
и ys = ('a', 'b', 'c')
? Выражение tuple_cat(make_tuple(xs, ys) ...)
сделает следующее (рис. 8.1).Сначала элементы из наборов
xs
и ys
будут сгруппированы попарно. Это «попарное чередование» выполняется в вызове make_tuple(xs,ys)
. Сначала мы получим список кортежей переменной длины по два элемента в каждом. Чтобы получить один большой кортеж, мы применяем вызов tuple_cat
— в результате получаем большой сконкатенированный кортеж, содержащий все члены исходных кортежей, которые чередуются. Замена void* с использованием std::any для повышения безопасности типов
Может случиться так: нам понадобится сохранить элементы любого типа в переменной. Для такой переменной следует проверить, содержит ли она что-либо, и если да, то нужно определить, что именно. Все это надо сделать безопасно для типов.
В прошлом мы имели возможность хранить указатели на различные объекты в указателе типа
void*
. Такой указатель сам по себе не может сказать, на какой объект ссылается, поэтому нужно вручную создать некий дополнительный механизм, который сообщит, чего стоит ожидать. Данное решение приводит к созданию некрасивого и небезопасного кода.Еще одним дополнением к STL в C++17 является тип
std::any
. Он разработан для того, чтобы хранить переменные любого вида, и предоставляет средства, которые позволяют выполнить проверку, безопасную для типов, и получить доступ к данным.В текущем разделе мы поработаем с этим вспомогательным типом для того, чтобы несколько лучше его понять.
Как это делается
В этом примере мы реализуем функцию, которая пробует вывести на экран какие-либо данные. В качестве ее типа аргумента служит тип
std::any
.
1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен
std
:
#include
#include
#include
#include
#include
using namespace std;
2. Чтобы сократить объем использования угловых скобок в следующей программе, определим псевдоним для типа
list
и будем применять его впоследствии:
using int_list = list;
3. Реализуем функцию, которая утверждает, что может вывести на экран любые данные. Обещание заключается вот в чем: она выводит любые данные, предоставленные как аргумент в виде переменной
std::any
:
void print_anything(const std::any &a)
{
4. Первое, что нужно сделать, — проверить, содержит ли аргумент какие-то данные, или же это пустой экземпляр типа any. Если он пуст, то нет смысла пытаться определить, как его выводить на экран.
if (!a.has_value()) {
cout << "Nothing.\n";
5. Если он не пуст, то можно попробовать сравнивать его с разными типами до тех пор, пока не получим совпадение. Первым типом послужит тип
string
. Если это строка, то можно выполнить преобразование a
к ссылке string
с помощью std::any_cast
и просто вывести его на экран. Из соображений эстетики мы поместим строку в кавычки:
} else if (a.type() == typeid(string)) {
cout << "It's a string: "
<< quoted(any_cast(a)) << '\n';
6. Если это не
string
, то может быть int
. При совпадении данного типа можно использовать преобразование any_cast
, чтобы получить реальное значение int
:
} else if (a.type() == typeid(int)) {
cout << "It's an integer: "
<< any_cast(a) << '\n';
7.
std::any
работает не только для простых типов наподобие string
и int
. В переменную any можно поместить и ассоциативный массив, список или экземпляр любого другого сложного типа данных. Посмотрим, являются ли входные данные списком целых чисел, и если да, то можем вывести его точно так же, как и любой другой список:
} else if (a.type() == typeid(int_list)) {
const auto &l (any_cast(a));
cout << "It's a list: ";
copy(begin(l), end(l),
ostream_iterator{cout, ", "});
cout << '\n';
8. Если не подошел ни один из перечисленных типов, то у нас закончатся догадки. В таком случае просто сдадимся и скажем пользователю, что не знаем, как выводить эти данные на экран:
} else {
cout << "Can't handle this item.\n";
}
}
9. В функции
main
можем вызвать эту функцию с произвольными типами, с пустой переменной типа any
с помощью {}
или передать ей строку "abc"
или целое число. Поскольку экземпляр типа std::any
может быть создан на основе этих типов неявно, не возникает задержек, связанных с синтаксисом. Мы даже можем создать целый список и передать его в эту функцию:
int main()
{
print_anything({});
print_anything("abc"s);
print_anything(123);
print_anything(int_list{1, 2, 3});
10. Если мы будем помещать объекты, копировать которые действительно дорого, в переменную типа
any
, то можем также выполнить конструкцию «на месте» (in-place). Попробуем сделать это для нашего списочного типа. Выражение in_place_type_t{}
представляет собой пустой объект, дающий конструктору типа any достаточно информации о том, что мы собираемся создать. Второй параметр, {1,2,3}
, — просто список инициализации, который будет передан в int_list
, будучи встроенным в переменную типа any
с целью создания объекта. Это способ избежать ненужного копирования или перемещения.
print_anything(any(in_place_type_t{}, {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()
, который говорит, содержит ли экземпляр значение. Но, помимо этого, он может содержать все что угодно, вследствие чего с ним работать немного сложнее, нежели с типом