I'm using the following SFINAE pattern to evaluate a predicate on a variadic type list:
#include <type_traits>
void f(int = 0); // for example
template<typename... T,
typename = decltype(f(std::declval<T>()...))>
std::true_type check(T &&...);
std::false_type check(...);
template<typename... T> using Predicate = decltype(check(std::declval<T>()...));
static_assert(!Predicate<int, int>::value, "!!");
static_assert( Predicate<int>::value, "!!");
static_assert( Predicate<>::value, "!!"); // fails
int main() {
}
To my surprise, the ellipsis overload is selected when check
is called with an empty argument list, so Predicate<>
is std::false_type
even when the SFINAE-expression is valid!
Shouldn't variadic function templates always be preferred to ellipsis functions?
Are there any workarounds?
Ellipsis in C++ allows the function to accept an indeterminate number of arguments. It is also known as the variable argument list. Ellipsis tells the compiler to not check the type and number of parameters the function should accept which allows the user to pass the variable argument list.
With the variadic templates feature, you can define class or function templates that have any number (including zero) of parameters. To achieve this goal, this feature introduces a kind of parameter called parameter pack to represent a list of zero or more parameters for templates.
Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration.
Variadic functions are functions that can take a variable number of arguments. In C programming, a variadic function adds flexibility to the program. It takes one fixed argument and then any number of arguments can be passed.
When T...
is empty, the compiler performs overload resolution to determine which of
std::true_type check(); // instantiated from the function template
std::false_type check(...);
is the best viable candidate, as described in [over.match.best] 13.3.3/1 (quoting N3936):
Define ICSi(F) as follows:
if F is a static member function, ICS1 (F) is defined such that ICS1 (F) is neither better nor worse than ICS1 (G) for any function G, and, symmetrically, ICS1 (G) is neither better nor worse than ICS1 (F)132; otherwise,
let ICSi(F) denote the implicit conversion sequence that converts the i-th argument in the list to the type of the i-th parameter of viable function F. 13.3.3.1 defines the implicit conversion sequences and 13.3.3.2 defines what it means for one implicit conversion sequence to be a better conversion sequence or worse conversion sequence than another.
Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that,
the context is an initialization by user-defined conversion (see 8.5, 13.3.1.5, and 13.3.1.6) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type. [ Example:
struct A { A(); operator int(); operator double(); } a; int i = a; // a.operator int() followed by no conversion // is better than a.operator double() followed by // a conversion to int float x = a; // ambiguous: both possibilities require conversions, // and neither is better than the other
—end example ] or, if not that,
the context is an initialization by conversion function for direct reference binding (13.3.1.6) of a reference to function type, the return type of F1 is the same kind of reference (i.e. lvalue or rvalue) as the reference being initialized, and the return type of F2 is not [ Example:
template <class T> struct A { operator T&(); // #1 operator T&&(); // #2 }; typedef int Fn(); A<Fn> a; Fn& lf = a; // calls #1 Fn&& rf = a; // calls #2
—end example ] or, if not that,
F1 is not a function template specialization and F2 is a function template specialization, or, if not that,
- F1 and F2 are function template specializations, and the function template for F1 is more specialized than the template for F2 according to the partial ordering rules described in 14.5.6.2.
In this case, the conversion sequences for both candidates are empty since there are no arguments. The second to last bullet is the deciding factor:
- F1 is not a function template specialization and F2 is a function template specialization, or, if not that,
hence the non-template std::false_type check(...);
is preferred.
My preferred workaround - obviously there are many - would be to make both candidates templates and discriminate via the ellipsis conversion [over.ics.ellipsis] 13.3.3.1.3/1:
An ellipsis conversion sequence occurs when an argument in a function call is matched with the ellipsis parameter specification of the function called (see 5.2.2).
by giving the "preferred" template declaration an extraneous parameter that is a clearly better match, since any other conversion sequence will be preferred to an ellipsis conversion per [over.ics.rank] 13.3.3.2/2:
When comparing the basic forms of implicit conversion sequences (as defined in 13.3.3.1)
- a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and
- a user-defined conversion sequence (13.3.3.1.2) is a better conversion sequence than an ellipsis conversion sequence (13.3.3.1.3).
Example:
template<typename... T,
typename = decltype(f(std::declval<T>()...))>
std::true_type check(int);
template<typename...>
std::false_type check(...);
template<typename... T> using Predicate = decltype(check<T...>(0));
That is surprising to me too.
One workaround could be to pass an int
(e.g 0
) as first argument to check()
and force the compiler to try the template version first:
template<typename... T, typename = decltype(f(std::declval<T>()...))>
std::true_type check(int &&, T &&...); //ADDED `int &&` as the first parameter type
std::false_type check(...);
template<typename... T> using Predicate = decltype(check(0, std::declval<T>()...));
Note that the temporary created out of 0
would try to bind to int&&
first (and that is very crucial here), then if the value-sfinae fails, then it would try the second overload.
Hope that helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With