Почему деструктор был казнен дважды?

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.
qiazi
источник
3
Может ли быть проблема в том, как компилятор обрабатывает срезы объектов при передаче Taxiобъекта в функцию, принимающую Carобъект по значению?
Какой-то программист чувак
1
Должно быть, ваш старый компилятор C ++. g ++ 9 дает ожидаемые результаты. Используйте отладчик, чтобы определить причину, по которой сделана дополнительная копия объекта.
Сэм Варшавчик
2
Я проверил g ++ с версией 7.4.0 и clang ++ с версией 6.0.0. Они дали ожидаемый результат, который отличается от результата операции. Так что проблема может быть в компиляторе, который он использует.
Марселин
1
Я воспроизвел с MS Visual C ++. Если я добавлю пользовательский конструктор копирования и конструктор по умолчанию для Carэтого, эта проблема исчезнет и даст ожидаемые результаты.
межджай
1
Пожалуйста, добавьте компилятор и версию к вопросу
Гонки

Ответы:

7

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

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

Тогда, это копирование Car снова для передачи по значению.

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

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

Гонки легкости на орбите
источник
Извините, но разве это не совсем то, что я сказал с «преобразованием из Такси в Автомобиль, если ваш компилятор не выполняет копирование».
Кристоф
Это несправедливое замечание, потому что передача по значению по сравнению с передачей по ссылке, чтобы избежать нарезки, была добавлена ​​только при редактировании, чтобы помочь ОП после этого вопроса. Тогда мой ответ не был выстрелом в темноте, он был четко объяснен с самого начала, откуда он может прийти, и я рад видеть, что вы пришли к тем же выводам. Теперь, глядя на вашу формулировку: «Похоже, я не знаю», я думаю, что здесь присутствует столько же неопределенности, потому что, честно говоря, ни я, ни вы не понимаете, почему компилятор должен генерировать этот темп.
Кристоф
Хорошо, тогда удалите несвязанные части вашего ответа, оставив после себя только один связанный абзац
Гонки
Хорошо, я удалил отвлекающий парализующий элемент и обосновал точку зрения об исключении копирования точными ссылками на стандарт.
Кристоф
Не могли бы вы объяснить, почему временный Автомобиль должен быть скомпонован из Такси и затем снова скопирован в параметр? И почему компилятор не делает этого, когда предоставляется простой автомобиль?
Кристоф
3

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

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

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

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

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

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

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

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

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

Мое предположение выше заключалось в том, что компилятор выбрал неоптимальный процесс передачи параметров, используя преобразование конструктора 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 сообщения деструктора, таким образом избегая ненужного временного объекта.

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

Christophe
источник
2
Не отвечает на вопрос
Гонки
1
@qiazi Я думаю, это подтверждает гипотезу о временном преобразовании для преобразования без исключения копирования, потому что этот временный объект будет сгенерирован из функции в контексте вызывающего.
Кристоф
1
Говоря «преобразование из Такси в Автомобиль, если ваш компилятор не делает выбор копии», на какую версию копирования вы ссылаетесь? Там не должно быть копии, которая должна быть исключена в первую очередь.
межджай
1
@interjay, потому что компилятору не нужно создавать временный объект Car на основе подобъекта Car Taxi для выполнения преобразования, а затем копировать этот темп в параметр Car: он может исключить копию и напрямую построить параметр из исходного подобъекта.
Кристоф
1
Отклонение от копирования - это когда в стандарте указывается, что копия должна быть создана, но при определенных обстоятельствах допускается удаление копии. В этом случае нет оснований для создания копии в первую очередь (ссылка на Taxiможет быть передана непосредственно в Carконструктор копирования), поэтому удаление копии не имеет значения.
междурядный