Рассмотрим следующий сценарий:

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?

Комментарии (9)


  1. lczero
    20.06.2025 13:43

    (⁠・⁠o⁠・⁠;⁠)


  1. martein
    20.06.2025 13:43

    > Его экземпляр создать невозможно, но компилятор это не волнует. Он действует на основании того, что ему сказали мы
    --------------------------------
    На основании того, что ему сказал Стандарт, а в комитете не только лишь Вы.


  1. BareDreamer
    20.06.2025 13:43

    Почему такой код не считается ошибкой?

    Derived(Derived const& d) : Base<T>(d) {}

    Зачем C++ позволяет написать в шаблоне код, который заведомо не может скомпилироваться при использовании шаблона?


    1. BareDreamer
      20.06.2025 13:43

      Сам догадался. Потому что может быть специализация шаблона Base или Derived где конструктор определён иначе:

      template<>
      struct Base<int>
      {
          Base() = default;
          Base(Base const &) = default;
      };


    1. KivApple
      20.06.2025 13:43

      Потому что SFINAE.

      Все такие ошибки (несовместимость типов, отчутсвующие методы и т п) происходят в момент использования шаблона, причём именно того самого метода, где происходит ошибка, а не в момент описания.

      Компилятор пытается создать конструктор копированиятолько в момент его вызова. А если конструктор не вызывался бы нигде в коде, то ошибки компиляции не было бы.

      Обычно это нужно для гибкости, чтобы можно было делать опциональные фичи в шаблоне требующие определённый тип и пропадающие с другими.

      Здесь накладывается ещё возможность частичной специализации. В общем случае конструктора копирования может не быть (он компилируется в некорректный код), но есть частная специализация для int где конструктор реализован иначе и валиден.


  1. vityo
    20.06.2025 13:43

    Иногда думаю, что наследование это лажа какая-то. И больше 3х в глубину поддерживать - целая заморочка, и фиг перепишешь при куче интерфейсов, а вот эти хитро сделанные моменты как в статье, это однажды станут как thy thine thee в англ, старыми, и ненужными, неудобными и знать будет мало людей. Может однажды забудут реализовать в каком-нибудь потом компиляторе. Ну потому что фигня вот это сидеть и додумывать что же там копируется или нет, это не потому, что мы тупые, а потому что язык должен быть простым и понятным во всех смыслах.

    Вот вы только представьте, что вы не филолог какой-то, который словам и предложениям пытается найти и дать определение, хорошие формулировки. Нет, представьте, что вы лопатами эти говны разбираете вагонами в сутки, хочется видеть понятный код, а не кал, который сидишь и анализируешь ещё, потому что как скажет кто-то такое, он конечно выпендрился, но ты видишь такое раз в 100 лет. Крякнешь улыбнёшься, и стираешь или дописываешь пусть и тупо, но понятно. Мне вот это разнообразие вариантов того, как можно выразиться, иногда доводит прям. Моя б воля может и ретурн бы убрал вообще, типа выход из функции только в конце. Ой, задело


  1. Panzerschrek
    20.06.2025 13:43

    Особенности кривизны C++. Инстанцирование шаблонных классов в нём не приводит к немедленному инстанцированию их методов и проверки корректности оных, методы компилируется, только если вызываются. Кода вызывается std::is_copy_constructible, класс инстанцируется и вроде даже проверяется наличие конструктора копирования в нём, но его тело на корректность не проверяется, ибо не нужно.


    1. KanuTaH
      20.06.2025 13:43

      Шаблоны тут вовсе не при чем. В C и C++ широко практикуется отделение объявления от реализации, и is_copy_constructible далеко не всегда может проверить, что именно делает copy constructor. Вот например без всяких шаблонов:

      $ cat module.h
      struct Base
      {
          Base() = default;
          Base(Base const &) = delete;
      };
      
      struct Derived: public Base
      {
          Derived() = default;
          Derived(Derived const&);
      };
      
      $ cat module.cpp
      #include "module.h"
      
      Derived::Derived(Derived const&) : Base() {}
      
      $ cat main.cpp
      #include <type_traits>
      #include "module.h"
      
      static_assert(std::is_copy_constructible_v<Derived>);
      
      int main() {}
      

      Как прикажете is_copy_constructible в main.cpp проверять, что именно делает конструктор копии, реализация которого находится в module.cpp? Что, если module.cpp существует только в виде какого-нибудь module.so?


      1. Panzerschrek
        20.06.2025 13:43

        Да, отделение объявления от реализации может дать тот же эффект. Просто для шаблонов кажется, что реализация вроде тут-же доступна, но это не так, ибо по факту она инстанцируется только по необходимости.