Почему кандидат на вычеты необходимо в качестве отдельного руководства по вычету?

template <typename T> struct A {
    A(T);
    A(const A&);
};

int main()
{
    A x(42); // #1
    A y = x; // #2
}

Насколько я понимаю, T для # 1 будет выводиться с использованием руководства по неявному выводу, сгенерированного с первого ctor. Тогда x будет инициализирован с использованием этого ctor.

Для # 2, однако, T будет выведено с использованием кандидата на вывод копии (который, как я понимаю, является конкретным случаем руководства по вычитанию) (а затем y будет инициализирован с использованием второго ctor).

Почему не удалось вывести T для # 2 с помощью (неявного) руководства по вычитанию, созданного с помощью copy-ctor?

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

Ответ 1

Первоначальный проект для добавления формулировки для вычитания копии был P0620R0, в котором упоминается

Настоящий документ предназначен для решения

  • Направление на обертку и копирование с EWG в понедельник в Kona

Некоторые замечания по этой встрече доступны на https://botondballo.wordpress.com/2017/03/27/trip-report-c-standards-meeting-in-kona-february-2017/:

Копирование против поведения обертывания. Предположим, что a является переменной типа tuple<int, int>, и мы пишем tuple b{a}; , Если тип b является tuple<int, int> (поведение "копирование") или tuple<tuple<int, int>> (поведение "wrapping")? Этот вопрос возникает для любого типа, подобного оболочке (например, pair, tuple или optional), который имеет как конструктор копирования, так и конструктор, который берет объект обернутого типа. EWG считает, что копирование было лучшим дефолтом. Были некоторые разговоры о том, что поведение зависит от синтаксиса инициализации (например, синтаксис { } всегда должен быть обернут), но EWG чувствовал, что введение новых несоответствий между поведением различных синтаксисов инициализации принесет больше вреда, чем пользы.

@kiloalphaindia объяснил это в комментарии:

Если # 2 будет использовать A::A(T) мы закончим с y beeing A<A<int>> <A <int A<A<int>>. [...]

Это правильно. Конструктор A<A<int>>::A(A<int>) имеет точное совпадение в типе параметра. С другой стороны, вы также правы, что A<int>::A(const A<int> &) в этом случае были бы предпочтительными.

Но рассмотрим эту альтернативу, где эквивалент функции показывает, что A<A<int>> было бы предпочтительным, если бы не кандидат на копирование:

template <typename T>
struct A {
    A(T &&);
    A(const A<T> &);
};

template <typename T> auto f(T &&) -> A<T>;
template <typename T> auto f(const A<T> &) -> A<T>;

int main() {
  A x1(42);                   // A<int>
  A y1 = std::move(x1);       // A<int>

  auto x2 = f(42);            // A<int>
  auto y2 = f(std::move(x2)); // A<A<int>>
}

Ответ 2

Основная проблема заключается в том, что в общем случае вы не знаете, если что-то является конструктором copy/move, пока вы не узнаете аргументы шаблона и не создадите специализацию, но для CTAD вы не знаете аргументы шаблона (duh) и должны идти только декларациями:

template<bool B, class T>
struct A {
   A(std::conditional_t<B, A, int>&); // copy ctor? maybe
   A(std::conditional_t<!B, A&&, char>); // move ctor?

   T t;
   // A(const A&); // implicitly declared? or not? 
                   // is this even the right signature? (depends on T)
   // A(A&&); // implicitly declared? or not?
};
A a = A<true, int>(); // deduce?

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