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

Из статьи 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 *** "правильной"?

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


person David Hammen    schedule 13.04.2019    source источник
comment
Чтобы быть предельно ясным по поводу бонуса: мне нужен ответ, не связанный с культом карго, предпочтительнее глава и стих, ориентированный на c ++ 11 или c ++ 14.   -  person David Hammen    schedule 13.04.2019
comment
Вы продолжаете использовать фразу карго-культ. Я не думаю, что это означает то, что вы думаете, и вы, кажется, используете его больше для бессмысленного оскорбления людей, чем для достижения какого-либо решения вашего вопроса.   -  person David Hammen    schedule 17.04.2019
comment
Я разрываюсь между этим ответом и другим ответом с самым высоким рейтингом. Здесь цитируются главы и стихи, но глава и стих взяты из будущей версии стандарта 2020 года. Поставщики компиляторов по-прежнему обращаются к сообщениям об ошибках, касающихся соответствия C ++ 11, не говоря уже о текущей фикции C ++ 20. Совсем недавно я столкнулся с ошибкой компилятора в отношении компилятора, который, как утверждается, совместим с C ++ 14. (И это была ошибка; более поздние версии clang и gcc не имели проблем.) OTOH, в этом ответе цитируются главы и стихи, а некоторые концепции (но не сами концепции) восходят к С ++ 11.   -  person Barry    schedule 23.04.2019


Ответы (6)


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

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

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

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

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

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

  • Затем с помощью [temp.over.link] / 7:

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

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

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

Перефразируя цитату cppreference, в неправильном случае мы имеем:

person xskxzr    schedule 20.04.2019
comment
Эти два разных типа немного вводят в заблуждение, поскольку они оба _1_, не так ли? - person David Hammen; 25.04.2019

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

 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

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

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

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


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

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

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

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

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

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

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

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

 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:

чтобы не надо было добавлять typename (по сравнению со старым enable_if)

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

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

person Picaud Vincent    schedule 13.04.2019
comment
да, но std :: is_integral ‹...› и is_floating_point ‹...› имеют значение - person Rakete1111; 13.04.2019
comment
Безусловно, в обоих случаях является параметром по умолчанию; один является типом по умолчанию, а другой не является типом по умолчанию. Какая разница? Я бы предпочел объяснение по главам и стихам. - person Picaud Vincent; 13.04.2019
comment
@DavidHammen Я добавил в пост обновление, это помогает? - person David Hammen; 13.04.2019
comment
@PicaudVincent - Не совсем. Где в стандарте говорится, что недопустимый параметр typename по умолчанию может быть проигнорирован (и не приводит к сбою подстановки), но что недопустимый параметр по умолчанию, не являющийся типом, действительно приводит к сбою подстановки (который нельзя игнорировать). - person Picaud Vincent; 13.04.2019
comment
@DavidHammen Я не могу дать точную ссылку на стандарт по этому поводу, извините. Более 1000 страниц читать довольно сложно. Может, здесь кто-то еще может помочь. - person David Hammen; 13.04.2019
comment
@PicaudVincent - Цитата из стандарта, [temp.deduct] Когда указывается специализация шаблона функции, все аргументы шаблона должны иметь значения. Выведение параметров шаблона должно завершиться неудачей в обоих случаях, и все же в коде _1_ оно, по-видимому, преуспевает, хотя код _2_ так же неверен (что хорошо с точки зрения SFINAE), как и код _3_. - person Picaud Vincent; 13.04.2019
comment
@DavidHammen, спасибо за отзыв, но, честно говоря, ты меня потерял. - person David Hammen; 13.04.2019
comment
@PicaudVincent - цитируется дальше от стандарта. Если аргумент шаблона не был выведен, а его соответствующий параметр шаблона имеет аргумент по умолчанию, аргумент шаблона определяется путем подстановки аргументов шаблона, определенных для предшествующих параметров шаблона, в аргумент по умолчанию. Если замена приводит к недопустимому типу, как описано выше, выведение типа не удастся. Я добавлю это к своему вопросу, потому что это меня беспокоит. Ошибка замены должна привести к коду _1_. Но почему-то это не так. - person Picaud Vincent; 13.04.2019
comment
@DavidHammen Нет ничего общего с SFINAE: SFINAE происходит, когда шаблон функции вызывается (или используется каким-либо другим способом). Но простого определения шаблонов функций с одинаковой сигнатурой достаточно, чтобы вызвать ошибку компиляции. - person David Hammen; 13.04.2019
comment
А чем первая версия неправильная? Единственное различие между _1_ и _2_ - это параметр типа по умолчанию, который оценивается как мусор (но каким-то образом проходит), и параметр по умолчанию, не являющийся типом, который оценивается как мусор и не проходит. Это ответ культа карго, извините за оскорбление. - person cpplearner; 13.04.2019

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

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

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

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

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

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

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

person cpplearner    schedule 13.04.2019
comment
@DavidHammen Похоже, вы сделали комментарий, не прочитав мой ответ, поскольку вы повторяете то, что сказали, без какой-либо ссылки на ответ. Если вы считаете, что ответ неверен, просьба уточнить его. - person David Hammen; 13.04.2019
comment
@xskxzr Это потому, что цитата cpplearners из стандарта применяется только к объявлениям, а не к определениям. Если вы удалите определение функции, оно в основном работает (за исключением того, что оно не позволит вам повторно объявить конструктор - хотя оно работает для бесплатных функций). Проблема на самом деле в том, что списки параметров шаблона одинаковы (см. Мой ответ), это произойдет в примере, который дал ccplearner, если вы удалите аргумент по умолчанию из одной функции и дадите им определения - person xskxzr; 20.04.2019
comment
@rtpax Если мы удалим определения, все равно будет ошибка. Я хочу сказать, что область действия класса отличается от области пространства имен, и повторное объявление в области класса не допускается. - person rtpax; 23.04.2019
comment
@xskxzr Эта проблема только, потому что вы не можете повторно объявить конструктор. Это работает gcc.godbolt.org/z/GyhU0o. Это не работает gcc.godbolt.org/z/DViyS0 - person xskxzr; 23.04.2019
comment
@xskxzr Перечитав ваш комментарий, я вижу, что вы уже поняли то, что я сделал в своем последнем комментарии, и мы согласны - person rtpax; 23.04.2019
comment
Re Также обратите внимание, что нет необходимости использовать typename до std :: enable_if_t ‹std :: is_floating_point ‹Floating› :: value, int› в правом объявлении шаблона, потому что там нет имен зависимых типов. Сделай так. Я сделал это с несколькими компиляторами. Все они терпят неудачу и предлагают добавить _1_. Квалификатор _2_ необходим, потому что _3_ - имя зависимого типа. - person rtpax; 23.04.2019

Вау, они такие же! Итак, T(Floating) на самом деле является переопределением версии T(Integer) While Right, объявляющей два шаблона с разными параметрами:

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

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

Также обратите внимание, что нет необходимости использовать typename перед std::enable_if_t<std::is_floating_point<Floating>::value, int> в объявлении шаблона «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)

Дело не в типе или отсутствии типа

person user7860670    schedule 13.04.2019
comment
@DavidHammen Здесь нет typename. Обратите внимание, что имя зависимого типа typename, которого просто нет, когда используется std::enable_if<std::is_whatever<FirstParameter>::value>::type - person David Hammen; 13.04.2019
comment
Ты прав; Я использовал транскрипцию кода cppreference на С ++ 11, где требуется std::enable_if<std::is_whatever<FirstParameter>::value>::type. ::type * не требуется, если используется std::enable_if_t. То, что связанный cppreference.com действительно использует _4_, еще больше заставляет меня думать, что даже cppreference является предметом культового программирования. Они использовали его, потому что это сделал кто-то другой. - person user7860670; 13.04.2019
comment
Я бы предпочел главы и стихи, объясняющие, почему недопустимый параметр typename по умолчанию игнорируется, в то время как недопустимый параметр по умолчанию, не являющийся типом, не игнорируется. Оба ответа на сегодняшний день являются ответами культа карго, насколько я понимаю, как и cppreference. - person David Hammen; 13.04.2019
comment
@DavidHammen Оба параметра по умолчанию полностью игнорируются компилятором, когда он пытается различить эти две функции шаблона. Не имеет значения, действительны ли значения параметров по умолчанию. - person David Hammen; 13.04.2019
comment
Процитируйте, пожалуйста, главу и стих. Если то, что вы написали, правда, SFINAE имеет очень небольшую ценность, если вообще имеет ее значение. Он имеет большую ценность, но за ним стоит много программ культа карго. Я хочу выйти за рамки этого культового программирования. - person user7860670; 13.04.2019
comment
@DavidHammen Я не уверен, почему вы так сосредоточились на SFINAE, это не имеет ничего общего с проблемой неправильного варианта кода. На самом деле, не имеет значения, являются ли параметры шаблона параметрами шаблона типа или параметрами шаблона нетипа. Проблема в том, что компилятор не может различать эти функции шаблона, потому что их сигнатуры эквивалентны. Поэтому, если кто-то хочет использовать обычную перегрузку или SFINAE, он сначала должен убедиться, что сигнатуры функций шаблона разные. - person David Hammen; 14.04.2019
comment
@DavidHammen Также обратите внимание, что подход с параметром шаблона без типа, используемым в варианте Right, не является обязательным, это всего лишь одна из альтернатив. - person user7860670; 14.04.2019
comment
Ваша версия без _1_ на самом деле не работает. Он просто не завершит компиляцию, пока вы не попытаетесь создать экземпляр _2_. Не то чтобы это помогло бы понять, если бы оно действительно работало, поскольку оно очень похоже на версию _3_ после удаления _4_ - person user7860670; 14.04.2019

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

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

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

So :

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

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

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

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

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

В вашем ПРАВИЛЬНОМ случае: компилятор видит: то же имя, такое же количество аргументов шаблона и аргумент зависит от шаблона (со значением по умолчанию, но сейчас ему все равно) => давайте посмотрим, когда это звонок => СФИНАЭ => ОК

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

Из этого сообщения

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

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

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

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

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

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

#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;
}

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

person Martin Morterol    schedule 18.04.2019
comment
Ааа, и я только что понял, почему это не работает, когда я пытаюсь создать его экземпляр, потому что у вас нет параметра по умолчанию для второго аргумента шаблона (который вы не можете указать, так как тип по умолчанию enable_if недействителен), он должен быть добавленным явно. Смотри мой ответ - person rtpax; 19.04.2019
comment
@rtpax thx, чтобы указать на это, я знаю, что это не сработает, мой ответ был недостаточно ясным, я перефразирую его - person rtpax; 19.04.2019
comment
Формулировка изменена в текущем проекте, по-видимому, объединение Concepts TS. - person Martin Morterol; 19.04.2019

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

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

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

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

f<int,void>(1);

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

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

f(1.f);

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

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

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

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

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

Я добавил предпоследний абзац и тег языкового юриста примерно через 45 минут после того, как задал вопрос. Мне не нужен ответ культа карго. Мне нужен ответ по главе и стиху, если это вообще возможно.

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


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

person rtpax    schedule 18.04.2019
comment

- person xskxzr; 20.04.2019