デストラクタが2回実行されたのはなぜですか?


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){ }関数本体を期待どおりに残すと、1つのデストラクタが実行されます。そしてtest(taxi);が終わると追加のデストラクタが現れました。

test(Car c)関数は、仮パラメータとして値を使用しています。関数に行くと車がコピーされます。そのため、機能を離れる際に「車が破壊される」のは1つだけになると思いました。しかし、実際には、関数を終了するときに「Car is destructed」が2つあります(出力に示されている1行目と2行目)。なぜ「Car is destructed」が2つあるのですか?ありがとうございました。

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

class Car たとえば、仮想関数を追加するとvirtual void drive() {} 、期待した出力が得られます。

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

3
値でオブジェクトを取得する関数にオブジェクトを渡すときに、コンパイラがオブジェクトのスライスを処理する方法に問題がTaxiある可能性がありCarますか?
一部のプログラマー、

1
古いC ++コンパイラでなければなりません。g ++ 9は期待される結果を与えます。デバッガーを使用して、オブジェクトの追加のコピーが作成される理由を判別します。
Sam Varshavchik

2
私はバージョン7.4.0でg ++とバージョン6.0.0でclang ++をテストしました。彼らはopの出力とは異なる期待される出力を与えました。したがって、問題は彼が使用するコンパイラーに関するものかもしれません。
Marceline

1
MS Visual C ++で再現しました。ユーザー定義のコピーコンストラクターとデフォルトコンストラクターを追加するとCar、この問題はなくなり、期待どおりの結果が得られます。
-interjay

1
質問にコンパイラとバージョンを追加してください
オービットのライトネスレース

回答:


7

Visual Studioコンパイラがスライスするときに少し近道を取っているようです taxi、関数呼び出しのため皮肉なことに、予想以上に多くの作業が行われます。

まず、それはあなたを取っています taxiCar引数を一致させるために、それをからコピー構築し。

次に、それは Car 値渡しのために再度ます。

ユーザー定義のコピーコンストラクターを追加すると、この動作はなくなります。そのため、コンパイラーは独自の理由(おそらく内部的にはより単純なコードパス)でこれを実行しているようです。コピー自体は簡単です。自明ではないデストラクタを使用してこの動作を引き続き観察できるという事実は、少し異常です。

これが正当である範囲(特にC ++ 17以降)、またはコンパイラがこのアプローチを採用する理由はわかりませんが、直感的に期待した出力ではないことに同意します。GCCもClangもこれを行いませんが、同じように動作する場合もありますが、その場合はコピーを除外する方が優れています。私がしているにも2019 VS保証エリジオンではまだ大きくはないことに気づきました。


申し訳ありませんが、「コンパイラがコピーの省略を行わない場合、タクシーから車への変換」で私が言ったこととは正確には同じではありません。
Christophe

これは不公平な発言です。スライスを回避するための値渡しvs参照渡しは、この質問を超えてOPを支援するために編集でのみ追加されたためです。それから私の答えは暗闇の中でのショットではなく、それがどこから来るのか最初からはっきりと説明されました、そしてあなたが同じ結論に至るのを見て私は嬉しく思います。さて、あなたの定式化を見ると、「それは...わからない」のように見えますが、ここでも同じくらいの不確実性があると思います。正直なところ、コンパイラーがこの一時を生成する必要がある理由を私もあなたも理解していないためです。
Christophe

さて、回答の無関係な部分を削除して、関連する1つの段落だけを残します
オービットのライトネスレース

OK、私は煩わしいスライシングパラを削除し、標準を正確に参照して、コピーの省略に関する要点を正当化しました。
Christophe

一時的な車がタクシーからコピー構築され、パラメータに再度コピーされる理由を説明できますか?そして、なぜ普通の車が提供されているのにコンパイラがこれを行わないのですか?
Christophe

3

何が起こっている ?

を作成するTaxiと、Carサブオブジェクトも作成されます。そして、タクシーが破壊されると、両方のオブジェクトが破壊されます。呼び出すときtest()Car、値で渡します。そのため、1秒後Carにコピーが作成され、test()が残されると破棄されます。したがって、3つのデストラクタの説明があります。シーケンスの最初と2つは最後です。

4番目のデストラクタ(シーケンスの2番目)は予期せぬものであり、他のコンパイラでは再現できませんでした。

引数のCarソースとして作成されるのは一時的なものだけCarです。直接提供する場合、それは発生しませんのでCar、引数として値を、私はそれが変換するためである疑いがあるTaxiにしCarCarすべてのにすでにサブオブジェクトがあるため、これは予期しないことですTaxi。したがって、コンパイラーは不要なtempへの変換を行い、このtempを回避できた可能性のあるコピーの省略は行わないと思います。

コメントでの説明:

言語弁護士が私の主張を検証するための基準を参照した説明:

  • ここで言及している変換は、コンストラクタによる変換です。 [class.conv.ctor]。つまり、別のタイプの引数(ここではタクシー)に基づいて、あるクラス(ここではCar)のオブジェクトを構築します。
  • この変換では、一時オブジェクトを使用してそのCar値を返します。コンパイラは、コピー省略を行うことを許可されます[class.copy.elision]/1.1、一時的なものを作成する代わりに、パラメーターに直接返される値を作成できることができます。
  • したがって、このtempが副作用をもたらす場合、それはコンパイラがこの可能なコピー削除を明らかに使用していないためです。コピー省略は必須ではないので、それは間違いではありません。

分析の実験的確認

同じコンパイラを使用してケースを再現し、実験を行って何が起こっているのかを確認できます。

上記の私の想定では、コンパイラCar(const &Taxi)は、のCarサブオブジェクトから直接コピーを構築する代わりに、コンストラクター変換を使用して、次善のパラメーター受け渡しプロセスを選択しましたTaxi

呼び出ししようとした私はそうtest()はなく、明示的にキャストTaxiAへCar

私の最初の試みは状況を改善することに成功しませんでした。コンパイラーは依然として次善のコンストラクター変換を使用しました。

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

私の2回目の試みは成功しました。キャストも行いますが、ポインターのキャストを使用して、この愚かな一時オブジェクトを作成せずにの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この一時変数は呼び出し元のコンテキストで関数から生成されるため、これはコピー省略なしの一時変換の仮説を裏付けていると思います。
Christophe

1
「コンパイラがコピー省略を行わない場合のタクシーから車への変換」と言うとき、どのコピー省略を参照していますか?そもそも削除する必要のあるコピーはないはずです。
-interjay

1
@interjayは、コンパイラーが変換を行うためにタクシーのCarサブオブジェクトに基づいてCarの一時オブジェクトを構築し、このtempをCarパラメーターにコピーする必要がないためです。コピーを省略して、元のサブオブジェクトからパラメーターを直接構築できます。
Christophe

1
コピーの省略は、コピーを作成する必要があると規格で規定されている場合ですが、特定の状況下ではコピーを省略できます。この場合、最初にコピーが作成される理由がないため(への参照をコピーコンストラクターにTaxi直接渡すことができますCar)、コピーの省略は関係ありません。
インタージェイ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.