Почему эллипсис предпочитает вариационный шаблон при вызове без аргументов?

Я использую следующий шаблон SFINAE для оценки предиката в списке вариационного типа:

#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() {
}

К моему удивлению, перегрузка многоточия выбирается, когда check вызывается с пустым списком аргументов, поэтому Predicate<> есть std::false_type, даже если справедливо выражение SFINAE!

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

Есть ли способы обхода?

Ответ 1

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

std::true_type check(); // instantiated from the function template
std::false_type check(...);

является наилучшим жизнеспособным кандидатом, как описано в [over.match.best] 13.3.3/1 (цитирование N3936):

Определите ICSi (F) следующим образом:

  • если F является статической функцией-членом, ICS1 (F) определяется таким образом, что ICS1 (F) не лучше и не хуже, чем ICS1 (G) для любой функции G, и симметрично ICS1 (G) является ни лучше, ни хуже, чем ICS1 (F) 132; в противном случае,

  • пусть ICSi (F) обозначает неявную последовательность преобразований, которая преобразует i-й аргумент в список в тип i-го параметра жизнеспособной функции F. 13.3.3.1 определяет неявные последовательности преобразования и 13.3.3.2 определяет, что означает, что одна неявная последовательность конверсии является лучшей последовательностью преобразования или хуже, чем другая.

Учитывая эти определения, жизнеспособная функция F1 определяется как лучшая функция, чем другая жизнеспособная функция F2, если для всех аргументов я ICSi (F1) не является худшей последовательностью преобразования, чем ICSi (F2), а затем

  • для некоторого аргумента j, ICSj (F1) является лучшей последовательностью преобразования, чем ICSj (F2), или, если не это,

  • контекст представляет собой инициализацию путем пользовательского преобразования (см. 8.5, 13.3.1.5 и 13.3.1.6) и стандартную последовательность преобразования из возвращаемого типа F1 в тип назначения (т.е. тип инициализированный объект) является лучшей последовательностью преобразования, чем стандартная последовательность преобразования из возвращаемого типа F2 в тип назначения. [Пример:

    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] или, если не это,

  • контекст представляет собой инициализацию с помощью функции преобразования для привязки прямого ссылки (13.3.1.6) ссылки на тип функции, возвращаемый тип F1 является тем же типом ссылки (то есть lvalue или rvalue) в качестве ссылки инициализируется, а тип возврата F2 не является [Пример:

    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] или, если не это,

  • F1 не является специализированной функцией шаблона, а F2 является специализированной функцией шаблона или, если не это,

  • F1 и F2 являются специализированными шаблонами функций, а шаблон функции для F1 более специализирован, чем шаблон для F2 в соответствии с правилами частичного упорядочения, описанными в 14.5.6.2.

В этом случае последовательности преобразования для обоих кандидатов пусты, поскольку аргументов нет. Решающим фактором является вторая - последняя пуля:

  • F1 не является специализированной функцией шаблона, а F2 является специализированной функцией шаблона, или, если это не так,

поэтому предпочтительнее не шаблон std::false_type check(...);.


Мое предпочтительное обходное решение - очевидно, есть много - было бы делать шаблоны обоих кандидатов и различать с помощью преобразования многоточия [over.ics.ellipsis] 13.3.3.1.3/1:

Последовательность преобразования многоточия возникает, когда аргумент в вызове функции сопоставляется с спецификацией параметра многоточия функции, называемой (см. 5.2.2).

предоставив "предпочтительное" объявление шаблона постороннего параметра, который явно лучше соответствует, так как любая другая последовательность преобразования будет предпочтительнее преобразования многоточия за [over.ics.rank] 13.3.3.2/2:

При сравнении основных форм неявных последовательностей преобразования (как определено в 13.3.3.1)

  • стандартная последовательность преобразования (13.3.3.1.1) является лучшей последовательностью преобразования, чем пользовательская последовательность преобразования или последовательность преобразования многоточия, и
  • определяемая пользователем последовательность преобразований (13.3.3.1.2) является лучшей последовательностью преобразования, чем последовательность преобразования многоточия (13.3.3.1.3).

Пример:

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));

Ответ 2

Это тоже удивительно.

Обходным решением может быть передача int (например, 0) в качестве первого аргумента в check() и принудительное выполнение компилятором первой версии шаблона:

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>()...));

Обратите внимание, что временное созданное из 0 будет пытаться привязываться к int&& first (и это очень важно здесь), тогда, если значение-sfinae терпит неудачу, тогда оно попытается вторая перегрузка.

Надеюсь, что это поможет.