Ответ 1
Как будет выглядеть идеальный интерфейс?
Если бы мы дали переменную типа Ints<S...>
, мы бы идеально могли использовать S...
с минимальными изменениями, насколько это возможно.
В этом случае мы можем спроектировать интерфейс, который позволит нам использовать пакет параметров в качестве входных данных для функции с переменным числом или лямбда-выражения, вплоть до повторного использования значений в качестве параметров шаблона.
Предлагаемый интерфейс [Динамический регистр/целые числа, переданные как значения]
Как статический, так и динамический корпус имеют схожие интерфейсы, однако динамический корпус немного чище и позволяет лучше познакомиться с ним. Учитывая переменную и функцию, мы применяем функцию с пакетом параметров, содержащимся в определении переменной.
Ints<1, 2, 3> ints;
// Get a vector from ints
// vec = {1, 2, 3}
auto vec = ints | [](auto... S) { return std::vector {S...}; };
// Get an array from ints
// arr = {1, 2, 3}
auto arr = ints | [](auto... S) { return std::array {S...}; };
// Get a tuple from ints
// tup = {1, 2, 3}
auto tup = ints | [](auto... S) { return std::make_tuple(S...); };
// Get sum of ints using a fold expression
auto sum = ints | [](auto... S) { return (S + ...); };
Это простой унифицированный синтаксис, который позволяет нам брать S
и использовать его в качестве пакета параметров.
Написание этого интерфейса
Эта часть довольно проста тоже. Мы берем переменную типа Ints<S...>
и функцию и применяем функцию с S...
template<int... S, class Func>
auto operator|(Ints<S...>, Func&& f) {
return f(S...);
}
Предлагаемый интерфейс [Статический регистр/целые, используемые в качестве параметров шаблона]
Как указывалось ранее, статический случай имеет интерфейс, аналогичный динамическому случаю, и концептуально он не будет слишком сложным. С точки зрения пользователя, единственное отличие состоит в том, что вместо использования S...
в качестве пакета параметров мы будем ll use
S.value... 'в качестве пакета.
Для каждого значения мы хотим инкапсулировать его в соответствующий тип, связанный с этим значением. Это позволяет нам получить к нему доступ в контексте constexpr.
template<int Value>
struct ConstInt {
constexpr static int value = Value;
};
Чтобы отличить его от динамического случая, я собираюсь перегрузить /
вместо |
, В противном случае они ведут себя аналогично. Реализация почти такая же, как динамический случай, за исключением того, что значения заключены в класс ConstInt
, и каждый из них будет иметь свой собственный тип.
template<int... S, class F>
auto operator/(Ints<S...>, F&& func) {
return func(ConstInt<S>()...);
}
Используя этот интерфейс статически
C++ позволяет нам получать доступ к статическим членам класса, используя тот же синтаксис, что и нестатические члены, без потери статуса constexpr
.
Допустим, у меня есть некоторый ConstInt
со значением 10. Я могу напрямую использовать I.value
в качестве параметра шаблона или использовать decltype(I)::value
:
// This is what'll be passed in as a parameter
ConstInt<10> I;
std::array<int, I.value> arr1;
std::array<int, decltype(I)::value> arr2;
// Both have length 10
Поэтому расширение пакета параметров является чрезвычайно простым, и в конечном итоге оно практически идентично динамическому регистру, единственное отличие заключается в том, что .value
добавляется к S
Ниже показаны примеры из динамического регистра, на этот раз с использованием синтаксиса статического регистра:
Ints<1, 2, 3> ints;
// Get a vector from ints
auto vec = ints | [](auto... S) { return std::vector {S.value...}; };
// Get an array from ints
// arr = {1, 2, 3}
auto arr = ints | [](auto... S) { return std::array {S.value...}; };
// Get a tuple from ints
auto tup = ints | [](auto... S) { return std::make_tuple(S.value...); };
// Get sum of ints using a fold expression
auto sum = ints | [](auto... S) { return (S.value + ...); };
Так что нового? Поскольку value
является constexpr, S.value
можно использовать тривиально в качестве параметра шаблона. В этом примере мы используем S.value
для индексации кортежа с помощью std::get
:
auto tupA = std::make_tuple(10.0, "Hello", 3);
auto indicies = Ints<2, 0, 1>{};
// tupB = {3, 10.0, "Hello"}
auto tupB = indicies / [&](auto... S) {
return std::make_tuple(std::get<S.value>(tupA)...);
};
И в этом примере мы возводим в квадрат каждый элемент в последовательности и возвращаем новую последовательность:
auto ints = Ints<0, 1, 2, 3, 4, 5>();
// ints_squared = Ints<0, 1, 4, 9, 16, 25>();
auto ints_squared = ints / [](auto... S) {
return Ints<(S.value * S.value)...>();
};
Альтернативное решение, позволяющее избежать перегрузки оператора
Если вы хотите избежать перегрузки операторов, мы можем черпать вдохновение из функционального программирования и обрабатывать вещи с помощью функции unpack
, написанной так:
template<int... vals>
auto unpack(Ints<vals...>) {
return [](auto&& f) { return f(vals...); };
}
// Static case
template<int... vals>
auto unpack_static(Ints<vals...>) {
return [](auto&& f) { return f(ConstInt<vals>()...); };
}
Так что же такое unpack
? Эта функция принимает набор значений и возвращает функцию, которая принимает другую функцию и применяет функцию с значениями в качестве входных данных.
Функция unpack
позволяет нам применять эти значения к другой функции в качестве параметров.
Мы можем присвоить результат переменной с именем apply_ints
, а затем мы можем использовать apply_ints
для обработки всех конкретных случаев использования:
Ints<1, 2, 3> ints; //this variable has our ints
auto apply_ints = unpack(ints); // We use this function to unpack them
Мы можем переписать примеры из ранее, на этот раз используя apply_ints
:
// Get a vector from ints
// vec = {1, 2, 3}
auto vec = apply_ints([](auto... S) { return std::vector {S...}; });
// Get an array from ints
// arr = {1, 2, 3}
auto arr = apply_ints([](auto... S) { return std::array {S...}; });
// Get a tuple from ints
// tup = {1, 2, 3}
auto tup = apply_ints([](auto... S) { return std::make_tuple(S...); });
// Get sum of ints using a fold expression
auto sum = apply_ints([](auto... S) { return (S + ...); });
аппендикс
В этом приложении дается краткий обзор, показывающий, как использовать этот синтаксис более широко (например, при работе с несколькими отдельными пакетами параметров).
Бонусный пример: объединение значений из двух отдельных пакетов
Чтобы дать вам лучшее представление о гибкости этого интерфейса, вот пример, в котором мы используем его для объединения значений из двух отдельных пакетов.
Ints<1, 2, 3> intsA;
Ints<10, 20, 30> intsB;
// pairs = {{1, 10}, {2, 20}, {3, 30}}
auto pairs = intsA | [&](auto... S1) {
return intsB | [&](auto... S2) {
return std::vector{ std::pair{S1, S2}... };
};
};
NB: MSVC и GCC компилируют этот пример без проблем, однако Clang не справляется с этим. Я предполагаю, что MSVC и GCC верны, но я точно не знаю.
Бонусный пример: получение двумерной таблицы времен
Этот пример немного сложнее, но мы также можем создать двухмерные массивы значений, которые извлекаются из всех комбинаций значений из отдельных пакетов.
В этом случае я использую его для создания таблицы времени.
Ints<1, 2, 3, 4, 5, 6, 7, 8, 9> digits;
auto multiply = [](auto mul, auto... vals) {
return std::vector{(mul * vals)...};
};
auto times_table = digits | [&](auto... S1) {
return digits | [&](auto... S2) {
return std::vector{ multiply(S1, S2...)... };
};
};