Начиная с версии C++11 язык C++ предоставляет возможность определять шаблоны, принимающие переменное число параметров.
Например, можно написать функцию, которая принимает произвольное число параметров произвольных типов и возвращает их сумму. Если суммирование для какой-нибудь пары типов не определено, то получим ошибку компиляции.
template <class Arg>
Arg sum(Arg arg) { return arg; }
template <class First, class... Other>
auto sum(First first, Other... other)
{
return first + sum(other...);
}
Почему в примере выше для организации выхода из рекурсии определён вариант sum, принимающий один параметр, а не вариант sum, принимающий нуль параметров и возвращающий число нуль?
Итак, для определения шаблона с переменным числом параметров необходимо указать пакет типов в template-заголовке:
template <class... Types>
Возможны комбинации с обычными параметрами (как в примере с суммой), но допустим максимум один пакет. Пакет может быть пустым. Узнать размер пакета можно с помощью sizeof…(имя пакета).
Types можно использовать в списке параметров функции:
template <class... Types>
void f(Types... parameters)
Теперь parameters — это пакет параметров, типы которых задаются “параллельным” пакетом Types. Пакет параметров может быть только в функции-шаблоне, и ему обязан соответствовать пакет типов.
Внутри функции можно использовать как пакет типов, так и пакет параметров. Для этого после имени пакета ставится многоточие ...
, которое интерпретируется компилятором как раскрытие пакета в список (через запятую) в указанном месте (см. пример sum выше — раскрытие пакета параметров other). Из-за такого минимализма возможных действий с пакетами обычным способом определения функций-шаблонов с переменным числом параметров является рекурсивное определение.
Рассмотрим простенький пример шаблона шаблона класса с переменным числом параметров. На этот раз параметры шаблона не будут типами, а будут значениями типа size_t.
template <size_t...>
struct Sum {};
template <>
struct Sum<>
{ enum { value = 0 }; };
template <size_t first, size_t... other>
struct Sum<first, other...>
{ enum { value = first + Sum<other...>::value }; };
Похожий код можно записать на каком-нибудь функциональном языке программирования, например, Haskell:
sum :: [Int] -> Int
sum [] = 0
sum (first:other) = first + sum other
Зачем сразу три определения?
Первое определение
template <size_t...>
struct Sum {};
говорит компилятору, что у нас есть шаблонный класс Sum, параметрами которого является пакет значений типа size_t.
Второе определение
template <>
struct Sum<>
{ enum { value = 0 }; };
является специализацией шаблона Sum для случая пустого пакета параметров. В этом случае мы определяем вложенную константу value со значением 0 (enum используется для краткости записи).
Третье определение
template <size_t first, size_t... other>
struct Sum<first, other...>
{ enum { value = first + Sum<other...>::value }; };
также является специализацией шаблона Sum, но теперь для случая наличия хотя бы одного параметра (first). Остальные параметры собираются в пакет other, который может быть пуст. Это определение позволяет собственно “запустить” рекурсивный процесс вычисления суммы. Рано или поздно other окажется пустым пакетом и будет подставлен нуль, определённый в предыдущей специализации.
cout << Sum<1, 2, 3, 4>::value; // > 10
Теперь представим, что требуется уметь вычислять суммарный размер произвольного набора типов — сумму значений sizeof для каждого типа. На основе Sum можно определить новый шаблон Sum_sizeof:
template <class... Types>
struct Sum_sizeof
{ enum { value = Sum<sizeof(Types)...>::value }; };
Запись sizeof(Types)...
есть раскрытие уже не пакета Types, а выражения от Types в список выражений от каждого элемента пакета. Например, если Types = { int, char, bool }
, то sizeof(Types)...
раскрывается в sizeof(int), sizeof(char), sizeof(bool)
, что, в свою очередь, может быть передано в качестве пакета целых чисел шаблону Sum.
При использовании C++14 (шаблонов переменных) можно переписать весь код выше более кратко:
template <size_t...>
constexpr size_t sum = 0;
template <size_t first, size_t... other>
constexpr size_t sum<first, other...> = first + sum<other...>;
template <class... Types>
constexpr size_t sum_sizeof = sum<sizeof(Types)...>;
// Проверка:
static_assert(sum_sizeof<int, bool, char> == sizeof(int) + sizeof(bool) + sizeof(char));
Определим две функции: print, выводящую подряд все свои параметры в cout, и println, переадресующую вызов print и затем печатающую символ перевода строки. Шаблоны функций в C++ нельзя специализировать частично (как классы), но можно перегружать. При вызове компилятор должен выбрать наиболее конкретный из подходящих вариантов.
inline void print() {} // Нечего печатать.
template <class First, class... Other>
void print(First first, Other... other)
{
cout << first;
print(other...);
}
template <class... Args>
void println(Args... args)
{
print(args...);
cout << '\n';
}
В данной версии определений аргументы копируются (передаются по значению), что может быть неоптимальным решением при выводе на печать строк или других больших объектов. Передачу по значению в данном случае можно заменить передачей по const-ссылке (знак &
должен стоять до многоточия).
inline void print() {} // Нечего печатать.
template <class First, class... Other>
void print(const First &first, const Other&... other)
{
cout << first;
print(other...);
}
template <class... Args>
void println(const Args&... args)
{
print(args..., '\n');
}
Попробуем теперь обобщить данный код — разрешим указывать объект потока вывода первым параметром. Если он не указан, то выводить в cout.
inline void print() {} // Нечего печатать.
inline void print(ostream&) {} // Нечего печатать.
template <class First, class... Other>
void print(ostream &os,
const First &first, const Other&... other)
{
os << first;
print(os, other...);
}
template <class... Other>
void print(const Other&... other)
{
print(cout, other...);
}
template <class... Other>
void println(ostream &os, const Other&... other)
{
print(os, other..., '\n');
}
template <class... Args>
void println(const Args&... args)
{
println(cout, args...);
}
Наконец, даже этот код можно обобщить: легко заметить, что print(wcout, L"...")
не скомпилируется, поскольку wcout не является объектом класса ostream. Классы ostream и wostream являются частными случаями шаблона basic_ostream<Char, Traits>.
inline void print() {} // Нечего печатать.
template <class Ch, class Tr>
void print(std::basic_ostream<Ch, Tr>&) {} // Нечего печатать.
template <class Ch, class Tr, class First, class... Other>
void print(std::basic_ostream<Ch, Tr> &os,
const First &first, const Other&... other)
{
os << first;
print(os, other...);
}
template <class... Other>
void print(const Other&... other)
{
print(std::cout, other...);
}
template <class Ch, class Tr, class... Other>
void println(std::basic_ostream<Ch, Tr> &os, const Other&... other)
{
print(os, other..., '\n');
}
template <class... Args>
void println(const Args&... args)
{
println(std::cout, args...);
}
Предположим, есть набор файлов, описывающих строки интерфейса некоторого приложения на разных языках. Например, английском и русском:
en.dat:
Intensity
Volume
%1% may take values between %2% and %3%.
weather
price
%1% %2% forecasting
oil
gold
ru.dat:
Насыщенность
Объём
%1% может принимать значения между %2% и %3%.
погоды
цены
прогноз %2% для %1%
нефти
золота
Мы хотим строить фразы, подставляя в модельную фразу-шаблон конкретные слова или значения. Определим для этого функцию format, принимающую первым параметром “модель”, а прочими параметрами значения для подстановки и возвращающую строку. Пусть например, строки из файла были считаны в вектор строк phrase, тогда
format(phrase[2], phrase[0], 0, 1)
даст “Intensity may take values between 0 and 1.” в случае выбора английского файла и “Насыщенность может принимать значения между 0 и 1.” в случае выбора русского файла. А
format(phrase[5], phrase[6], phrase[4])
даст, соответственно “oil price forecasting” и “прогноз цены для нефти” (обратите внимание на возможность разного порядка подстановок).
Попробуем определить функцию format с помощью Стандартной библиотеки C++ и шаблонов с переменным числом параметров.
inline string format(const string &model)
{
return model;
}
template <class First, class... Other>
string format(const string &model,
const First &first, const Other&... other)
{
// ???
}
Если попытаться написать её рекурсивно в стиле print из предыдущего примера, то придётся подставлять по одной строке и организовать подсчёт номера текущей подстановки, т. е. понадобятся вспомогательные определения функции, принимающей номер. Здесь рассмотрим другой способ, а именно, создание массива строковых представлений параметров.
template <class... Params>
string format(const string &model, const Params&... params)
{
// Конвертировать params в массив строк.
using std::to_string;
string param[] { "", to_string(params)... };
// Выполнить подстановку.
// ...
}
Теперь нет рекурсии, и нешаблонный вариант format уже не нужен. Мы инициализируем массив строк param строками, возвращаемыми для них функцией to_string. (Осторожно: to_string не принимает string! С этим разберёмся далее.). Пустая строка в начале позволяет гарантировать, что массив param будет содержать по крайней мере 1 элемент (массивы размера 0 запрещены).
Чтобы параметры-строки не вызывали ошибки компиляции, создадим свою вспомогательную функцию to_str и будем использовать её вместо to_string:
namespace impl
{
template <class T>
string to_str(const T &val)
{
using std::to_string;
return to_string(val);
}
inline string to_str(const char *p)
{
return p;
}
inline const string& to_str(const string &s)
{
return s;
}
}
Реализовать format (собственно выполнение подстановки) можно по-разному. Например, “в лоб” на основе рукописного конечного автомата так:
template <class... Params>
string format(const string &model, const Params&... params)
{
// Конвертировать params в массив строк.
string param[] { "", impl::to_str(params)... };
// Подготовить место для результата.
size_t result_possible_size = model.size();
for (auto &p: param)
result_possible_size += p.size();
string result;
result.reserve(result_possible_size);
// Выполнить подстановку (конечный автомат).
size_t param_index;
auto p = model.c_str();
auto stored_p = p;
outer:
while (auto ch = *p++)
{
if (ch == '%')
goto percent;
result += ch;
}
return result;
percent:
stored_p = p;
switch (auto ch = *p++)
{
case '\0':
result += '%';
return result;
case '%':
result += '%';
goto outer;
default:
if (!isdigit(ch))
{
(result += '%') += ch;
goto outer;
}
param_index = ch - '0';
}
while (auto ch = *p++)
{
if (ch == '%' && param_index <= sizeof...(params))
{
result += param[param_index];
goto outer;
}
if (!isdigit(ch))
break;
param_index = param_index * 10 + (ch - '0');
}
result += '%';
p = stored_p;
goto outer;
}
Используя регулярные выражения (#include <regex>
) из Стандартной библиотеки, можно записать похожий код более кратко:
template <class... Params>
string format(string model, const Params&... params)
{
// Конвертировать params в массив строк.
string param[] { "", impl::to_str(params)... };
// Выполнить подстановку (конечный автомат через std::regex).
string result;
regex preg("%(\\d*)%");
smatch match;
size_t i;
while (regex_search(model, match, preg))
{
result += match.prefix();
if (match.size() == 1 || match[1].length() == 0)
result += '%';
else if ((i = stoi(match[1])) <= sizeof...(params))
result += param[i];
else
result += match[0];
model = match.suffix();
}
result += match.suffix();
return result;
}
Список типов type list — конструкция времени компиляции, задающая последовательность (“список”) произвольных типов и позволяющая оперировать этой последовательностью как единым целым.
Начнём с простейшего примера. Предположим, мы хотим подсчитать, сколько раз встречается заданный тип в пакете. Это можно сделать так (C++14):
template <class... Types>
constexpr size_t t_count = 0;
template <class T, class... Types>
constexpr size_t t_count<T, T, Types...> = 1 + t_count<T, Types...>;
template <class T, class X, class... Types>
constexpr size_t t_count<T, X, Types...> = t_count<T, Types...>;
Здесь, однако, нельзя говорить о списке типов Types, поскольку пакет параметров шаблона не является типом, и возможности его использования довольно ограничены. Впрочем, благодаря механизму частичной специализации шаблонов с подстановкой по образцу, легко связать пакет с конкретным типом. Для этого определим шаблон Type_list (который мог бы вовсе не иметь членов):
template <class... Types>
struct Type_list
{
static constexpr auto size = sizeof...(Types);
// или enum { size = sizeof...(Types) };
};
template <class T, class L>
constexpr size_t t_count = 0;
template <class T, class... Tail>
constexpr size_t t_count<T, Type_list<T, Tail...>> = 1 + t_count<T, Type_list<Tail...>>;
template <class T, class X, class... Tail>
constexpr size_t t_count<T, Type_list<X, Tail...>> = t_count<T, Type_list<Tail...>>;
После этого следующий код выведет 0 2
:
cout << t_count<bool, Type_list<int, double>> << ' ';
cout << t_count<char, Type_list<int, char, bool, char>>;
Помимо подсчёта числа вхождений можно абсолютно аналогично через рекурсивное определение реализовать поиск первого вхождения (индекс):
template <class T, class L>
constexpr size_t t_index = -1;
template <class T, class... Tail>
constexpr size_t t_index<T, Type_list<T, Tail...>> = 0;
template <class T, class X, class... Tail>
constexpr size_t t_index<T, Type_list<X, Tail...>> = 1 + t_index<T, Type_list<Tail...>>;
Теперь предположим, что надо, наоборот, по номеру в списке выбрать из него тип. Это тоже можно определить рекурсивно, но уже не как переменную, а как тип. Обычно это делается через определение структуры с вложенным типом type, который и получает соответствующее значение.
template <size_t i, class L>
struct t_at {};
template <size_t i, class T, class... Tail>
struct t_at<i, Type_list<T, Tail...>>
{
using type =
typename t_at<i - 1, Type_list<Tail...>>
::type;
};
template <class T, class... Tail>
struct t_at<0, Type_list<T, Tail...>>
{
using type = T;
};
Чтобы при использовании не писать typename
и ::type
, можно определить using-синоним типа:
template <size_t i, class L>
using t_at_t = typename t_at<i, L>::type;
// Теперь, например,
// t_at_t<2, Type_list<bool, char, int, float>>
// равно int.
В Стандартной библиотеке (в <type_traits>
) есть множество похожих определений, позволяющих выполнять ряд манипуляций с типами.
Метафункцией называется отображение в множество типов или из множества типов.
Конструкции вроде t_at, t_at_t или даже t_index выше можно считать метафункциями, но синтаксически затруднительно использовать их сами в качестве аргументов других метафункций, что ослабляет комбинативную мощность подобных конструкций.
“Полноценной” метафункцией в синтаксисе C++ можно назвать нешаблонный класс, имеющий вложенное определение шаблона типа, которое уже можно использовать как t_at. Это позволяет передавать метафункцию как обычный тип.
Метафункцией высшего порядка называется метафункция, принимающая другие метафункции как аргументы.
Например, определим метафункцию добавления элемента в начало списка типов.
struct T_prepend
{
template <class A, class B>
struct apply {};
template <class A, class... Other>
struct apply<A, Type_list<Other...>>
{
using type = Type_list<A, Other...>;
};
};
Недостаток этой конструкции очевиден: уж очень навороченно выглядит “вызов” метафункции:
template <class T, class L>
using T_prepend_t =
typename T_prepend::template apply<T, L>::type;
В принципе, к каждой определяемой метафункции можно добавлять аналогичное парное определение синонима типа через using, но если метафункция пришла как параметр, то мы не знаем её имя и будем вынуждены писать полный синтаксис применения.
Другой вариант состоит в определении макроса, раскрывающегося в эту идиоматическую конструкцию:
#define APPLY(meta, ...) \
typename meta::template apply<__VA_ARGS__>::type
Теперь можно записывать более сложные конструкции с меньшим синтаксическим шумом. Например, рекурсивное определение метафункции высшего порядка T_map, порождающей список результатов для заданной метафункции Map (“отображение”) и списка аргументов:
// map m [] = []
// map m (t:tail) = (m t):(map m tail)
struct T_map
{
template <class Map, class List>
struct apply { using type = List; }; // Пустой список.
template <class Map, class T, class... Tail>
struct apply<Map, Type_list<T, Tail...>>
{
using type =
APPLY(T_prepend, \
APPLY(Map, T), \
APPLY(T_map, Map, Type_list<Tail...>));
};
};
Можно ли T_map записать более кратко без рекурсии?
Определим ещё одну метафункцию высшего порядка — свёртку (fold). Пример свёрток: сумма элементов, произведение элементов, максимум, минимум и т.п. Аргументами свёртки являются ассоциативная бинарная операция и список.
// fold f (t:[]) = t
// fold f (t:tail) = f t (fold f tail)
struct T_fold
{
// Для пустого списка результат не определён.
template <class Fold, class List> struct apply {};
// Для списка из одного элемента возвращаем этот же элемент.
template <class Fold, class T>
struct apply<Fold, Type_list<T>>
{
using type = T;
};
template <class Fold, class T, class... Tail>
struct apply<Fold, Type_list<T, Tail...>>
{
using type = APPLY(Fold, T, APPLY(T_fold, Fold, Type_list<Tail...>));
};
};
В случае, если аргументы метафункции должны быть значениями, а не типами (наиболее типичные случаи: булевские и целочисленные константы), то их удобно “обернуть” в тип и представлять далее как типы. Например, для целочисленных констант можно было бы определить обёртку вроде
template <int i>
struct Int
{
using type = int;
enum { value = i };
};
// Int<10>::value == 10
// Int<1> и Int<2> -- разные типы.
Впрочем, в Стандартной библиотеке уже есть аналогичный шаблон integral_constant, первый параметр которого — тип константы, второй — сама константа.
Определим функцию “максимум”, принимающую аргументы integral_constant:
// max a b = if a < b then b else a
struct T_max
{
template <class A, class B>
struct apply
{
using type = std::integral_constant<
std::common_type_t<typename A::value_type, typename B::value_type>,
(A::value < B::value? B::value: A::value)>;
};
};
Определим функцию T_sizeof, преобразующую тип в его размер (в виде соответствующего integral_constant).
struct T_sizeof
{
template <class T>
struct apply { using type = std::integral_constant<size_t, sizeof(T)>; };
};
Теперь мы можем найти максимальный размер типа из заданного списка:
// fold max map sizeof [int, char, bool, char]
// -> fold max [sizeof int, sizeof char, sizeof bool, sizeof char]
// -> max (sizeof int) (max (sizeof char) (max (sizeof bool) (sizeof char)))
cout << APPLY(T_fold, \
T_max, \
APPLY(T_map, \
T_sizeof, \
Type_list<int, char, bool, char>) \
){} << '\n'; // выведет размер int, например 4.
// {} создаёт объект integral_constant<size_t, вычисленное значение>,
// для которого приведение типа к целому числу даёт само вычисленное значение.
В наследство от C языку C++ достался ряд низкоуровневых средств. Одним из таких средств является “объединение” (union) — набор значений, размещённых по одному адресу в памяти.
union int_float_t
{
uint32_t u;
int32_t i;
float f;
};
int main()
{
int_float_t a;
cout << &a.u << '\n'; // Все три поля
cout << &a.i << '\n'; // размещены по
cout << &a.f << '\n'; // одному адресу.
a.f = 2.5;
cout << a.u; // ?
}
Объединение никак не решает вопрос определения того, какое именно поле используется в данный момент. Однако в C++ можно реализовать шаблон, являющийся типобезопасным аналогом объединения — вариантный тип. Объект вариантного типа в каждый момент времени хранит значение одного из заданных типов (параметры шаблона) и “знает”, какого типа значение хранится в нём в данный момент.
Позаимствуем часть кода из предыдущего примера (список типов).
template <class... Types>
struct Type_list
{
static constexpr auto size = sizeof...(Types);
};
template <class T, class L>
constexpr size_t t_count = 0;
template <class T, class... Tail>
constexpr size_t t_count<T, Type_list<T, Tail...>> = 1 + t_count<T, Type_list<Tail...>>;
template <class T, class X, class... Tail>
constexpr size_t t_count<T, Type_list<X, Tail...>> = t_count<T, Type_list<Tail...>>;
template <class T, class L>
constexpr size_t t_index = -1;
template <class T, class... Tail>
constexpr size_t t_index<T, Type_list<T, Tail...>> = 0;
template <class T, class X, class... Tail>
constexpr size_t t_index<T, Type_list<X, Tail...>> = 1 + t_index<T, Type_list<Tail...>>;
template <size_t i, class L>
struct t_at {};
template <size_t i, class T, class... Tail>
struct t_at<i, Type_list<T, Tail...>>
{
using type =
typename t_at<i - 1, Type_list<Tail...>>
::type;
};
template <class T, class... Tail>
struct t_at<0, Type_list<T, Tail...>>
{
using type = T;
};
template <size_t i, class L>
using t_at_t = typename t_at<i, L>::type;
Здесь для простоты метафункции высшего порядка общего вида использовать не будем: вычисление максимума sizeof и alignof будем выполнять с помощью рекурсивных определений шаблонов переменных.
template <class L>
constexpr size_t max_sizeof = 0;
template <class T, class... Tail>
constexpr size_t max_sizeof<Type_list<T, Tail...>> =
std::max(sizeof(T), max_sizeof<Type_list<Tail...>>);
template <class L>
constexpr size_t max_alignof = 1;
template <class T, class... Tail>
constexpr size_t max_alignof<Type_list<T, Tail...>> =
std::max(alignof(T), max_alignof<Type_list<Tail...>>);
Можно ли записать эти определения более кратко и без рекурсии?
Проверка “является ли список 1 подмножеством списка 2” (квадратичный рекурсивный алгоритм времени компиляции):
template <class L1, class L2>
constexpr bool t_subset = true; // for empty list L1.
template <class T, class... Types1, class... Types2>
constexpr bool t_subset<
Type_list<T, Types1...>,
Type_list<Types2...>> =
t_count<T, Type_list<Types2...>> != 0 &&
t_subset<Type_list<Types1...>, Type_list<Types2...>>;
Вспомогательная конструкция: найти в списке типов первый тип (вернуть его номер), для которого определён конструктор, принимающий заданный набор параметров Args. Возвращает –1, если такого типа в списке нет:
constexpr size_t ti_prop1(size_t a)
{
return a != size_t(-1)? a + 1: -1;
}
template <class... Args>
constexpr size_t t_first_constructible = -1;
template <class T, class... Types, class... Args>
constexpr size_t t_first_constructible<
Type_list<T, Types...>, Args...> =
std::is_constructible<T, Args...>::value? 0
: ti_prop1(t_first_constructible<Type_list<Types...>, Args...>);
Теперь есть достаточно вспомогательных определений, чтобы сделать простенькую версию вариантного типа. (Более продвинутый вариант, к тому же не требующий C++14, есть в пакете библиотек Boost, аналог планируется включить в Стандартную библиотеку C++17.)
template <class... Types>
class Variant
{
public:
static_assert(sizeof...(Types) > 0,
"Variant: at least one type needed.");
static_assert(sizeof...(Types) < 64,
"Variant: too many possible types.");
using types = Type_list<Types...>;
Пакет Types (и отвечающий ему список типов types) содержит типы, значения которых могут хранится в объекте Variant. Потребуем, чтобы их было не меньше 0, но и не больше 63 (в принципе, текущая реализация допускает до 255 типов).
private:
using Storage = std::aligned_storage_t<
max_sizeof<types>, max_alignof<types>>;
Storage _stor;
uint8_t _type = -1;
public:
/// Извлечь индекс хранимого типа.
uint8_t get_type() const noexcept { return _type; }
/// Получить адрес хранилища.
const void* data() const noexcept { return &_stor; }
/// Проверить, "пуст" ли вариант (содержит ли он какое-либо значение).
bool empty() const noexcept { return _type == -1; }
Память выделяется статически в виде некоего “объекта” (просто кусок памяти нужного размера с нужным выравниванием) _stor. Номер типа текущего значения хранится в поле _type. Для “неинициализированных” Variant это –1, т.е. никакого значения нет. Такие объекты Variant будем называть пустыми.
Теперь представим, что нам нужно уничтожить значение, хранящееся в Variant. Иными словами, надо вызвать деструктор для того типа, номер которого записан в поле _type. Это легко реализовать, если обернуть вызовы деструкторов для всех Types в функции с общим интерфейсом, указатели на которые можно просто сохранить в массиве и выбирать их, используя _type в качестве индекса.
private:
template <class T>
static void dtor(void *p)
{
static_cast<T*>(p)->~T(); // явный вызов деструктора типа T.
}
static auto fetch_dtor(uint8_t t)
{
static void (*d[])(void*)
{ &dtor<Types>... }; // таблица деструкторов.
return d[t];
}
public:
/// Очистить содержимое, вернув к состоянию "пусто".
void clear()
{
if (!empty())
{
// Dispatch destructor call:
fetch_dtor(_type)(&_stor);
_type = -1;
}
}
// Деструктор, вызывает clear().
~Variant() { clear(); }
Для инициализации/присваивания объектов Variant данный трюк несложно повторить. Чтобы не усложнять код, реализуем минимальный рабочий вариант на основе конструктора копирования.
private:
template <class T>
static void copy_ctor(void *p, const void *val)
{
new (p) T(*static_cast<const T*>(val));
}
static auto fetch_copy_ctor(uint8_t t)
{
static void (*cc[])(void*, const void*)
{ ©_ctor<Types>... }; // Copy constructor table.
return cc[t];
}
public:
// Конструктор по умолчанию, создаёт пустой объект варианта.
Variant() = default;
// Копирующий конструктор.
Variant(const Variant &other) { *this = other; }
// Перемещающий конструктор.
// (По сути, не реализован -- вызывает копирование.)
Variant(Variant&& other) { *this = other; }
// Копирующий оператор присваивания -- выполняет основную работу.
Variant& operator=(const Variant &other)
{
if (this != &other)
{
clear();
// Dispatch copy constructor call:
fetch_copy_ctor(other._type)(&_stor, &other._stor);
_type = other._type;
}
return *this;
}
// Оператор присваивания произвольного значения.
// Доступен только если тип T есть в Types.
template <class T>
std::enable_if_t<
t_count<T, types> != 0,
Variant<Types...>&> operator=(const T &val)
{
return *this = Variant<Types...>(val); // possibly suboptimal.
}
Можно определить также универсальный конструктор, принимающий любой набор параметров и пытающийся подобрать первый тип из списка, который допускает инициализацию таким набором параметров. Ему не нужна таблица функций-обёрток, поскольку он выбирает нужный тип статически.
// Конструктор из произвольного набора аргументов.
// Выбирает первый подходящий тип из Types (слева направо) для инициализации.
template <class... Args>
Variant(Args&&... args)
{
static constexpr size_t i = t_first_constructible<types, Args&&...>;
static_assert(i != -1, "Variant constructor: invalid arguments.");
new (static_cast<void*>(&_stor))
t_at_t<i, types>(std::forward<Args>(args)...);
_type = uint8_t(i);
}
Наконец, можно определить оператор присваивания одной специализации Variant значения другой специализации Variant. Для этого нам понадобится таблица перевода значений _type:
// Присваивание варианта с более узким множеством возможных типов значений.
template <class... Subset>
std::enable_if_t<
t_subset<Type_list<Subset...>, types>,
Variant<Types...>&> operator=(const Variant<Subset...> &other)
{
clear();
if (other.empty())
return *this;
// Таблица перевода индексов other._type в this->_type.
static const uint8_t tt[]
{ t_index<Subset, types>... }; // для каждого элемента пакета Subset.
fetch_copy_ctor(tt[other.get_type()])(&_stor, other.data());
_type = tt[other.get_type()];
return *this;
}
Удовлетворяет ли наш класс Variant правилу пяти?
При работе с подобными полиморфными объектами может быть удобно состыковать их с встроенными средствами C++ RTTI (run-time type information). В частности, можно реализовать извлечение результата typeid для хранимого в Variant значения. Это опять же можно сделать с помощью заранее созданной таблицы.
// Получить значение type_info для хранимого типа.
const std::type_info& get_typeid() const
{
if (empty())
return typeid(void);
static const std::type_info* const ti[]
{ &typeid(Types)... };
return *ti[_type];
}
Наконец, требуется реализовать некий универсальный способ извлечения хранимого значения. Следуя Boost и C++17 реализуем шаблон проектирования “Посетитель” visitor. В качестве посетителя будет выступать функтор, принимающий любой из типов Types. Variant, получив такой функтор, вызовет его для типа хранимого значения. Нетрудно заметить, что деструктор, в общем-то, такой же посетитель, поэтому снова применим приём с таблицей функций-обёрток:
private:
template <class T, class Visitor>
static void do_visit(Visitor &&visitor, void *val)
{
visitor(*static_cast<T*>(val));
}
template <class Visitor>
static auto fetch_do_visit(uint8_t t)
{
using DVT = void(Visitor&&, void*);
static DVT* const vt[]
{ &do_visit<Types, Visitor>... };
return vt[t];
}
public:
// Обработать значение варианта с помощью паттерна "посетитель".
template <class Visitor>
void visit(Visitor &&visitor)
{
fetch_do_visit<Visitor>(_type)
(std::forward<Visitor>(visitor), &_stor);
}
Для простоты кодирования возможный результат функтора-посетителя отбрасывается.
Кувшинов Д.Р. © 2017