Рассмотрим следующий сценарий:
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?
lczero
(・o・;)