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

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?

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


  1. lczero
    20.06.2025 13:43

    (⁠・⁠o⁠・⁠;⁠)