Как получить параметры шаблона переменной формы без шаблона вспомогательной функции?

Предположим, у меня есть

template<int ...>
struct Ints { };

class MyClass
{
public:
    Ints<1, 2, 3> get() { return Ints<1, 2, 3>(); }
};

То, что я хочу сделать, просто.

template <class T>
vector<int> MyFunc1(T& x)
{
    Ints<S...> result = x.get();
    vector<int> t = { S... };
    return t;
}

Примерно так. (Здесь MyClass может быть одним из примеров T) По-видимому, для компилятора S... кажется недействительным.

template <class T, int... S>
vector<int> MyFunc2(T& x)
{
    Ints<S...> result = x.get();
    vector<int> t = { S... };
    return t;
}

Это тоже не работает. Я думаю, что из get() S... становится специфичным и автоматически выводимым, но компилятор не распознает его. (Я не уверен, но C++ не выводит параметры шаблона, смотрящие внутри функции, а только аргументы и возвращаемый тип)

Единственный способ, который я нашел, - это использовать другую функцию, которая узнает, что int... было.

template <int ...S>
vector<int> temp(Ints<S...> not_used)
{
    return { S... };
}

template <class T>
vector<int> MyFunc3(T& x)
{
    auto result = x.get();
    return temp(result);
}

Он работает хорошо, но требует еще одной дополнительной вспомогательной функции, которая ничего не делает, а просто обеспечивает синтаксически понятный способ сопоставления S... с использованием шаблонов.

Я действительно хочу сделать это только в одной функции. Действительно ли мне нужно определять вспомогательную функцию всякий раз, когда я хочу получать пакеты параметров, каждый раз?

Изменение: Ints и MyFunc просто игрушечный пример. Я хочу знать общий метод получения параметров шаблона!

Ответы

Ответ 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...)... };
    };
};

Ответ 2

В С++ 2a вы можете использовать шаблонную лямбду для определения вашего помощника внутри вашей функции, что-то вроде:

auto v = []<std::size_t...Is>(std::index_sequence<Is...>){return std::vector{Is...};}(seq);
//         ^^^^^^^^^^^^^^^^^^ New in C++2a

демонстрация

Ответ 3

Я бы предложил добавить функции в struct Int чтобы получить разные представления

#include <vector>
#include <array>

template<int ...values>
struct Ints {
    auto getAsVector() const {
        return std::vector<int>({ values... });
    }

    constexpr auto getAsArray() const {
        return std::array<int, sizeof...(values)>({ values... });
    }
};

class MyClass
{
public:
    Ints<1, 2, 3> get() { return Ints<1, 2, 3>(); }
};

int main() {
    MyClass a;
    auto array = a.get().getAsVector();
    return array.size();

}

Ответ 4

Если вы не используете/не создаете вспомогательный шаблон, вам нужен другой способ предоставления значений.

Самый простой, канонический и общий способ, который я могу придумать, это поместить их в ту же область видимости класса, чтобы ваша структура Ints стала:

template<int ...ints>
struct Ints {
  constexpr static std::initializer_list<int> vals = {ints...};
};

Так как это constexpr его следует оценивать во время компиляции, а не затраты времени выполнения. Теперь вы сможете сделать что-то вроде:

return std::vector<int>(ints.vals);