Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is an ellipsis preferred to a variadic template when called with no arguments?

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?

like image 671
ecatmur Avatar asked May 22 '14 10:05

ecatmur


People also ask

What does ellipsis mean in C++?

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.

What is the use of Variadic templates?

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.

What is Variadic template in C++?

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.

What is a variadic function in C?

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.


2 Answers

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));
like image 58
Casey Avatar answered Oct 05 '22 06:10

Casey


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.

  • Demo @ Coliru

Hope that helps.

like image 45
Nawaz Avatar answered Oct 05 '22 04:10

Nawaz