Почему использование enable_if дает разные результаты?

Почему две версии конструктора копирования дают разные результаты с ипользованием enable_if? В моем понимании два конструктора копирования по использованию должны давать один и тот же результат (быть неопределены только для Settings<int>)

template <typename Type>
struct Predicate : std::integral_constant<bool, true>
{
};
template <>
struct Predicate<int> : std::integral_constant<bool, false>
{
};

template <typename FooType>
struct Settings
{
    Settings() {}

    //  Here it works fine
    template <typename OtherFooType>
    Settings(const Settings<OtherFooType>& other, std::enable_if_t<Predicate<OtherFooType>::value, int*> = 0) {}

    //  In this case enable_if does not work
    //template <typename OtherFooType>
    //Settings(typename std::enable_if<Predicate<OtherFooType>::value, const Settings<OtherFooType>&>::type other){}
};

int main()
{
    Settings<float> f = Settings<char>();
    return 0;
}

Ответы (1 шт):

Автор решения: Chorkov

Упростим ситуацию:

template<typename T> struct S{};
template<typename T> struct B{ typedef S<T> type; };

template<typename T> 
void foo1(S<T> ) {};

template<typename T> 
void foo2(typename B<T>::type ) {};

foo1( S<int>{} );
foo2( S<int>{} ); // error!
foo2<int>( S<int>{} ); // ok!

По сути, B<T>::type всегда эквивалентно S<T>, но это только для человека, который обучен решать обратную задачу. В общем случае, если в реализации шаблона B<T> применены частичная специализация и т.д., то нет очевидного алгоритма для решения задачи, "какой тип T нужно подставить, чтобы B<T>::type было равно S<int> ". Помня, что шаблоны C++, образуют тюринг-полный, функциональный язык программирования, задача эквивалентна задаче "при каком наборе исходных параметров, программа выдаст заданный результат", она решаема, только в ограниченном числе частных случаев. Поэтому, авторы стандарта не заставляют авторов компилятора подбирать параметры шаблона, за исключениям случаев перечисленных в 14.8.2 Template argument deduction [temp.deduct] (в стандарте 16 страниц, пересказ: en.cppreference.com, в русской версии эта страница не переведена).

Совсем короткий пересказ: тип аргумента, является социализацией шаблонного типа, и именно этот шаблон указан в аргументе функции, то тип подбираем, (потом, проверяем наследование, приведение типа и т.д.). Если нет - то считаем что тип аргумента не подходит.

Зачем вообще сделали допустимым синтаксис void foo2(typename B<T>::type ), если компилятор никогда не пытается разобраться с аргументами B, а значить функцию невозможно вызвать? Во первых, пользователь может подсказать компилятору: foo2<int>( S<int>{} ), во вторых, тип T может быть определен, например, при подстановке других аргументов функции.

Когда вы пишете:

template <typename OtherFooType>
Settings(const Settings<OtherFooType>& other, std::enable_if_t<Predicate<OtherFooType>::value, int*> = 0) {}

То тип OtherFooType определяется при подстановке первого аргумента шаблонной функции. Поскольку мы обращаемся с аргументом Settings<char> то пытаемся сопоставить этот тип с Settings<OtherFooType>. Поскольку в обоих случаях использован шаблон Settings, от задача сводится к сопоставление char и OtherFooType, и такое сопоставление происходит. Поскольку второго аргумента конструктора нет - то используем значение по умолчанию. Но тип второго аргумента - уже определен, поскольку, он зависит только от уже определенных (при подстановке первого аргумента) аргументов шаблона. Т.е. если только удастся вывести тип std::enable_if_t<Predicate<OtherFooType>::value, int*>, то компиляция пройдет успешно.

Рассмотрим:

template <typename OtherFooType>
Settings(typename std::enable_if<Predicate<OtherFooType>::value, const Settings<OtherFooType>&>::type other){}

Поскольку зависимость финального типа, от аргументов шаблона - сложная, а не просто сравнить имя шаблона, то компилятор всегда считает, что эта специализация не подходит. (Если только вы не укажите тип явно. Но, для конструктора копирования, его явно указать нельзя.)

Что делать, если не хочется вводить фиктивных аргументов? В С++20, можно вместо enable_if указывать requires:

    template <typename OtherFooType>
    Settings(const Settings<OtherFooType>& other) 
         requires ( Predicate<OtherFooType>::value )
    {}

Для более старых версий С++ можно ввести фиктивный аргумент шаблона, вместо фиктивного аргумента функции:

template <typename OtherFooType, 
          typename UnusedCheckType = std::enable_if_t<Predicate<OtherFooType>::value, void*> >
    Settings(const Settings<OtherFooType>& other) 
{}

ИМХО: Хрен редьки не слаще.

→ Ссылка