1

Тема: Що робить даний уривок коду?

Вітаю.
Недавно побачив цікавий момент про приведення типів, сенс там був приблизно таким

struct Base {
    virtual void foo() = 0;
    ~Base();
};

struct Derived : Base {
    void foo() override;
    ~Derived();
};

int main() {
    Base* b = new Derived;
    // ...
    delete static_cast<Derived*>(b); // ось тут
}

І ніяк не можу зрозуміти для чого так робити?

2

Re: Що робить даний уривок коду?

Ну, по-перше, цей код не скомпілюється. Наслідування має бути публічним.

Щодо деструкторів. Базове правило: деструктор має бути віртуальним завжди, коли використовується посилання на базовий клас.
Якщо в цьому коді зробити

delete b;

то компілятор підставить виклик деструктор ~Base(), і а деструктор ~Derived() не буде викликано взагалі. Тому і робиться приведення типів. Але якби деструктори були віртуальними, то видалення delete b призвело б до віртуальної диспетчеризації виклика деструктора, і був би викликаний ~Derived().

3 Востаннє редагувалося Олександр Ковальчук (28.12.2022 21:03:07)

Re: Що робить даний уривок коду?

koala написав:

Щодо деструкторів. Базове правило: деструктор має бути віртуальним завжди, коли використовується посилання на базовий клас.
Якщо в цьому коді зробити

Навіть коли в базовому класі не було віртуальних методів?

4 Востаннє редагувалося wander (28.12.2022 21:01:05)

Re: Що робить даний уривок коду?

mimik написав:

І ніяк не можу зрозуміти для чого так робити?

Якщо коротко, то щоб уникнути UB. Тобто, щоб динамічний тип не залучати, бо це призведе до UB через відсутність віртуального деструктора. Хоча.. Я б таки глянув, що там буде реалізація робити. Адже, як ви знаєте, реалізація може генерувати декілька деструкторів, в залежності від організації вашого класу.

koala написав:

Ну, по-перше, цей код не скомпілюється. Наслідування має бути публічним.

Гась? Чому не скомпілюється? https://godbolt.org/z/TqovMKvs6 :)

Олександр Ковальчук написав:

Навіть коли в базовому класі не було віртуальних методів?

Якщо у вас немає віртуальних методів, то нащо вам віртуалізація (поліморфізм)?

Подякували: leofun01, mimik2

Re: Що робить даний уривок коду?

wander написав:

Якщо у вас немає віртуальних методів, то нащо вам віртуалізація (поліморфізм)?

А якщо , припустимо :

class Base{}
class MyClass : public Base{}

Base* ptr = new MyClass();
delete ptr;

Деструктор MyClass не спрацює....

6

Re: Що робить даний уривок коду?

Олександр Ковальчук написав:
wander написав:

Якщо у вас немає віртуальних методів, то нащо вам віртуалізація (поліморфізм)?

А якщо , припустимо :

class Base{}
class MyClass : public Base{}

Base* ptr = new MyClass();
delete ptr;

Деструктор MyClass не спрацює....

Не спрацює, але питання я поставив по-іншому. Нащо ви робите

Base* ptr = new MyClass();

Якщо Base та MyClass немає «спільного» інтерфейсу? У вас немає віртуальних методів, відповідно віртуальної таблиці у вас теж не буде. Який сенс так писати? :)

Подякували: mimik1

7

Re: Що робить даний уривок коду?

wander написав:

Чому не скомпілюється?

Бо я прогальмував, що це struct, а не class.

Подякували: leofun011

8 Востаннє редагувалося Олександр Ковальчук (28.12.2022 22:03:27)

Re: Що робить даний уривок коду?

wander написав:

Якщо Base та MyClass немає «спільного» інтерфейсу? У вас немає віртуальних методів, відповідно віртуальної таблиці у вас теж не буде. Який сенс так писати? :)

Це був просто приклад...я розумію що в такому записі сенсу не багато....
Просто .... якось викладач не нароком зовсім випустив таку "мізерну" деталь як віртуальний деструктор.
Через те допитуюсь.

9

Re: Що робить даний уривок коду?

wander написав:

Якщо коротко, то щоб уникнути UB. Тобто, щоб динамічний тип не залучати, бо це призведе до UB через відсутність віртуального деструктора. Хоча.. Я б таки глянув, що там буде реалізація робити. Адже, як ви знаєте, реалізація може генерувати декілька деструкторів, в залежності від організації вашого класу.

Я напевно спитаюсь дурницю, але про які деструктори йде мова? Це ви про типу default деструктори?

10 Востаннє редагувалося wander (30.12.2022 13:41:49)

Re: Що робить даний уривок коду?

mimik написав:
wander написав:

Якщо коротко, то щоб уникнути UB. Тобто, щоб динамічний тип не залучати, бо це призведе до UB через відсутність віртуального деструктора. Хоча.. Я б таки глянув, що там буде реалізація робити. Адже, як ви знаєте, реалізація може генерувати декілька деструкторів, в залежності від організації вашого класу.

Я напевно спитаюсь дурницю, але про які деструктори йде мова? Це ви про типу default деструктори?

Ем, ні. Я про інші деструктори говорив. Але, питання насправді хороше, про це не дуже часто пишуть в книжках. Хоча, здається, про deleting destructors є у якихось книжках Александреску або Майерса (правда яких конкретно вже не згадаю).

Кхм.. Зайдім но трохи здалека. Ви коли-небудь задумувались, як працюють new та delete? А, особливо нас цікавить останній. Бо, якщо логічно подумати, то

Base* b = new Derived;
// ...
delete static_cast<Derived*>(b); // ось тут

тут delete-expression мала б зробити дві речі, а саме:

  1. викликати деструктор об'єкта, на який вказує b;

  2. викликати operator delete на b, щоб відновити пам’ять у купі.

І, якщо ще трохи подумати, то постає питання, а власне, звідки цей operator delete береться?
Як ви знаєте, кожен користувацький тип (клас) може також перевантажувати оператори new та delete. Ці (de)allocation функції можна визначити як статичні функції-члени класу. І, якщо вони передбачені, то будуть викликані by new- чи delete-expression відповідно, в іншому випадку викличеться глобальний ::operator delete.

http://eel.is/c++draft/class.free#1-3 написав:

Any allocation function for a class T is a static member (even if not explicitly declared static).
Any deallocation function for a class T is a static member (even if not explicitly declared static).

Якщо додамо функцію деалокації у ваш приклад

struct Base {
    virtual void foo() = 0;

    virtual ~Base() {
        std::cout << "Base::~Base()" << std::endl;
    }
};

struct Derived : Base {
    void foo() override {
        std::cout << "Derived::foo()" << std::endl;
    }

    ~Derived() {
        std::cout << "Derived::~Derived()" << std::endl;
    }

    void operator delete(void* p) {
        std::cout << "Derived::operator delete(void*)" << std::endl;
        ::operator delete(p);
    }
};

int main() {
    Base* b = new Derived;
    delete b;
}

, то отримаємо вивід

Derived::~Derived()
Base::~Base()
Derived::operator delete(void*)

Перша частина досить зрозуміла: "статичний" тип bBase, але компілятор знає, що Base має віртуальний деструктор. Таким чином, він шукає правильний деструктор для виклику у віртуальній таблиці, що зберігається в об’єкті, на який вказує b. Оскільки динамічним типом b є Derived, знайдений там деструктор буде Derived::~Derived(), що правильно.

А як щодо оператора delete? Оператор видалення теж віртуальний? Чи зберігається він у віртуальній таблиці? Тому що, якщо це не так, як компілятор дізнається, який оператор видалення викликати?

Ні, оператор delete не віртуальний. Він не зберігається у віртуальній таблиці. Насправді оператор delete є статичним членом. Стандарт C++ чітко говорить про це:

http://eel.is/c++draft/class.free#4 написав:

Since member allocation and deallocation functions are static they cannot be virtual.

При цьому специфікація мови вимагає, щоб вибір конкретного T::operator delete робився так, ніби його пошук (name lookup) виконується з деструктора об'єкта, що видаляється.
--------------------------------
Отже, як це працює, якщо оператор delete не є віртуальним? Відповідь криється в спеціальному деструкторі, створеному компілятором. Він називається deleting destructor, і його існування описується Itanium C++ ABI:

deleting destructor of a class T - A function that, in addition to the actions required of a complete object destructor, calls the appropriate deallocation function (i.e. operator delete) for T.

Далі ABI надає додаткові відомості:

The entries for virtual destructors are actually pairs of entries. The first destructor, called the complete object destructor, performs the destruction without calling delete() on the object. The second destructor, called the deleting destructor, calls delete() after destroying the object.

Отже, тепер механізм цієї операції має бути досить зрозумілим. Компілятор імітує «віртуальність» оператора видалення, викликаючи його з деструктора. Оскільки деструктор є віртуальним, те, що в кінцевому результаті викликається, є деструктором для динамічного типу об’єкта. У нашому прикладі це буде деструктор Derived, який може викликати правильний оператор delete, оскільки він знаходиться в тій самій області пошуку.

Однак, як каже ABI, такі класи потребують двох деструкторів. Якщо об’єкт знищено, але не видалено з купи, виклик оператора delete є неправильним. Тому існує окрема версія деструктора для знищення, без видалення.

Виглядає це приблизно так:

*b:                       Virtual table for Derived:
+------------+            +-----------------------------+
| vtable ptr | ---------> |        Derived::foo()       |  0x00
+------------+            +-----------------------------+
                          |     Derived::~Derived()     |  0x08
                          +-----------------------------+
                          | Derived::~DeletingDerived() |  0x10
                          +-----------------------------+

Тобто delete-expression викличе deleting destructor класу Derived через його ж віртуально таблицю, додавши до неї зміщення 0x10. Сам deleting destructor виглядає приблизно так:

Derived::~DeletingDerived() {
     Derived::~Derived();            // Виклик звичайного деструктора
     Derived::operator delete(this); // Виклик оператора видалення
}

Але, як ви розумієте, це все деталі реалізації, стандарт С++ нічого такого не описує, а тим більше не гарантує. І, описаний механізм роботи deleting destructors — не єдиний. Якщо я не помиляюсь Microsoft ABI, своєю чергою, реалізує це у дещо інший спосіб. Без додаткового деструктора, натомість передає у звичайний деструктор булевий прапорець.

Прихований текст
Derived::~Derived(bool call_delete) {   // неявний параметр
    std::cout << "Derived::~Derived()" << std::endl;

    Base::~Base(false);                 // неявно

    if (call_delete)                    // неявно
        Derived::operator delete(this); // неявно
}

--------------------------------
Тому, повертаючись до вашого питання про static_cast<Derived*>(b), я б не говорив про "правильність" викликів деструкторів. Без цього касту буде просто UB. Про деструктори вже мова не йде. Хоча у більшості реалізацій проблема буде саме у відсутності виклику. Але абстрактно, ніхто не дасть гарантій, що ви не отримаєте креш, тому що потрібний <деструктор> не був згенерований. Бо з погляду реалізації можлива різна поведінка.

P.S. - відповідь ще редагуватиметься

11

Re: Що робить даний уривок коду?

wander, окреме дякую за такий детальний розпис