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

#include <iostream>

struct uct
{
    uct() { std::cerr << "default" << std::endl; }

    uct(const uct &) { std::cerr << "copy" << std::endl; }
    uct(      uct&&) { std::cerr << "move" << std::endl; }

    uct(const int  &) { std::cerr << "int" << std::endl; }
    uct(      int &&) { std::cerr << "int" << std::endl; }

    template <typename T>
    uct(T &&) { std::cerr << "template" << std::endl; }
};

int main()
{
    uct u1    ; // default
    uct u2( 5); // int
    uct u3(u1); // template, why?
}

coliru

Шаблонная перегрузка конструктора подходит для обоих объявлений (u2 и u3). Но когда int передается в конструктор, выбирается не шаблонная перегрузка. Когда вызывается конструктор копирования, выбирается перегрузка шаблона. Насколько я знаю, не шаблонная функция всегда предпочтительнее шаблонной функции во время разрешения перегрузки. Почему конструктор копирования обрабатывается по-другому?

Ответы

Ответ 1

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

Это верно, только когда специализация и шаблон не совпадают. Это не тот случай, здесь. Когда вы вызываете uct u3(u1), наборы перегрузки получают

uct(const uct &)
uct(uct &) // from the template

Теперь, поскольку u1 не является const, ему придется применить преобразование const для вызова конструктора копирования. Чтобы вызвать специализацию шаблона, ему ничего не нужно делать, так как это точное совпадение. Это означает, что шаблон выигрывает, поскольку он лучше соответствует.

Чтобы остановить это, вы можете использовать SFINAE, чтобы ограничить функцию шаблона, вызываемую только тогда, когда T не является uct. Это будет выглядеть как

template <typename T, std::enable_if_t<!std::is_same_v<uct, std::decay_t<T>>, bool> = true>
uct(T &&) { std::cerr << "template" << std::endl; }

Ответ 2

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

template <typename T>
uct(T &&) { std::cerr << "template" << std::endl; }
//    ^^

Причина выбора шаблонной версии заключается в том, что компилятор может
генерировать конструктор с подписью (T &), который подходит лучше и поэтому выбран.

  • Если вы изменили подпись с uct u1 на const uct u1, тогда она подойдет для конструктора копирования (так как u1 не является константой для начала).

  • Если вы измените подпись с uct(const uct &) на uct(uct&), она будет лучше подходить и выберет ее вместо шаблонной версии.

  • Кроме того, uct(uct&&) будет выбран, если вы использовали uct u3(std::move(u1));


Чтобы исправить это, вы можете использовать SFINAE для отключения перегрузки, когда T совпадает с uct:

template <typename T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, uct>>>
uct(T&&)
{
  std::cerr << "template" << std::endl;
}

Ответ 3

Проблема состоит в том, что конструктор шаблона не имеет квалификации const, в то время как конструктор копирования без шаблона имеет квалификатор const в своем параметре. если вы объявите объект u1 как объект const, будет вызван не шаблонный конструктор копирования.

Из C++ STandard (7 стандартных преобразований)

1 Стандартные преобразования - это неявные преобразования со встроенным значением. Раздел 7 перечисляет полный набор таких преобразований. Стандарт последовательность преобразования представляет собой последовательность стандартных преобразований в следующий порядок:

(1.4) — Zero or one qualification conversion

Поэтому конструктору копирования требуется одно стандартное преобразование, в то время как конструктору шаблона не требуется такое преобразование.