Рассмотрим следующий сценарий:
template<typename T> struct Base { // Есть конструктор по умолчанию Base() = default; // Не копируемый Base(Base const &) = delete; }; template<typename T> struct Derived : Base<T> { Derived() = default; Derived(Derived const& d) : Base<T>(d) {} }; // Это assertion выполняется? static_assert( std::is_copy_constructible_v<Derived<int>>);
Почему выполняется это assertion? Очевидно, что скопировать Derived<int> нельзя, ведь при этом мы попытаемся скопировать некопируемый Base<int>. И в самом деле, если попробовать скопировать его, то мы получим ошибку:
void example(Derived<int>& d) { Derived<int> d2(d); // msvc: error C2280: 'Base<T>::Base(const Base<T> &)': // attempting to reference a deleted function // gcc: error: use of deleted function 'Base<T>::Base(const Base<T>&) // [with T = int]' // clang: error: call to deleted constructor of 'Base<int>' }
Итак, компилятор считает, что Derived<int> копируемый, но когда мы пытаемся его скопировать, выясняется, что это не так!
Причина заключается в том, что компилятор определяет копируемость, проверяя, имеет ли класс неудалённый конструктор копирования. А в случае Derived<T> он имеет неудалённый конструктор копирования. Мы объявили его сами!
Derived(Derived const& d) : Base<T>(d) {}
Так что да, конструктор копирования существует. Его экземпляр создать невозможно, но компилятор это не волнует. Он действует на основании того, что ему сказали мы, а мы сказали, что его можно скопировать.
Ещё одним возможным конструктором копирования мог бы быть такой:
Derived(Derived const& d) : Base<T>() {}
и его экземпляр успешно создаётся. Копирование Derived создаёт по умолчанию базовый класс Base, а не создаёт его копированием.
Представим, что мы перенесли определение:
template<typename T> struct Derived : Base<T> { Derived() = default; Derived(Derived const& d); };
Что должно отвечать на вопрос «Есть ли конструктор копирования?». Мы не знаем определение, нам известно только его объявление. Должен ли компилятор прекратить компиляцию с сообщением об ошибке «Не могу предсказать будущее»? Но что, если мы не хотим раскрывать реализацию конструктора копирования в файле заголовка?
Правило определения конструирования копированием заключается в проверке наличия неудалённого конструктора копирования. В случае Derived он присутствует. Пусть создать его экземпляр невозможно, но is_copy_constructible проверяет не это1.
1 Требование того, что тип будет полным и все его члены будут определены — это неразумное требование, ведь для этого потребуется наличие в файлах заголовков определений всех методов классов. Вся программа редуцируется до проекта из одних заголовков.
Некопируемость по умолчанию наследуется, поэтому мы могли просто разрешить использовать по умолчанию конструктор копирования:
template<typename T> struct Derived : Base<T> { Derived() = default; Derived(Derived const& d) = default; };
Косвенно определённый или явный конструктор копирования по умолчанию определяется как удалённый, если любой базовый класс не конструируется копированием; в этом случае объявление обрабатывается так, как будто оно гласило = delete. Это = delete можно обнаружить при помощи is_copy_constructible , в результате чего assertion не выполняется.
Но если создать собственный неудаляемый конструктор копирования, то компилятор предположит, что вы не нарушите своё обещание.
См. также: Why does std::is_copy_constructible report that a vector of move-only objects is copy constructible?
