#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.
Taxi
obiektu do funkcji przyjmującejCar
obiekt według wartości?Car
problem ten znika i daje oczekiwane wyniki.Odpowiedzi:
Wygląda na to, że kompilator Visual Studio używa skrótu podczas krojenia twojego
taxi
wywoł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ą
taxi
i 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.
źródło
Co się dzieje ?
Tworząc a
Taxi
, tworzysz równieżCar
podobiekt. A kiedy taksówka zostaje zniszczona, oba obiekty są niszczone. Kiedy dzwonisztest()
, przekazujeszCar
wartość. Tak więc sekundaCar
zostaje skonstruowana jako kopia i zostanie zniszczona, gdytest()
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
Car
utworzony jako źródłoCar
argumentu. Ponieważ nie zdarza się to, gdy podaje się bezpośrednioCar
wartość jako argument, podejrzewam, że służy to przekształceniuTaxi
wCar
. Jest to nieoczekiwane, ponieważCar
w każdym jest już podobiektTaxi
. 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ń:
[class.conv.ctor]
, tj. Konstruowaniem obiektu jednej klasy (tutaj Car) na podstawie argumentu innego typu (tutaj Taxi).Car
wartoś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.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 zCar
podobiektuTaxi
.Więc próbowałem zadzwonić,
test()
ale jawnie przerzuciłemTaxi
naCar
.Mojej pierwszej próbie nie udało się poprawić sytuacji. Kompilator nadal używał nieoptymalnej konwersji konstruktora:
Moja druga próba się powiodła. Wykonuje także rzutowanie, ale używa rzutowania wskaźnika, aby zdecydowanie zasugerować kompilatorowi użycie
Car
podobiektuTaxi
i bez tworzenia tego głupiego obiektu tymczasowego: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ę:
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.
źródło
Taxi
można przekazać bezpośrednio doCar
konstruktora kopii), więc wybór kopii nie ma znaczenia.