Возьмите следующий код, который представляет собой упрощенный пример:
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...);
});
}
Функция принимает обобщенную c вариативную c лямбду и вызывает ее с фиксированным набором аргументов. Сама эта лямбда затем просто вызывает другую функцию / лямбда с соответствующим прототипом. Как и следовало ожидать, в сценарии 2, когда f
вызывается внутри foo
, компилятор выводит params...
как пакет параметров {1, 1}
.
Для сценария № 1 я использую код из другого Q&A , чтобы вывести арность вызываемого объекта. Если, однако, такой объект может быть вызван с более чем заранее определенным максимальным количеством аргументов, он рассматривается как «вариант c». В деталях, is_variadic_v
будет использовать форму выражения SFINAE, в которой будет предпринята попытка вызвать объект функции с уменьшающимся числом аргументов, имеющих «произвольный тип», который неявно может быть преобразован во что угодно.
Проблема в том, что теперь, когда очевидно, что компилятор будет выводить F (и его пакет аргументов) во время этого метакода, и если это вариант c (например, в этом случае), он выводит F как лямбду, принимая фиктивные аргументы, то есть что-то вроде main()::lambda(<arbitrary_type<0>, arbitrary_type<1>, arbitrary_type<2>, ..., arbitrary_type<N>>)
, если N - "предел вариативности c" сверху. Теперь 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...);
});
}
Могу ли я предотвратить такое поведение? Могу ли я заставить компилятор повторно выводить список параметров?
РЕДАКТИРОВАТЬ:
Дополнительная особенность состоит в том, что компилятор, кажется, действует как шизофреник c. Когда я изменяю содержимое foo
на
foo([&some](auto... params) {
// int foo = std::index_sequence<sizeof...(params)>{ };
std::cout << sizeof...(params) << '\n';
});
, компилятор создаст программу, которая напечатает 2
в этом примере. Если, однако, я включаю закомментированную строку (которая, поскольку это не имеет смысла, должна вызывать диагностику компилятора c), я сталкиваюсь с
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
, потому что тогда он этого не сделал.
По-видимому, очень важно для вывода пакета параметров, что написано внутри лямбда. Это только у меня или такое поведение действительно выглядит патологическим? c?