Рассмотрим следующий сценарий:
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)
martein
20.06.2025 13:43> Его экземпляр создать невозможно, но компилятор это не волнует. Он действует на основании того, что ему сказали мы
--------------------------------
На основании того, что ему сказал Стандарт, а в комитете не только лишь Вы.
BareDreamer
20.06.2025 13:43Почему такой код не считается ошибкой?
Derived(Derived const& d) : Base<T>(d) {}
Зачем C++ позволяет написать в шаблоне код, который заведомо не может скомпилироваться при использовании шаблона?
BareDreamer
20.06.2025 13:43Сам догадался. Потому что может быть специализация шаблона
Base
илиDerived
где конструктор определён иначе:template<> struct Base<int> { Base() = default; Base(Base const &) = default; };
KivApple
20.06.2025 13:43Потому что SFINAE.
Все такие ошибки (несовместимость типов, отчутсвующие методы и т п) происходят в момент использования шаблона, причём именно того самого метода, где происходит ошибка, а не в момент описания.
Компилятор пытается создать конструктор копированиятолько в момент его вызова. А если конструктор не вызывался бы нигде в коде, то ошибки компиляции не было бы.
Обычно это нужно для гибкости, чтобы можно было делать опциональные фичи в шаблоне требующие определённый тип и пропадающие с другими.
Здесь накладывается ещё возможность частичной специализации. В общем случае конструктора копирования может не быть (он компилируется в некорректный код), но есть частная специализация для int где конструктор реализован иначе и валиден.
vityo
20.06.2025 13:43Иногда думаю, что наследование это лажа какая-то. И больше 3х в глубину поддерживать - целая заморочка, и фиг перепишешь при куче интерфейсов, а вот эти хитро сделанные моменты как в статье, это однажды станут как thy thine thee в англ, старыми, и ненужными, неудобными и знать будет мало людей. Может однажды забудут реализовать в каком-нибудь потом компиляторе. Ну потому что фигня вот это сидеть и додумывать что же там копируется или нет, это не потому, что мы тупые, а потому что язык должен быть простым и понятным во всех смыслах.
Вот вы только представьте, что вы не филолог какой-то, который словам и предложениям пытается найти и дать определение, хорошие формулировки. Нет, представьте, что вы лопатами эти говны разбираете вагонами в сутки, хочется видеть понятный код, а не кал, который сидишь и анализируешь ещё, потому что как скажет кто-то такое, он конечно выпендрился, но ты видишь такое раз в 100 лет. Крякнешь улыбнёшься, и стираешь или дописываешь пусть и тупо, но понятно. Мне вот это разнообразие вариантов того, как можно выразиться, иногда доводит прям. Моя б воля может и ретурн бы убрал вообще, типа выход из функции только в конце. Ой, задело
Panzerschrek
20.06.2025 13:43Особенности кривизны C++. Инстанцирование шаблонных классов в нём не приводит к немедленному инстанцированию их методов и проверки корректности оных, методы компилируется, только если вызываются. Кода вызывается
std::is_copy_constructible
, класс инстанцируется и вроде даже проверяется наличие конструктора копирования в нём, но его тело на корректность не проверяется, ибо не нужно.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
?Panzerschrek
20.06.2025 13:43Да, отделение объявления от реализации может дать тот же эффект. Просто для шаблонов кажется, что реализация вроде тут-же доступна, но это не так, ибо по факту она инстанцируется только по необходимости.
lczero
(・o・;)