Могу ли я изменить порядок вывода аргументов шаблона для универсальной вариативной лямбды?

Возьмите следующий код, который представляет собой упрощенный пример:

template <typename F>
void foo(F f) {
    //bool some = is_variadic_v<F>; // Scenario #1
    bool some = true;               // Scenario #2
    f(int(some), int(some));
}

int main() {
    auto some = [](int i, int j) {
        std::cout << i << " " << j << '\n';
    };
    
    foo([&some](auto... params) {
        some(params...);
    });
}

Функция принимает универсальную вариативную лямбду и вызывает ее с фиксированным набором аргументов. Сама эта лямбда затем просто вызывает другую функцию / лямбда с соответствующим прототипом. Как и следовало ожидать, в сценарии 2, когда f вызывается внутри foo, компилятор выводит params... как пакет параметров {1, 1}.

Для сценария № 1 я использую код из другого вопросов и ответов, чтобы определить арность вызываемого объекта. . Однако, если такой объект вызывается с более чем заранее определенным максимальным количеством аргументов, он считается вариативным. В деталях, is_variadic_v будет использовать форму выражения SFINAE, в которой будет предпринята попытка вызвать объект функции с уменьшающимся числом аргументов, имеющих произвольный тип, который неявно может быть преобразован во что угодно.

Проблема теперь в том, что, по-видимому, компилятор будет выводить F (и его пакет аргументов) во время этого метакода, и если он является вариативным (например, в этом случае), он выводит F как лямбду, принимая фиктивные аргументы, то есть что-то вроде main()::lambda(<arbitrary_type<0>, arbitrary_type<1>, arbitrary_type<2>, ..., arbitrary_type<N>>), если N - вариативный предел сверху. Теперь params... выводится как arbitrary_type<1>, arbitrary_type<2>, ... и, соответственно, вызов some(params...) завершится ошибкой. Это поведение можно продемонстрировать в этом небольшом примере кода:

#include <utility>
#include <type_traits>
#include <iostream>

constexpr int max_arity = 12; // if a function takes more arguments than that, it will be considered variadic

struct variadic_type { };

// it is templated, to be able to create a
// "sequence" of arbitrary_t's of given size and
// hence, to 'simulate' an arbitrary function signature.
template <auto>
struct arbitrary_type {
    // this type casts implicitly to anything,
    // thus, it can represent an arbitrary type.
    template <typename T>
    operator T&&();

    template <typename T>
    operator T&();
};

template <
    typename F, auto ...Ints,
    typename = decltype(std::declval<F>()(arbitrary_type<Ints>{ }...))
>
constexpr auto test_signature(std::index_sequence<Ints...> s) {
    return std::integral_constant<int, size(s)>{ };
}

template <auto I, typename F>
constexpr auto arity_impl(int) -> decltype(test_signature<F>(std::make_index_sequence<I>{ })) {
    return { };
}

template <auto I, typename F, typename = std::enable_if_t<(I > 0)>>
constexpr auto arity_impl(...) {
    // try the int overload which will only work,
    // if F takes I-1 arguments. Otherwise this
    // overload will be selected and we'll try it 
    // with one element less.
    return arity_impl<I - 1, F>(0);
}

template <typename F, auto MaxArity>
constexpr auto arity_impl() {
    // start checking function signatures with max_arity + 1 elements
    constexpr auto tmp = arity_impl<MaxArity+1, F>(0);
    if constexpr (tmp == MaxArity+1) 
        return variadic_type{ }; // if that works, F is considered variadic
    else return tmp; // if not, tmp will be the correct arity of F
}

template <typename F, auto MaxArity = max_arity>
constexpr auto arity(F&&) { return arity_impl<std::decay_t<F>, MaxArity>(); }

template <typename F, auto MaxArity = max_arity>
constexpr auto arity_v = arity_impl<std::decay_t<F>, MaxArity>();

template <typename F, auto MaxArity = max_arity>
constexpr bool is_variadic_v = std::is_same_v<std::decay_t<decltype(arity_v<F, MaxArity>)>, variadic_type>;

template <typename F>
void foo(F f) {
    bool some = is_variadic_v<F>;
    //bool some = true;
    f(int(some), int(some));
}

int main() {
    auto some = [](int i, int j) {
        std::cout << i << " " << j << '\n';
    };
    
    foo([&some](auto... params) {
        some(params...);
    });
}

Могу ли я предотвратить такое поведение? Могу ли я заставить компилятор повторно выводить список параметров?


РЕДАКТИРОВАТЬ:

Дополнительная особенность состоит в том, что компилятор, кажется, ведет себя как бы шизофреником. Когда я меняю содержимое foo на

foo([&some](auto... params) {
    // int foo = std::index_sequence<sizeof...(params)>{ };
    std::cout << sizeof...(params) << '\n';
});

компилятор создаст программу, которая будет печатать 2 в этом примере. Если, однако, я включаю закомментированную строку (которая, поскольку это не имеет смысла, должна запускать диагностику компилятора), я сталкиваюсь с

error: cannot convert 'std::index_sequence<13>' {aka 'std::integer_sequence<long unsigned int, 13>'} to 'int' in initialization
   85 |         int foo = std::index_sequence<sizeof...(params)>{ };

Итак, теперь компилятор выводит sizeof...(params) как 2 и 13 одновременно? Или он передумал и теперь выбирает 13 только потому, что я добавил в лямбду еще одно выражение? Компиляция также не удастся, если я вместо этого выберу static_assert(2 == sizeof...(params));. Итак, компилятор выводит sizeof...(params) == 2, за исключением случаев, когда я спрашиваю его, действительно ли он выводил 2, потому что тогда он этого не сделал.

По-видимому, для вывода пакета параметров очень важно то, что написано внутри лямбда. Это только у меня или такое поведение действительно выглядит патологическим?


person Jodocus    schedule 10.07.2020    source источник
comment
теперь компилятор выводит sizeof ... (params) равным 2 и 13 одновременно Оба! Но не одновременно. Он проверяет (экземпляры - правильное слово) тело лямбды для обоих случаев (то есть для каждого набора типов параметров, с которыми вы пытаетесь вызвать лямбда).   -  person HolyBlackCat    schedule 11.07.2020
comment
Я получаю флюиды XY-проблемы. Зачем нужно знать, является ли лямбда вариативной или нет?   -  person HolyBlackCat    schedule 11.07.2020
comment
@HolyBlackCat Для меня это звучит одновременно фантастически и пугающе. В обычном SFINAE он удаляет кандидатов на перегрузку только в том случае, если создание экземпляра дает ошибку в типе возвращаемого значения или аргументах (шаблоне), но не в теле. Согласно вашей теории, если я добавлю static_assert (2 == sizeof ... (params)); перед cout, он все равно должен компилироваться, не так ли? Но это не так! Есть кое-что, чего я не понимаю.   -  person Jodocus    schedule 11.07.2020
comment
@HolyBlackCat О XY: у меня есть функция, которая создает экземпляр цикла времени компиляции N раз, N - это арность переданной функции. Если эта функция, однако, является вариативной, я предполагаю максимальное количество разумных циклов, которое, как правило, не равно пределу, при котором я считаю функцию вариативной.   -  person Jodocus    schedule 11.07.2020
comment
довольно сбивает с толку то, что вы говорите о двух разных версиях кода, и то, что кажется важной частью, не включено в вопрос. Ссылки могут гнить, также ссылки на другие вопросы / ответы SO. Что, если этот ответ отредактируют? Пожалуйста, включите весь код в свой вопрос   -  person 463035818_is_not_a_number    schedule 11.07.2020
comment
@Jodocus Согласно вашей теории, если я добавлю static_assert тело лямбды не будет считаться частью так называемого немедленного контекста, поэтому любая ошибка в нем будет аппаратная ошибка (то есть прервет компиляцию, а не обнаружит с помощью SFINAE). Кажется, что в этом случае тело лямбда создается (и поэтому проверяется) только для определения возвращаемого типа. Если вы добавите -> void к лямбде, ваш пример wandbox будет скомпилирован.   -  person HolyBlackCat    schedule 11.07.2020
comment
У меня есть функция, которая N раз создает экземпляр цикла времени компиляции Не могли бы вы дать более подробную информацию? Для чего нужны петли? Вы пытаетесь вызвать лямбду со всеми возможными комбинациями аргументов?   -  person HolyBlackCat    schedule 11.07.2020
comment
@ idclev463035818 Извините, я добавил в вопрос полный код.   -  person Jodocus    schedule 11.07.2020
comment
@HolyBlackCat Скажем, у меня есть функция обратного вызова, принимающая N (целых) аргументов. Каждый аргумент работает в определенном диапазоне [начало, конец], который зависит от предыдущих аргументов. Метапрограмма генерирует вложенный цикл для каждого аргумента и сохраняет вложенные циклы до тех пор, пока количество циклов == Арность обратного вызова, s.t. обратный вызов вызывается для каждой комбинации в четко определенном порядке. В вариативном случае я хочу сгенерировать максимальное количество вложенных циклов. Мне очень нужен этот шаблон, поэтому я хочу автоматически сгенерировать этот код с помощью метапрограммы. Надеюсь, это добавит ясности.   -  person Jodocus    schedule 11.07.2020
comment
@HolyBlackCat Похоже, что в этом случае тело лямбда создается (и поэтому проверяется) только для определения возвращаемого типа. Да, это работает! Хотя подробности правил остаются для меня туманными, у меня до сих пор нет полезного сообщения. Это похоже на то, что у компилятора есть кандидат на вывод, и когда он входит в тело лямбда-выражения для вывода возвращаемого типа, он отбрасывает его, а также выводит новые типы аргументов (в данном случае список произвольных_типов ‹›)?   -  person Jodocus    schedule 11.07.2020