Как изменение аргумента шаблона с типа на нетип заставляет SFINAE работать? - PullRequest
7 голосов
/ 13 апреля 2019

Из статьи cppreference.com в std::enable_if,

Примечания
Распространенной ошибкой является объявление двух шаблонов функций, которые отличаютсятолько в их аргументах шаблона по умолчанию.Это недопустимо, поскольку аргументы шаблона по умолчанию не являются частью сигнатуры шаблона функции, а объявление двух разных шаблонов функций с одной и той же сигнатурой недопустимо.

/*** WRONG ***/

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename = std::enable_if_t<std::is_floating_point<Floating>::value>
    >
    T(Floating) : m_type(float_t) {} // error: cannot overload
};

/* RIGHT */

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
    >
T(Floating) : m_type(float_t) {} // OK
};

У меня естьТрудно понять, почему версия *** WRONG *** не компилируется, а версия *** RIGHT***.Объяснение и пример являются для меня культом груза.Все, что было сделано выше, это изменение параметра шаблона типа на параметр шаблона нетипичного типа.Для меня обе версии должны быть действительными, потому что обе полагаются на std::enable_if<boolean_expression,T>, имеющий член typedef с именем type, а std::enable_if<false,T> не имеет такого члена.Ошибка замены (которая не является ошибкой) должна приводить к обеим версиям.

Глядя на стандарт, в [temp.deduct] говорится, что

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

и позже,

, если аргумент шаблона не был выведен, а соответствующий ему параметр шаблона имеет аргумент по умолчаниюаргумент шаблона определяется путем подстановки аргументов шаблона, определенных для предыдущих параметров шаблона, в аргумент по умолчанию. Если подстановка приводит к недопустимому типу, как описано выше, вычитание типа завершается неудачно.

То, что сбой вычета типа не обязательно является ошибкой, - это то, чем занимается SFINAE.

Почему изменение параметра шаблона typename в версии *** WRONG *** на параметр без typename делает *** RIGHT *** версию "правильной"?

Ответы [ 6 ]

10 голосов
/ 20 апреля 2019

В основном потому, что [temp.over.link] / 6 не говорит об аргументе шаблона по умолчанию:

Две головки шаблона эквивалентны, еслиих списки параметров-шаблонов имеют одинаковую длину, соответствующие параметры-шаблона эквивалентны, и если любой из них имеет require-claus , они оба имеют require-предложения и соответствующие выражения-ограничения эквивалентны.Два шаблона-параметра эквивалентны при следующих условиях:

  • они объявляют параметры шаблона одного типа,

  • если любой из них объявляет пакет параметров шаблона, они оба делают,

  • , если они объявляют нетипичные параметры шаблона, они имеют эквивалентные типы,

  • если они объявляют параметры шаблона шаблона, их параметры шаблона эквивалентны, и

  • , если любой из них объявлен с имя-квалифицированного концепта , они оба являются, и квалифицированные имена понятий эквивалентны.

Затем [temp.over.link] / 7 :

Два функциональных шаблона эквивалентны , если они объявлены в одной и той же области, имеют одно и то же имя, имеют эквивалентные головки-шаблона и имеют типы возврата, списки параметрови завершающие require-пункты (если таковые имеются), которые эквивалентны с использованием описанных правилЧтобы сравнить выражения с параметрами шаблона, см. выше.

... два шаблона в первом примере эквивалентны, а два шаблона во втором примере - нет.Таким образом, два шаблона в вашем первом примере объявляют одну и ту же сущность и приводят к неправильной конструкции: [class.mem] / 5 :

Член не должен быть объявлендважды в спецификации члена , ...

10 голосов
/ 13 апреля 2019

Переписав ссылку на cppreference, в неправильном случае имеем:

 typename = std::enable_if_t<std::is_integral<Integer>::value>
 typename = std::enable_if_t<std::is_floating_point<Floating>::value>

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

В правильном случае:

typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0

и

typename std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0

у вас больше нет аргументов шаблона по умолчанию, но двух разных типов со значением значение по умолчанию (= 0). Отсюда подписи разные


Обновление от комментария : чтобы уточнить разницу,

Пример с параметром шаблона с типом по умолчанию:

template<typename T=int>
void foo() {};

// usage
foo<double>();
foo<>();

Пример с нетиповым параметром шаблона со значением по умолчанию

template<int = 0>
void foo() {};

// usage
foo<4>();
foo<>();

Последнее, что может сбить с толку в вашем примере, это использование enable_if_t, фактически в вашем правильном регистре кода у вас есть лишнее typename:

 template <
    typename Integer,
    typename std::enable_if_t<std::is_integral<Integer>::value, int> = 0
>
T(Integer) : m_type(int_t) {}

лучше записать как:

template <
    typename Floating,
    std::enable_if_t<std::is_floating_point<Floating>::value, int> = 0
>

(то же самое относится ко второму объявлению).

Это точно роль enable_if_t:

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

для добавления не нужно typename (по сравнению со старшим enable_if)

6 голосов
/ 13 апреля 2019

Первая версия неверна, так же как и этот фрагмент:

template<int=7>
void f();
template<int=8>
void f();

Причина не связана с ошибкой замещения: подстановка происходит только в том случае, если шаблоны функций используются (например, в функции вызов ), а простые объявления достаточно, чтобы вызвать ошибку компиляции.

Соответствующая стандартная формулировка: [dcl.fct.default] :

Аргумент по умолчанию должен быть указан только в [...] или в параметре шаблона ([temp.param]); [...]

Аргумент по умолчанию не должен быть переопределен более поздним объявлением (даже с тем же значением).

Вторая версия верна, поскольку шаблоны функций имеют разную сигнатуру и поэтому не рассматриваются компилятором как одна и та же сущность.

3 голосов
/ 13 апреля 2019

Давайте попробуем опустить значения параметров по умолчанию и другие имена (помните: параметры шаблона по умолчанию не являются частью сигнатуры шаблона функции, как и имена параметров) и посмотрим, как будут выглядеть «неправильные» сигнатуры функций шаблона:

template
<
     typename FirstParamName
,    typename SecondParamName
>
T(FirstParamName)

template
<
    typename FirstParamName
,   typename SecondParamName
>
T(FirstParamName)

Ух, они точно такие же!Таким образом, T(Floating) на самом деле является переопределением версии T(Integer), тогда как версия Right объявляет два шаблона с разными параметрами:

template
<
     typename FirstParamName
,    std::enable_if_t<std::is_integral<FirstParamName>::value, int> SecondParamName
> 
T(FirstParamName)

template
<
    typename FirstParamName
,   std::enable_if_t<std::is_floating_point<FirstParamName>::value, int> SecondParamName
>
T(FirstParamName)

Также обратите внимание, что нет необходимости использовать typename до std::enable_if_t<std::is_floating_point<Floating>::value, int> в«Правильное» объявление шаблона, потому что там нет имен зависимых типов.

1 голос
/ 19 апреля 2019

Я собираюсь сделать небольшую переписку неверной версии, чтобы помочь поговорить о том, что происходит.

struct T {
    enum { int_t,float_t } m_type;
    template <
        typename Integer,
        typename U = std::enable_if_t<std::is_integral<Integer>::value>
    >
    T(Integer) : m_type(int_t) {}

    template <
        typename Floating,
        typename U = std::enable_if_t<std::is_floating_point<Floating>::value>
    >
    T(Floating) : m_type(float_t) {} // error: cannot overload
};

Все, что я сделал, - присвоил ранее анонимному второму параметру имя - U.

Причина, по которой эта первая версия не работает, заключается в том, что нет способа выбрать между ними в случае, если вы явно задаете второй параметр.Например 1

f<int,void>(1);

На какую функцию это следует выводить?Если это целочисленная версия, она, конечно, работает, но как насчет плавающей версии.Ну, у него есть T = int, но как насчет U?Ну, мы только что дали ему тип bool, поэтому у нас есть U = bool.Таким образом, нет никакого способа выбрать между двумя в этом случае, они идентичны.(Обратите внимание, что в целочисленной версии у нас все еще есть U = bool).

Так что, если мы явно дадим имя второму параметру шаблона, вычет будет неудачным.И что?В реальном случае это не должно происходить.Мы собираемся использовать что-то вроде

f(1.f);

, где возможно сделать вычет.Хорошо, вы заметите, что компилятор выдает ошибку даже без объявления .Это означает, что он решил, что он не может сделать вывод, даже не дав ему тип вывода, поскольку обнаружил проблему, на которую я указал выше.Ну а из intro.defs мы имеем сигнатуру

«шаблон функции-члена класса» имя, список параметров-типов, класс, членом которого является функция, cv-квалификаторы (если есть), ref-квалификатор (если есть), тип возвращаемого значения (если есть) и список параметров шаблона

И из temp.over.link мы знаем, чтодва определения функций шаблона не могут иметь одну и ту же сигнатуру.

К сожалению, стандарт кажется довольно расплывчатым в том, что именно означает «список параметров шаблона».Я искал пару разных версий стандарта, и ни одна из них не дала четкого определения, которое я смог найти.Не ясно, является ли «список параметров шаблона» тем же, если параметр типа с другим значением по умолчанию является уникальным или нет.Учитывая, что я собираюсь сказать, что это на самом деле неопределенное поведение, и ошибка компилятора является приемлемым способом справиться с этим.

Вердикт еще не вынесен, если кто-то может найти явное определение в стандартедля "списка параметров шаблона" я был бы рад добавить его для более удовлетворительного ответа.

Редактировать:

Как отметил xskxkr, самый обновленный черновик на самом деле дает более конкретныйопределение.Шаблоны имеют template-head , который содержит template-parameter-list , который является серией template-parameters .Он не включает аргументы по умолчанию в определении.Таким образом, в соответствии с текущим проектом, иметь два одинаковых шаблона, но с разными аргументами по умолчанию, однозначно неправильно, но вы можете «обмануть» его, думая, что у вас есть две отдельные головки-шаблона , сделавтип второго параметра зависит от результата enable_if.


1 В качестве примечания, я не смог найти способ явно создать экземпляр шаблонаконструктор не шаблонного класса.Это странная конструкция.Я использовал f в своих примерах, так как я мог заставить его работать с бесплатной функцией.Может быть, кто-то еще может понять синтаксис?

1 голос
/ 18 апреля 2019

Это не о типе или нетипе

Дело в том: проходит ли первый шаг Двухфазный поиск .

Зачем ?Поскольку SFINAE работает на втором этапе поиска, когда шаблон называется (как сказал @cpplearner)

Итак:

Это не работает (случай 1):

 template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>
    >

И это работает так же, как и ваш случай, не связанный с типом (случай 2):

template <
        typename Integer,
        typename = std::enable_if_t<std::is_integral<Integer>::value>,  
        typename = void
    >

В первом случае компилятор видит: то же имя, тот же номераргумента шаблона и аргумент не зависят от шаблона , одинаковые arugments => это одно и то же => ОШИБКА

В случае два, а не одно и то же количество аргументов, давайте посмотрим, если этоработает позже => SFINAE => OK

В вашем ПРАВОМ случае: компилятор видит: то же имя, то же число аргументов шаблона и аргумент ARE, зависящий от шаблона (со значением по умолчанию, но онсейчас все равно) => посмотрим, когда он будет вызываться => SFINAE => OK

Кстати, как вы вызываете конструктор?

С этой записи

Нет способа явно указать шаблоны для конструктора, так как вы не можете назвать конструктор.

И вы действительно не можете:

T t =T::T<int,void>(1);

ошибка: невозможно вызвать конструктор 'T :: T' напрямую [-fpermissive]

Вы все еще можете сделатьэто работает со специализацией и SFINAE:

#include <iostream>
#include <type_traits>
using namespace std;


template <
        typename Type,
        typename = void
    >
struct T {
};

template < typename Type>
struct T<
    Type,
    std::enable_if_t<std::is_integral<Type>::value>
>  {
    float m_type;

    T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }
};

template < typename Type>
struct T<
    Type,
    std::enable_if_t<std::is_floating_point<Type>::value>
>  {
    int m_type;

    T(Type t) : m_type(t) { cout << __PRETTY_FUNCTION__ << endl; }

};

int main(){

    T<int> t(1); // T<Type, typename std::enable_if<std::is_integral<_Tp>::value, void>::type>::T(Type) [with Type = int; typename std::enable_if<std::is_integral<_Tp>::value, void>::type = void]
    cout << endl;
    T<float> t2(1.f);// T<Type, typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type>::T(Type) [with Type = float; typename std::enable_if<std::is_floating_point<_Tp>::value, void>::type = void]

    return 0;
}

Это стиль C ++ 14, в 17 может быть, вы можете придумать версию, которая компилируется только с T t(1), но я не эксперт Вычет аргумента шаблона класса

...