Dlaczego destruktor został wykonany dwukrotnie?

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;
}

to jest wynik :

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

Używam MS Visual Studio Community 2017 (przepraszam, nie wiem jak zobaczyć wersję Visual C ++). Kiedy korzystałem z trybu debugowania. Uważam, że jeden destruktor jest wykonywany podczas opuszczania void test(Car c){ }ciała funkcji zgodnie z oczekiwaniami. Po zakończeniu pojawił się dodatkowy destruktor test(taxi);.

test(Car c)Funkcja używa wartości jako parametru formalnego. Samochód jest kopiowany podczas przechodzenia do funkcji. Pomyślałem więc, że po opuszczeniu funkcji będzie tylko jedno „Samochód jest zniszczony”. Ale tak naprawdę są dwa „samochód jest zniszczony” przy opuszczaniu funkcji. (Pierwszy i drugi wiersz jak pokazano na wyjściu) Dlaczego są dwa „samochód jest zniszczony”? Dziękuję Ci.

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

kiedy dodam funkcję wirtualną class Car na przykład: virtual void drive() {} wtedy otrzymuję oczekiwany wynik.

Car is destructed.
Taxi is destructed.
Car is destructed.
qiazi
źródło
3
Może występować problem z tym, jak kompilator obsługuje wycinanie obiektów podczas przekazywania Taxiobiektu do funkcji przyjmującej Carobiekt według wartości?
Jakiś programista koleś
1
Musi być twoim starym kompilatorem C ++. g ++ 9 daje oczekiwane wyniki. Za pomocą debugera określ przyczynę utworzenia dodatkowej kopii obiektu.
Sam Varshavchik
2
Testowałem g ++ z wersją 7.4.0 i clang ++ z wersją 6.0.0. Dały oczekiwany wynik, który różni się od wyniku operacji. Problemem może być kompilator, którego używa.
Marceline
1
Reprodukowałem w MS Visual C ++. Jeśli dodam zdefiniowany przez użytkownika konstruktor kopii i domyślny konstruktor, Carproblem ten znika i daje oczekiwane wyniki.
interjay
1
Dodaj kompilator i wersję do pytania
Wyścigi lekkości na orbicie

Odpowiedzi:

7

Wygląda na to, że kompilator Visual Studio używa skrótu podczas krojenia twojego taxiwywołania funkcji, co jak na ironię powoduje, że wykonuje on więcej pracy, niż można by się spodziewać.

Po pierwsze, pobiera z niego twoją taxii konstruuje kopię Car, aby argument był zgodny.

Następnie kopiuje Car ponownie wartość pass-by-value.

To zachowanie zanika, gdy dodajesz zdefiniowany przez użytkownika konstruktor kopii, więc kompilator wydaje się robić to z własnych powodów (być może wewnętrznie jest to prostsza ścieżka kodu), wykorzystując fakt, że jest to dozwolone, ponieważ sama kopia jest banalna. Fakt, że nadal można zaobserwować to zachowanie za pomocą nietrywialnego destruktora, jest trochę aberracją.

Nie wiem, w jakim stopniu jest to legalne (szczególnie od C ++ 17), ani dlaczego kompilator miałby takie podejście, ale zgodziłbym się, że nie jest to wynik, którego intuicyjnie oczekiwałbym. Ani GCC, ani Clang nie robią tego, chociaż może być tak, że robią to w ten sam sposób, ale są lepsi w elekcji kopii. I nie zauważyłem, że nawet VS 2019 nie jest jeszcze świetny w gwarantowanym elizji.

Lekkość Wyścigi na orbicie
źródło
Przykro mi, ale czy nie jest to dokładnie to, co powiedziałem w przypadku „konwersji z Taxi na samochód, jeśli kompilator nie wykonuje kopiowania”.
Christophe
To niesprawiedliwa uwaga, ponieważ przekazywanie według wartości względem przekazywania przez referencje w celu uniknięcia krojenia zostało dodane tylko w edycji, aby pomóc OP poza tym pytaniem. Wtedy moja odpowiedź nie była strzałem w ciemność, od początku była jasno wyjaśniona, skąd może pochodzić i cieszę się, że doszedłeś do takich samych wniosków. Patrząc teraz na twoje sformułowanie: „Wygląda na ... nie wiem”, myślę, że jest tutaj tyle samo niepewności, ponieważ szczerze mówiąc, ja i ja nie rozumiemy, dlaczego kompilator musi generować taką temperaturę.
Christophe
Okej, a następnie usuń niepowiązane części twojej odpowiedzi, pozostawiając tylko jeden powiązany akapit za sobą
Wyścigi lekkości na orbicie
Ok, usunąłem rozpraszające fragmenty krojenia i uzasadniłem kwestię usuwania kopii precyzyjnymi odniesieniami do normy.
Christophe
Czy możesz wyjaśnić, dlaczego tymczasowy samochód powinien zostać skonstruowany z taksówki, a następnie ponownie skopiowany do parametru? I dlaczego kompilator nie robi tego, gdy jest wyposażony w zwykły samochód?
Christophe
3

Co się dzieje ?

Tworząc a Taxi, tworzysz również Carpodobiekt. A kiedy taksówka zostaje zniszczona, oba obiekty są niszczone. Kiedy dzwonisz test(), przekazujesz Carwartość. Tak więc sekunda Carzostaje skonstruowana jako kopia i zostanie zniszczona, gdy test()zostanie pozostawiona. Mamy więc wyjaśnienie dla 3 niszczycieli: pierwszego i dwóch ostatnich w sekwencji.

Czwarty destruktor (czyli drugi w sekwencji) jest nieoczekiwany i nie mogłem odtworzyć z innymi kompilatorami.

Może być tylko tymczasowo Carutworzony jako źródło Carargumentu. Ponieważ nie zdarza się to, gdy podaje się bezpośrednio Carwartość jako argument, podejrzewam, że służy to przekształceniu Taxiw Car. Jest to nieoczekiwane, ponieważ Carw każdym jest już podobiekt Taxi. Dlatego myślę, że kompilator dokonuje niepotrzebnej konwersji na temp i nie wykonuje kopiowania, które mogłoby uniknąć tej temp.

Wyjaśnienie podane w komentarzach:

Oto wyjaśnienie w odniesieniu do standardu dla prawnika ds. Języka w celu weryfikacji moich roszczeń:

  • Konwersja, o której tu mówię, jest konwersją konstruktora [class.conv.ctor], tj. Konstruowaniem obiektu jednej klasy (tutaj Car) na podstawie argumentu innego typu (tutaj Taxi).
  • Ta konwersja wykorzystuje wówczas obiekt tymczasowy do zwrócenia swojej Carwartości. Kompilator byłby uprawniony do wykonania korekcji kopiowania zgodnie z tym [class.copy.elision]/1.1, że zamiast konstruować tymczasowy, mógłby skonstruować wartość, która zostanie zwrócona bezpośrednio do parametru.
  • Więc jeśli ta temperatura daje efekty uboczne, to dlatego, że kompilator najwyraźniej nie korzysta z tej możliwości eliminacji kopii. To nie jest złe, ponieważ wybór kopii nie jest obowiązkowy.

Eksperymentalne potwierdzenie analizy

Mogę teraz odtworzyć Twój przypadek za pomocą tego samego kompilatora i narysować eksperyment, aby potwierdzić, co się dzieje.

Moje powyższe założenie było takie, że kompilator wybrał proces nieoptymalnego przekazywania parametrów, używając konwersji konstruktora Car(const &Taxi)zamiast konstruowania kopii bezpośrednio z Carpodobiektu Taxi.

Więc próbowałem zadzwonić, test()ale jawnie przerzuciłem Taxina Car.

Mojej pierwszej próbie nie udało się poprawić sytuacji. Kompilator nadal używał nieoptymalnej konwersji konstruktora:

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

Moja druga próba się powiodła. Wykonuje także rzutowanie, ale używa rzutowania wskaźnika, aby zdecydowanie zasugerować kompilatorowi użycie Carpodobiektu Taxii bez tworzenia tego głupiego obiektu tymczasowego:

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

I niespodzianka: działa zgodnie z oczekiwaniami, produkując tylko 3 wiadomości o zniszczeniu :-)

Podsumowujący eksperyment:

W ostatnim eksperymencie podałem niestandardowego konstruktora poprzez konwersję:

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

i zaimplementuj to za pomocą *this = *static_cast<Car*>(&taxi);. Brzmi głupio, ale generuje również kod, który wyświetla tylko 3 komunikaty destruktora, unikając w ten sposób niepotrzebnego obiektu tymczasowego.

Prowadzi to do wniosku, że w kompilatorze może być błąd, który powoduje takie zachowanie. Chodzi o to, że w niektórych okolicznościach można by pominąć możliwość bezpośredniego tworzenia kopii z klasy podstawowej.

Christophe
źródło
2
Nie odpowiada na pytanie
Lekkość ściga się na orbicie
1
@qiazi Myślę, że to potwierdza hipotezę tymczasowej konwersji bez usuwania kopii, ponieważ ta tymczasowa byłaby generowana z funkcji, w kontekście dzwoniącego.
Christophe
1
Kiedy mówisz „konwersja z Taxi na samochód, jeśli twój kompilator nie wykonuje wyboru kopiowania”, o którym to opisie chodzi? Nie powinno być żadnej kopii, którą należy udzielić w pierwszej kolejności.
interjay
1
@interjay, ponieważ kompilator nie musi konstruować tymczasowej wartości Car w oparciu o podobiekty Car Taxi w celu wykonania konwersji, a następnie skopiować tę temperaturę do parametru Car: może pominąć kopię i bezpośrednio skonstruować parametr z oryginalnego podobiektu.
Christophe
1
Eliminacja kopii ma miejsce, gdy standard stanowi, że kopia powinna zostać utworzona, ale w pewnych okolicznościach pozwala na uniknięcie kopii. W takim przypadku nie ma powodu, aby kopia była tworzona w pierwszej kolejności (odwołanie do Taximożna przekazać bezpośrednio do Carkonstruktora kopii), więc wybór kopii nie ma znaczenia.
interjay