Почему деструктор был казнен дважды? - PullRequest
10 голосов
/ 27 октября 2019
#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.

Ответы [ 2 ]

6 голосов
/ 27 октября 2019

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

Во-первых, этоберя ваш taxi и копируя из него Car, чтобы аргумент соответствовал.

Затем он копирует Car снова для передачи по значению.

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

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

3 голосов
/ 27 октября 2019

Что происходит?

Когда вы создаете Taxi, вы также создаете подобъект Car. А когда такси уничтожается, оба объекта разрушаются. Когда вы звоните test(), вы передаете Car по значению. Таким образом, вторая Car создается копией и будет разрушена, когда test() останется. Итак, у нас есть объяснение 3 деструкторам: первый и два последних в последовательности.

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

Это может быть только временный Car, созданный в качестве источника для аргумента Car. Так как этого не происходит при прямом предоставлении значения Car в качестве аргумента, я подозреваю, что это для преобразования Taxi в Car. Это неожиданно, поскольку в каждом Taxi уже есть подобъект Car. Поэтому я думаю, что компилятор делает ненужное преобразование в temp и не выполняет копирование, которое могло бы избежать этой температуры.

Разъяснения даны в комментариях:

Вот пояснения со ссылкой на стандарт для языка-юриста для проверки моих претензий:

  • Преобразование, на которое я здесь ссылаюсь, - это преобразование по конструктору [class.conv.ctor], т.е. создание объекта одного класса (здесь Car) на основе аргумента другого типа (здесь Taxi).
  • Это преобразование затем использует временный объект для возврата значения Car. Компилятору будет разрешено создавать копию в соответствии с [class.copy.elision]/1.1, поскольку вместо создания временного он может создать значение, которое будет возвращено непосредственно в параметр.
  • Так что, если этот temp дает побочные эффекты,это потому, что компилятор явно не использует это возможное копирование. Это не так, поскольку копирование не обязательно.

Экспериментальное подтверждение анализа

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

Мое вышеизложенное предположение заключалось в том, что компилятор выбрал неоптимальный процесс передачи параметров, используя преобразование конструктора Car(const &Taxi) вместо копирования непосредственно из подобъекта Car объекта Taxi.

Поэтому я попытался позвонить test(), но явно преобразовал Taxi в 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 сообщения деструктора, таким образом избегая ненужного временного объекта.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...