Почему классы С++ с виртуальными функциями требуют наличия нетривиального конструктора копирования?

В соответствии со стандартом С++ класс, имеющий виртуальные функции, не может иметь тривиальный конструктор копирования:

Конструктор копирования/перемещения для класса X тривиален, если он не предоставляется пользователем и если

- класс X не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1) и

- конструктор, выбранный для копирования/перемещения каждого подобъекта прямого базового класса, тривиален и

- для каждого нестатического элемента данных X, относящегося к типу класса (или его массиву), конструктор, выбранный для копирования/перемещения этого элемента, тривиален;

в противном случае конструктор copy/move не является тривиальным.

Теперь представьте себе иерархию классов, которая удовлетворяет всем упомянутым условиям, кроме условия "нет виртуальных функций":

struct I
{
    virtual void f() = 0;
};

struct D final : I
{
   void f() override 
   {
   }
};

С точки зрения реализации эти классы содержат только указатель на таблицу виртуальной диспетчеризации. И базовый класс не имеет никаких объявленных пользователем конструкторов, а производный класс является окончательным, поэтому указанный указатель всегда может иметь постоянное значение. Учитывая все это, конструктор копирования может быть тривиальным, но стандарт явно запрещает рассматривать его как таковой.

С другой стороны, выполняются условия обработки таких классов, как тривиально разрушаемые. Класс не объявляет виртуальный деструктор и использует неявно определенные деструкторы.

Требование тривиальности деструкторов выглядит как указание на оптимизацию - неявно определенные деструкторы не должны восстанавливать указатели на виртуальную таблицу в этом случае.

Но такая оптимизация идет на полпути, на мой взгляд; классы с виртуальными функциями по-прежнему не могут быть сняты, даже если они могут быть уничтожены тривиально.

Вопросы:

  • Есть ли какие-либо причины, о которых я не думал с точки зрения реализации, почему у таких классов должны быть нетривиальные конструкторы-копии?

  • Есть ли какие-либо причины, ограничения в тривиальности для конструкторов-копий не могут быть смягчены в стандарте?

Ответ 1

Есть ли причины, по которым я не думал о перспективе реализации, почему у таких классов должны быть нетривиальные конструкторы копирования?

Существует довольно очевидное: copy-constructor I не является тривиальным. И это не окончательно, поэтому могут быть и другие производные классы. Поэтому он должен быть нетривиальным и правильно задавать указатель виртуальной таблицы после memcpy, так как на нем могут быть производные классы.

Есть ли причины, по которым ограничения на тривиальность для конструкторов-копий не могут быть смягчены в стандарте?

1) Часть тривиальности конструктора просто не пересматривалась с включением ключевого слова final.

2) Люди считают, что ключевые слова, такие как delete, final и overrride, должны помочь избежать большинства распространенных ошибок и прояснить намерение программиста, а не изменять поведение программы.

3) Это усложняет язык: Конструктор тривиален, если у вас нет виртуальной функции, тогда это нетривиально, если только ваш класс не является окончательным, тогда он снова тривиален, если только что-то еще, то это не так, если...

4) Никто, несмотря на то, что стоит написать официальный документ, доказывая полезность этого добавления в Комитет и подталкивая это изменение к языку.

Ответ 2

Копии и перемещения полиморфных классов не могут быть тривиальными, потому что они будут разбивать режущие копии и перемещения, которые копируют базовый тип динамического типа объекта. Например:

struct Base { virtual int f() { return 0; } };
struct D1 : Base { int f() override { return 1; } };
struct D2 : Base { int f() override { return 2; } };

int main() {
  D1 d1;
  D2 d2;
  Base b = d1; // if this copy constructor were trivial, b would have D1 vtable
  b.f();       // ...and this call would return 1 instead of 0.
  b = d2;      // Ditto: b would have D2 vtable
  b.f();       // ...and this call would return 2 instead of 0.
}

Ответ 3

Тривиальные конструкторы означают, что никаких дополнительных усилий не требуется. Поэтому для копирования оператора c-tor/assigment это означает простой memcpy. Виртуальные функции, как вы упомянули, создают vtable, которая представляет собой таблицу указателей на функции. Поэтому, если вы попытаетесь скопировать память объекта D, вы также скопируете ее vtable. Новый объект имел бы vtable с указателями, указывающими на старую память. Это было бы не идеальной ситуацией.