Чому деструктор був виконаний двічі?


12
#include <iostream>
using namespace std;

class Car
{
public:
    ~Car()  { cout << "Car is destructed." << endl; }
};

class Taxi :public Car
{
public:
    ~Taxi() {cout << "Taxi is destructed." << endl; }
};

void test(Car c) {}

int main()
{
    Taxi taxi;
    test(taxi);
    return 0;
}

це вихід :

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.

Я використовую MS Visual Studio Community 2017 (Вибачте, я не знаю, як побачити видання Visual C ++). Коли я використовував режим налагодження. Я вважаю, що один деструктор виконується, коли виходить із void test(Car c){ }функції функції, як очікувалося. І зайвий руйнівник з'явився, коли test(taxi);закінчився.

test(Car c)Функція використовує значення в якості формального параметра. Автомобіль копіюється під час переходу до функції. Тому я подумав, що при виході з функції буде лише один «Автомобіль знищений». Але насправді є два "Автомобіль знищений" при виході з функції (перший і другий рядок, як показано на виході) Чому є два "Автомобіль знищений"? Дякую.

===============

коли я додаю віртуальну функцію, class Car наприклад: virtual void drive() {} Тоді я отримую очікуваний вихід.

Car is destructed.
Taxi is destructed.
Car is destructed.

3
Чи може бути проблемою в тому, як компілятор обробляє нарізку об'єкта при передачі Taxiоб'єкта функції, що приймає Carоб'єкт за значенням?
Якийсь програміст чувак

1
Повинен бути ваш старий компілятор C ++. g ++ 9 дає очікувані результати. Використовуйте відладчик, щоб визначити причину створення додаткової копії об'єкта.
Сем Варшавчик

2
Я перевірив g ++ з версією 7.4.0 і clang ++ з версією 6.0.0. Вони давали очікуваний вихід, який відрізняється від виходу оп. Тому проблема може полягати в компіляторі, який він використовує.
Марселін

1
Я відтворив за допомогою MS Visual C ++. Якщо я додаю призначений для користувача конструктор копій і конструктор за замовчуванням, Carтоді ця проблема зникає, і вона дає очікувані результати.
interjay

1
Додайте компілятор та версію до питання
Гонки легкості в орбіті

Відповіді:


7

Схоже, компілятор Visual Studio робить трохи ярлика під час нарізки taxiфункції для виклику функції, що іронічно призводить до того, що він робить більше роботи, ніж можна було очікувати.

По-перше, це взяття вашого taxiта копіювання Carз нього, щоб аргумент збігався.

Потім це копіювання Car знову для значення проходу.

Така поведінка зникає, коли ви додаєте призначений користувачем конструктор копій, тому компілятор, здається, робить це з власних причин (можливо, внутрішньо це простіший шлях коду), використовуючи той факт, що це "дозволено" тому, що сама копія банальна. Те, що ви все ще можете спостерігати за такою поведінкою за допомогою нетривіального деструктора, є дещо відхиленням.

Я не знаю, наскільки це легально (особливо з моменту C ++ 17), або чому компілятор застосував би такий підхід, але я погоджуюся, що це не результат, який я інтуїтивно очікував би. Ні GCC, ні Clang не роблять цього, хоча може статися, що вони роблять так само, але тоді краще видаляти копію. Я вже помітив , що навіть VS 2019 все ще не велика на гарантованої елізії.


Вибачте, але чи не це саме те, що я сказав з "перетворенням таксі в автомобіль, якщо ваш компілятор не виконує копіювання".
Крістоф

Це несправедливе зауваження, оскільки пропуск за значенням та пропуск через refenece, щоб уникнути нарізки, додався лише в редагуванні, щоб допомогти ОП поза цим питанням. Тоді моя відповідь не була пострілом у темряві, це було чітко пояснено з самого початку, звідки це може виникнути, і я радий бачити, що ви прийшли до тих же висновків. Тепер, дивлячись на вашу формулювання: "Схоже, що ... я не знаю", я думаю, що тут є така ж кількість невизначеності, тому що відверто кажучи, ні я, ні ви не розумієте, чому компілятору потрібно генерувати цей темп.
Крістоф

Добре, тоді видаліть непов’язані частини вашої відповіді, залишивши позаду лише єдиний пов'язаний абзац
Гонки легкості в орбіті

Гаразд, я видалив відволікаючий параметр нарізки, і я виправдав думку про копіювання елісіона з точними посиланнями на стандарт.
Крістоф

Чи можете ви пояснити, чому тимчасовий автомобіль повинен бути скопійований з таксі і потім скопійований знову в параметр? І чому компілятор цього не робить, якщо забезпечений звичайним автомобілем?
Крістоф

3

Що відбувається ?

Коли ви створюєте Taxi, ви також створюєте Carсубопредмет. А коли таксі знищується, обидва об’єкти руйнуються. Під час дзвінка test()ви передаєте Carзначення за значенням. Таким чином, секунда Carотримує копію і буде знищена, коли test()її залишиться. Отже, у нас є пояснення для 3 деструкторів: перший і два останні в послідовності.

Четвертий деструктор (тобто другий у послідовності) несподіваний, і я не зміг відтворити інші компілятори.

Він може бути лише тимчасовим, Carствореним як джерело Carаргументу. Оскільки це не відбувається при наданні прямого Carзначення в якості аргументу, я підозрюю, що це для перетворення Taxiв Car. Це несподівано, оскільки Carу кожного вже є субектив Taxi. Тому я думаю, що компілятор робить непотрібне перетворення в темп і не робить елісій копіювання, який міг би уникнути цієї температури.

Пояснення, дане в коментарях:

Ось роз'яснення з посиланням на стандарт для мови-юриста для перевірки моїх претензій:

  • Конверсія, про яку я тут маю на увазі, - це перетворення конструктором [class.conv.ctor], тобто побудова об'єкта одного класу (тут Car) на основі аргументу іншого типу (тут Taxi).
  • Ця конверсія використовує тоді тимчасовий об'єкт для повернення свого Carзначення. Компілятору було б дозволено зробити елісію копії відповідно до того [class.copy.elision]/1.1, що замість побудови тимчасової вона може побудувати значення, яке буде повернуто безпосередньо в параметр.
  • Отже, якщо ця темп дає побічні ефекти, це тому, що компілятор, очевидно, не використовує цю можливу копію-елізію. Це не неправильно, оскільки копіювати елізію не обов’язково.

Експериментальне підтвердження аналізу

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

Моє припущення вище полягало в тому, що компілятор обрав процес передачі неоптимальних параметрів, використовуючи перетворення конструктора Car(const &Taxi)замість побудови копії безпосередньо з Carсубекта Taxi.

Тож я спробував зателефонувати, test()але явно перекинув Taxiна a Car.

Першої моєї спроби покращити ситуацію не вдалося. Компілятор все ще використовував неоптимальне перетворення конструктора:

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages

Друга моя спроба вдалася. Він також виконує кастинг, але використовує кастинг покажчиків, щоб настійно запропонувати компілятору використовувати Carсубект Taxiі без створення цього нерозумного тимчасового об'єкта:

test(*static_cast<Car*>(&taxi));  //  :-)

І сюрприз: він працює як слід, створюючи лише 3 повідомлення про знищення :-)

Завершальний експеримент:

В кінцевому експерименті я надав спеціальний конструктор шляхом перетворення:

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 

і реалізувати це за допомогою *this = *static_cast<Car*>(&taxi);. Звучить нерозумно, але це також генерує код, який відображатиме лише 3 повідомлення про деструктори, тим самим уникаючи зайвого тимчасового об’єкта.

Це призводить до думки, що в компіляторі може виникнути помилка, яка викликає таку поведінку. Це - можливість прямої копіювання з базового класу була б упущена за певних обставин.


2
Не відповідає на питання
гонки легкості в орбіті

1
@qiazi Я думаю, що це підтверджує гіпотезу про тимчасове для конвертації без копіювання elision, тому що це тимчасове може генеруватися поза функцією, в контексті абонента.
Крістоф

1
Коли ви говорите "перетворення з таксі в автомобіль, якщо ваш компілятор не виконує копіювання елісей", про яку копію елізію ви маєте на увазі? Не повинно бути жодної копії, яку потрібно спершу видалити.
interjay

1
@interjay, тому що компілятору не потрібно створювати тимчасовий автомобіль на основі суб'єкта автомобіля таксі для здійснення конверсії, а потім скопіювати цей темп у параметр Car: він може схилити копію та безпосередньо сконструювати параметр з вихідного субекти.
Крістоф

1
Копія elision - це коли стандарт зазначає, що копія повинна бути створена, але за певних обставин дозволяє копіюватись. У цьому випадку немає жодної причини створити копію, в першу чергу (посилання на неї Taxiможна передати безпосередньо на Carконструктор копій), тому елісія копії не має значення.
interjay
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.