Подтвердить что ты не робот

Почему использование виртуального базового класса изменяет поведение конструктора копирования

В следующей программе переменная-член a не копируется, когда B фактически получен из A и копируются экземпляры C (не B).

#include <stdio.h>

class A {
public:
    A() { a = 0; printf("A()\n"); }

    int a;
};

class B : virtual public A {
};

class C : public B {
public:
    C() {}
    C(const C &from) : B(from) {}
};

template<typename T>
void
test() {
    T t1;
    t1.a = 3;
    printf("pre-copy\n");
    T t2(t1);
    printf("post-copy\n");
    printf("t1.a=%d\n", t1.a);
    printf("t2.a=%d\n", t2.a);
}

int
main() {
    printf("B:\n");
    test<B>();

    printf("\n");

    printf("C:\n");
    test<C>();
}

выход:

B:
A()
pre-copy
post-copy
t1.a=3
t2.a=3

C:
A()
pre-copy
A()
post-copy
t1.a=3
t2.a=0

Обратите внимание, что если B обычно получается из A (вы удаляете virtual), то a копируется.

Почему в первом случае не копируется a (test<C>() с B, фактически полученным из A?

4b9b3361

Ответ 1

Стандарт С++ 11 говорит в 12.6.2/10:

В конструкторе без делегирования инициализация продолжается в в следующем порядке:
- Во-первых, и только для конструктора самого производного класса (1.8) виртуальные базовые классы инициализируются в порядке они появляются на первом переходе слева направо направо ациклический граф базовых классов, где "слева направо" - это порядок появление базовых классов в производном классе базовый спецификатор-лист.
- [прямые базовые классы и т.д....]

Это говорит об этом, в основном - самый производный класс отвечает за инициализацию каким бы способом он ни определялся (в OP: это не так, что приводит к инициализации по умолчанию). Следующий пример в стандарте содержит аналогичный сценарий, как в OP здесь, только с аргументом int для ctor; вызывается только по умолчанию ctor виртуальной базы, поскольку явный "mem-initializer" для виртуальной базы не предоставляется в самом производном классе.

Интерес, хотя и не прямо применяемый здесь, также составляет 12.6.2/7:

Память-инициализатор [the A() в возможном B(): A() {}. -pas], где идентификатор mem-initializer обозначает виртуальную базу класс игнорируется во время выполнения конструктора любого класса, который не является самым производным классом.

(Я нахожу это довольно жестким. Язык в основном говорит: "Мне все равно, что вы закодировали, я проигнорирую его". Там не так много мест, где он может это сделать, нарушая как-если.) Этот конструктор не самого производного класса будет B(). Предложение здесь не применяется непосредственно, потому что в B нет явного конструктора, поэтому нет mem-инициализатора. Но хотя я не мог найти формулировки для этого в стандарте, нужно предположить (и это согласуется), что то же правило применяется для сгенерированного конструктора копирования.

Для полноты, Stroustrup говорит в "языке программирования С++" (4.ed, 21.2.5.1) о самом производном классе D с виртуальной базой V по дороге где-то:

Тот факт, что V явно не упоминается как основа D, не имеет значения. Знание виртуальной базы и обязательство инициализировать ее "пузырится" до самого производного класса. Виртуальная база всегда считается прямой базой своего самого производного класса.

Именно это сказал Сэм Варшавчик в более раннем посте.

Далее, Stroustrup обсуждает, что получение класса DD из D делает необходимым переместить V-инициализацию в DD, что "может быть неприятным. Это должно побуждать нас не злоупотреблять виртуальными базовыми классами".

Я нахожу довольно неясным и опасным, что базовый класс остается неинициализированным (ну, точнее: инициализированным по умолчанию), если только сам производный класс явно что-то делает.

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

Я также не уверен, что согласен с обоснованием, данным в других сообщениях ( "какой из промежуточных классов должен выполнять инициализацию?" ). Стандарт имеет четкое представление о порядке инициализации ( "пересечение глубины влево-вправо" ). Не мог ли он указать, что первый класс, который фактически наследует базу, выполняет инициализацию?

Интересный факт, что копия ctor по умолчанию инициализирует виртуальную базу, предписывается в 12.8/15:

Каждый базовый или нестатический элемент данных копируется/перемещается способом соответствующий его типу:
[...]
- в противном случае основание или член с прямой инициализацией с соответствующей базой или членом x.

Виртуальный субобъекты базового класса должны быть инициализированы только один раз неявно заданный конструктор копирования/перемещения (см. 12.6.2).

В любом случае, поскольку C является самым производным классом, он отвечает за C (а не B) ответственность за копирование-построение виртуальной базы A.

Ответ 2

Виртуальное наследование - забавный зверь, в котором построение копии не "унаследовано" так же, как это было бы нормально. Ваша база A создается по умолчанию, потому что вы явно не копируете ее:

class C : public B {
public:
    C() {}
    C(const C &from) : A(from), B(from) {}
};

Ответ 3

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

Другими словами, иерархия классов в примере заканчивается тем, что говорит:

class A {
};

class B {
};

class C : public B, public A {
};

То, что, с некоторой абстрактной точки зрения, происходит здесь. "Самый производный" или "класс верхнего уровня" становится прямым "родителем" всех виртуальных классов в своей иерархии.

Следовательно, вы определяете конструктор C-копии, который копирует-конструирует B, однако, поскольку A больше не является подклассом B, ничего не копирует-строит A, следовательно, поведение, которое вы видите.

Обратите внимание, что все, что я только что сказал, применимо только к классу C. Класс B, сам по себе, получен из A, как вы ожидали. Это просто, когда вы объявляете дополнительные подклассы класса с виртуальными суперклассами, все виртуальные суперклассы "плавают" к вновь определенному подклассу.

Ответ 4

Рассмотрим наследование бриллиантов, где вы передаете объект C для копирования с B1 и B2 ctors:

class A { public: int a };

class B1: virtual public A {};
class B2: virtual public A {};

class C: public B1, public B2 {
public:
    C(const C &from): B1(from), B2(from) {}
};

(см. http://coliru.stacked-crooked.com/a/b81fad6cf00c664a).

Какой из них должен инициализировать член a? Первый, последний, оба (в каком порядке)? Что делать, если команды B1 и B2 инициализируют a разными способами?

Вот почему вам нужно явно вызвать конструктор a, иначе члены класса a будут построены по умолчанию.

Я действительно нашел забавным то, что по умолчанию C cctor компилятор успевает скопировать член a, но это еще один вопрос.