Как компилятор может вывести этот шаблон класса с помощью ссылки на пересылку?

Я рассматриваю вывод шаблона шаблона, доступный с С++ 17. Вот код, который я хотел бы задать:

#include <iostream>
#include <cmath>
using std::endl;
using std::cout;

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u) : t(std::forward<T>(u))
     { cout << "template" << endl;}

#ifdef ON
    MyAbs(const T& t) : t(t) {}
#endif

    T operator()() const
    {
        return std::abs(t);
    }
    T t;
};

/*
  // may need the following
template<typename U>
MyAbs(U&&) -> MyAbs<typename std::remove_reference<U>::type>;
*/

int main()
{
    const double d = 3.14;
    cout << MyAbs(4.7)() << endl;
    cout << MyAbs(d)() << endl;    
    return 0;
}

Когда MyAbs(const T&) не -DON условно (т.е. нет -DON), оба clang++ и g++ не могут вывести параметр шаблона T Учитывая -DON=1, оба компилятора строят простой пример выше.

Во-первых, я также догадался, что вычет должен потерпеть неудачу; компилятор может вывести U но не T Ошибки компиляции, которые я получил, были тем, что я ожидал. Пожалуйста, дайте мне знать, если я ошибаюсь.

Если бы я был прав на него, то я не могу понять, почему вывод с U&& преуспевает, когда добавляется MyAbs(const T&). То, что я ожидал, было выведено с ошибкой U&&, и SFINAE позволяет мне вызывать MyAbs(const T&) вместо этого для обоих случаев: 4.7 и d. Однако то, что произошло, отличается. Кажется, что эта программа вызывает версию шаблона для версии 4.7 и без шаблона для d.

$ g++ -Wall -std=c++17 ~/a.cc -DON=1
$ ./a.out 
template
4.7
3.14

Кажется, что версия шаблона внезапно стала жизнеспособной. Ожидается ли это? Если да, то какая причина?

Ответ 1

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

  1. Выведите аргументы шаблона класса.
  2. Затем выполните фактическую конструкцию с конкретным классом типа.

Шаг 1 поможет вам определить аргументы шаблона класса. Это не делает ничего относительно фактического конструктора (или специализации шаблона класса), который может быть использован на шаге 2.

Существование конструкторов может привести к вычету, но если данный конструктор используется для вывода, который ничего не говорит о том, используется ли он для его построения.


Итак, когда у вас только есть:

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u);
};

Недопустимый вывод аргумента шаблона шаблона: у вас нет указателя на вывод в вашем конструкторе, T - невыводимый контекст. Компилятор просто не могу понять, что T вы хотите в MyAbs(4.7) или MyAbs(d).

Когда вы добавили это:

template<typename T>
struct MyAbs {
     template<typename U>
     MyAbs(U&& u);

     MyAbs(T const&);
};

Теперь он может! В обоих случаях он выводит T как double. И как только это произойдет, мы продолжим и выполняем перегрузочное разрешение, как если бы мы написали MyAbs<double> для начала.

И здесь MyAbs<double>(4.7) предпочитает конструктор ссылок пересылки (меньше ссылочной ссылки cv), в то время как MyAbs<double>(d) предпочитает другой (не шаблон, предпочтительный для шаблона). И это нормально и ожидалось, просто потому, что мы использовали один конструктор для вычитания, не означает, что мы должны использовать именно этот конструктор для построения.