Гуру C++. Нужна ваша помощь с этой маленькой головкой:
#include <iostream>
struct B{
virtual ~B() = default;
virtual void talk() { std::cout << "Be-e-e\n"; }
};
struct D:B{
void talk() override { std::cout << "Duh\n"; }
~D() { std::cout << "~D()\n"; }
};
int main(){
B b{}; // vptr points to B
new (&b) D; // vptr now points to D
b.talk(); // "Be-e-e" (why? shouldn't the vptr be used?)
b = D{}; // "~D()" (why? shouldn't the copying be elided?)
b.talk(); // "Be-e-e"
B*b1{new D};
b1->talk(); // "Duh"
delete b1; // "~D()"
return 0;
}
Код довольно прост: иметь базовый объект в стеке, размещать в нем новый и производный (да, эээу, но терпи меня) и вызывать виртуальный метод, ожидая, что производный вывод будет напечатан.
Приведенный выше код дает следующий результат:
Be-e-e ~D() Be-e-e Duh ~D()
Такое поведение повсеместно наблюдается в MSVC, gcc, clang и нескольких онлайн-компиляторах, на которых я его пробовал (что является чрезвычайно убедительным свидетельством того, что это я ошибаюсь).
Новое размещение повторно добавляет объект производного типа в память базового типа. И это обновляет vptr, чтобы он указывал на vtable производного типа (непосредственно наблюдаемый в отладчике).
Главный вопрос: это ожидаемое поведение? (Я хочу сказать "да", поэтому, если это не так - объясните мне, пожалуйста)
Я хочу верить, что выполнение нового размещения (при условии, что для объекта производного типа достаточно памяти) должно на месте инициализировать совершенно новый объект производного типа.
Если я правильно понимаю, первый b.talk() должен выводить "Duh", поскольку теперь объект имеет производный тип. Почему он все еще печатает "Be-e-e"?
Назначение объекта производного типа объекту базового типа (в дополнение к сращиванию объектов) не копирует vptr, поэтому ожидается второй вывод "Be-e-e", при условии, что объект все еще относится к базовому типу, когда мы дойдем до этой строки кода.
Почему в назначении ~D() присутствует вызов b = D{};? Разве это не временное, которое следует исключить из копирования без необходимости вызова деструктора для этого временного?
Последний блок кода, в котором используются указатели, работает «как ожидалось» и находится здесь только для проверки работоспособности.
Также я думаю, что у вас есть неопределенное поведение в нескольких местах.
Думаю, это связано: stackoverflow.com/questions/15188894/…
b = D{}; также, по сути, ошибочен, поскольку производные классы могут использовать конструктор базового класса, но не наоборот ... (Здесь b - базовый класс и может Только использовать свой собственный конструктор ...
@Ruks "производные классы могут использовать конструктор базового класса, но не наоборот" Что ты имеешь в виду?





Смотрим на код:
B b{}; // vptr points to B
new (&b) D; // vptr now points to D
Это потенциальная проблема по двум причинам. Сначала вы не вызывали деструктор для базового объекта B. Во-вторых, размер B может быть слишком мал для размещения объекта типа D.
b.talk(); // "Be-e-e" (why? shouldn't the vptr be used?)
Виртуальные вызовы работают только при вызове через указатель или ссылку. Такой прямой вызов функции никогда не использует виртуальная отправка.
b = D{}; // "~D()" (why? shouldn't the copying be elided?)
Поскольку b объявлен как тип B, и вы не можете исключить копию между различными типами, такими как D и B.
"Сначала вы не вызывали деструктор для базового объекта B." Нет причин: этот конкретный деструктор не имеет функционального эффекта.
@curiousguy Деструктор может не иметь функциональный эффект по отношению к программе, но неизвестно, как он влияет на внутреннее состояние компилятора и его процесс принятия решений. Если деструктор не вызывается, это все еще неопределенное поведение.
"Это все еще неопределенное поведение" с каких это пор?
Виртуальные вызовы работают только через указатели и ссылки. Не на прямых объектах.