Шаблоны с переменным числом параметров

Кувшинов Д.Р.

2017


Общее оглавление


Введение

Начиная с версии 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). Из-за такого минимализма возможных действий с пакетами обычным способом определения функций-шаблонов с переменным числом параметров является рекурсивное определение.


Пример 1: суммирование целых чисел во время компиляции

Рассмотрим простенький пример шаблона шаблона класса с переменным числом параметров. На этот раз параметры шаблона не будут типами, а будут значениями типа 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));


Пример 2: println

Определим две функции: 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...);
}

C++, HTML


Пример 3: format

Предположим, есть набор файлов, описывающих строки интерфейса некоторого приложения на разных языках. Например, английском и русском:

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;
}

C++, HTML


Используя регулярные выражения (#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;
}

C++, HTML


Пример 4: список типов

Список типов 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++, HTML


Пример 5: Variant

В наследство от 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*)
    { &copy_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);
  }

Для простоты кодирования возможный результат функтора-посетителя отбрасывается.

C++, HTML


Общее оглавление

Кувшинов Д.Р. © 2017