仮想デストラクタを使用しない場合


48

私は仮想デストラクターについて何度も検索し、ほとんどが仮想デストラクターの目的と、仮想デストラクターが必要な理由に言及していると信じていました。また、ほとんどの場合、デストラクタは仮想である必要があると思います。

質問は次のとおりです。なぜC ++はデフォルトですべてのデストラクタを仮想に設定しないのですか?または他の質問:

仮想デストラクタを使用する必要がないのはいつですか?

どの場合、仮想デストラクタを使用すべきではありませんか?

仮想デストラクタを使用しなくても、それを使用する場合のコストはいくらですか?


6
そして、クラスが継承されることになっていない場合はどうなりますか?多くの標準ライブラリクラスを見てください。それらは継承されるように設計されていないため、仮想関数を持っているものはほとんどありません。
一部のプログラマーは

4
また、ほとんどの場合、デストラクタは仮想である必要があると思います。いや。どういたしまして。(構成を支持するのではなく)継承を乱用する人だけがそう考える。ほんの一握りの基本クラスと仮想関数を持つアプリケーション全体を見てきました。
マチューM.

1
@underscore_d典型的な実装では、そのような暗黙的なものがすべて仮想化されて最適化されていない限り、多態性クラスに対して追加のコードが生成されます。一般的なABI内では、これにはクラスごとに少なくとも1つのvtableが含まれます。クラスのレイアウトも変更する必要があります。パブリックインターフェイスの一部としてそのようなクラスを公開すると、信頼できる状態に戻ることはできません。これを再度変更すると、ABIの互換性が失われるためです。
FrankHB

1
@underscore_d「コンパイル時」というフレーズは不正確ですが、これは仮想デストラクタが自明ではなく、constexprを指定することもできないことを意味すると思います。実行時のパフォーマンスに多少の悪影響を及ぼします。
FrankHB

2
@underscore_d「ポインター」は赤いニシンのようです。おそらくメンバーへのポインターである必要があります(定義上、ポインターではありません)。通常のABIでは、メンバーへのポインターは(通常のポインターとして)マシンワードに収まらないことが多く、クラスを非多態から多態に変更すると、このクラスのメンバーへのポインターのサイズが変更されることがよくあります。
FrankHB

回答:


41

仮想デストラクタをクラスに追加する場合:

  • ほとんどの(すべて?)現在のC ++実装では、そのクラスのすべてのオブジェクトインスタンスがランタイムタイプの仮想ディスパッチテーブルへのポインターを格納する必要があり、その仮想ディスパッチテーブル自体が実行可能イメージに追加されます

  • 仮想ディスパッチテーブルのアドレスは、プロセス間で必ずしも有効ではないため、共有メモリでそのようなオブジェクトを安全に共有できなくなる可能性があります。

  • 仮想ポインタが埋め込まれているため、既知の入力または出力形式に一致するメモリレイアウトを持つクラスを作成Price_Tick*できません(たとえば、着信UDPパケットの適切にアライメントされたメモリを直接対象とし、データの解析/アクセスまたは変更に使用できます。またはnew発信パケットにデータを書き込むためにそのようなクラスを配置する)

  • デストラクタ呼び出し自体は、特定の条件下で、仮想的にディスパッチする必要があるため、アウトラインである必要がありますが、非仮想デストラクタは、呼び出し元にとって些細または無関係な場合、インライン化または最適化されます

「継承されるように設計されていない」という議論は、上で説明したように実用的な方法で悪化していなければ、常に仮想デストラクタを持っているわけではない実用的な理由にはなりません。しかし、それが悪い場合、コストを支払うタイミングの主要な基準です。クラスが基本クラスとして使用される場合は、デフォルトで仮想デストラクタを使用します。これは常に必要なわけではありませんが、派生クラスデストラクタが基本クラスポインタまたは参照を使用して呼び出された場合、偶発的な未定義の動作なしに階層内のクラスをより自由に使用できます。

「ほとんどの場合、デストラクタは仮想である必要があります」

そうではありません...多くのクラスにはそのような必要性はありません。それらを列挙するのは愚かなことだと思われる例がたくさんありますが、標準ライブラリを調べるか、ブーストと言うだけで、仮想デストラクタを持たないクラスの大部分があることがわかります。boost 1.53では、494個のうち72個の仮想デストラクターをカウントします。


23

どの場合、仮想デストラクタを使用すべきではありませんか?

  1. 継承されたくない具体的なクラスの場合。
  2. 多態的な削除のない基本クラスの場合。どちらのクライアントも、Baseへのポインターを使用して多態的に削除することはできません。

ところで、

どの場合に仮想デストラクタを使用する必要がありますか?

多態的な削除を伴う基本クラスの場合。


7
#2、特に多型削除なしの場合は+1 。デストラクタをベースポインタ経由で呼び出せない場合、特にクラスが以前に仮想化されていなかった場合は、仮想化することは不要で冗長です(そのため、RTTIで新しく肥大化します)。これに違反するユーザーを防ぐために、Herb Sutterがアドバイスしたように、基本クラスのdtorを保護された非仮想にし、派生デストラクタによってのみ、またはその後に呼び出されるようにします。
underscore_d

@underscore_d私が答えを逃した重要な点は、継承の存在のように、仮想コンストラクタを必要としない唯一のケースは、それが決して必要ではないことを確認できるときです
以前

14

仮想デストラクタを使用しなくても、それを使用する場合のコストはいくらですか?

仮想関数をクラス(継承またはクラス定義の一部)に導入するコストは、オブジェクトごとに格納される仮想ポインターの初期コストが非常に急(またはオブジェクトに依存しない)になる可能性があります。

struct Integer
{
    virtual ~Integer() {}
    int value;
};

この場合、メモリコストは比較的膨大です。クラスインスタンスの実際のメモリサイズは、64ビットアーキテクチャでは次のようになります。

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

このIntegerクラスの合計は、わずか4バイトではなく16バイトです。これらの数百万個をアレイに格納すると、最終的に16メガバイトのメモリ使用量になります。通常の8 MB L3 CPUキャッシュの2倍のサイズで、そのようなアレイを繰り返し反復すると、4メガバイトの同等の数倍も遅くなります追加のキャッシュミスとページフォールトの結果として、仮想ポインターなし。

ただし、オブジェクトごとのこの仮想ポインターコストは、仮想関数が増えても増加しません。1つのクラスに100個の仮想メンバー関数を含めることができ、インスタンスごとのオーバーヘッドは引き続き単一の仮想ポインターになります。

通常、仮想ポインタは、オーバーヘッドの観点からより直接的な懸念事項です。ただし、インスタンスごとの仮想ポインターに加えて、クラスごとのコストがかかります。仮想関数を持つ各クラスはvtable、仮想関数呼び出しが行われたときに実際に呼び出す(仮想/動的ディスパッチ)関数のアドレスを格納するメモリを生成します。vptrこのクラス固有の点、インスタンスごとに格納されvtable。このオーバーヘッドは通常、あまり心配ですが、それはあなたのバイナリサイズを膨らませると、このオーバーヘッドは、複雑なコードベースで千個のクラスのために不必要に支払われた場合は、ランタイム・コストのビットを追加するかもしれないが、例えばこのvtableコストの面では、実際に比例して、より増加しないとミックス内のより多くの仮想機能。

Javaユーザー定義型は暗黙的に中央のobject基本クラスから継承し、Javaのすべての関数は暗黙的に仮想(オーバーライド可能)であるため、パフォーマンスが重要な領域で作業するJava開発者は、この種のオーバーヘッドを非常によく理解しています(ボクシングのコンテキストで説明されることが多い)。)特に明記しない限り、本質的に。その結果、JavaはInteger同様にvptrインスタンスごとに関連付けられたこのスタイルのメタデータの結果として64ビットプラットフォームで16バイトのメモリを必要とする傾向があり、Javaでintランタイムを支払わずに単一のようなものをクラスにラップすることは通常不可能ですパフォーマンスコスト。

質問は次のとおりです。なぜC ++はデフォルトですべてのデストラクタを仮想に設定しないのですか?

C ++は、「従量制」という考え方とCから継承された多くのベアメタルハードウェア駆動の設計により、パフォーマンスを本当に優先します。vtable生成と動的ディスパッチに必要なオーバーヘッドを不必要に含めたくない関係するすべてのクラス/インスタンス。C ++のような言語を使用している主な理由の1つがパフォーマンスではない場合、C ++言語の多くは理想的なパフォーマンスよりも安全性と難易度が高いため、他のプログラミング言語の恩恵を受ける可能性があります。そのような設計を支持する主な理由。

仮想デストラクタを使用する必要がないのはいつですか?

かなり頻繁に。クラスが継承されるように設計されていない場合、クラスは仮想デストラクタを必要とせず、必要のないものに対して多分大きなオーバーヘッドを支払うだけになります。同様に、クラスが継承されるように設計されていても、ベースポインターを介してサブタイプインスタンスを削除しない場合でも、仮想デストラクターは必要ありません。その場合、安全な方法は、次のように、保護された非仮想デストラクタを定義することです。

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

どの場合、仮想デストラクタを使用すべきではありませんか?

実際に、仮想デストラクタ使用する必要がある場合に対応する方が簡単です。多くの場合、コードベース内のより多くのクラスは継承用に設計されません。

std::vectorそれは、このベースポインタの削除の問題になりやすいだろうとして、例えば、(、継承するように設計されておらず、一般的に継承すべきではない(非常に不安定なデザイン)std::vector不器用に加えて、故意に仮想デストラクタを避ける)オブジェクトスライス、あなたの場合の問題派生クラスは、新しい状態を追加します。

一般に、継承されるクラスには、パブリック仮想デストラクタまたは保護された非仮想デストラクタが必要です。C++ Coding Standards、第50章から:

50.基本クラスのデストラクターをパブリックおよび仮想、または保護された非仮想にします。削除する、または削除しない それが問題です。ベースBaseへのポインターを介した削除を許可する必要がある場合、Baseのデストラクターはパブリックで仮想でなければなりません。それ以外の場合は、保護され、非仮想でなければなりません。

C ++が暗黙的に強調する傾向があるものの1つ(設計が非常に脆くて扱いにくく、場合によっては安全でなくなる可能性があるため)は、継承が後付けとして使用するように設計されたメカニズムではないという考えです。多態性を念頭に置いた拡張性メカニズムですが、拡張性が必要な場所について先見性を必要とするメカニズムです。そのため、基本クラスは継承階層のルートとして事前に設計する必要があり、事前にそのような先見のない後付けとして後から継承するものではありません。

既存のコードを再利用するために単に継承したい場合、多くの場合、構成を強くお勧めします(複合再利用の原則)。


9

なぜC ++はデフォルトですべてのデストラクタを仮想に設定しないのですか? 追加のストレージと仮想メソッドテーブルの呼び出しのコスト。C ++は、これが負担になる可能性のあるシステム、低遅延、rtプログラミングに使用されます。


デストラクタダイナミックメモリのような多くのリソースが強い期限保証を提供するために使用することはできませんので、ハードリアルタイムシステムでの最初の場所で使用してはならない
マルコA.

9
@MarcoA。デストラクタはいつから動的なメモリ割り当てを意味するのですか?
chbaker0

@ chbaker0「いいね」を使用しました。私の経験では単純に使用されていません。
マルコA.

6
また、ハードリアルタイムシステムで動的メモリを使用できないこともナンセンスです。固定割り当てサイズと割り当てビットマップを使用して事前に構成されたヒープがメモリを割り当てるか、そのビットマップのスキャンに要する時間でメモリ不足の状態を返すことを証明するのはかなり簡単です。
MSalters

@msaltersは、私に考えさせてくれます。各操作のコストが型システムに保存されているプログラムを想像してください。リアルタイム保証のコンパイル時チェックを許可します。
-Yakk

5

これは、仮想デストラクタを使用しない場合の良い例です:Scott Meyersから:

クラスに仮想関数が含まれていない場合、それは多くの場合、基本クラスとして使用することを意図していないことを示しています。クラスが基本クラスとして使用されることを意図していない場合、デストラクタを仮想化することは通常悪い考えです。ARMでの議論に基づいて、この例を検討してください。

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

short intが16ビットを占有する場合、Pointオブジェクトは32ビットレジスタに収まります。さらに、Pointオブジェクトは、32ビット量として、CやFORTRANなどの他の言語で記述された関数に渡すことができます。ただし、Pointのデストラクタを仮想化すると、状況が変わります。

仮想メンバーを追加すると、そのクラスの仮想テーブルを指す仮想ポインターがクラスに追加されます。


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.ええ 仮想メソッドをまったく気にすることなく、クラスと継承を使用して再利用可能なメンバーと動作の連続したレイヤーを構築することが許可されたGood Old Daysを覚えている人はいますか?さあ、スコット。私は核心点を得るが、その「しばしば」は本当に到達している。
underscore_d

3

仮想デストラクタはランタイムコストを追加します。クラスに他の仮想メソッドがない場合、コストは特に高くなります。仮想デストラクタは、オブジェクトが基本クラスへのポインタを介して削除または破棄される特定のシナリオでのみ必要です。この場合、基本クラスのデストラクタは仮想である必要があり、派生クラスのデストラクタは暗黙的に仮想になります。デストラクタが仮想である必要がないような方法で多態性基本クラスが使用されるいくつかのシナリオがあります。

  • 派生クラスのインスタンスがヒープに割り当てられていない場合(たとえば、スタック上または他のオブジェクト内のみ)。(初期化されていないメモリと配置演算子newを使用する場合を除きます。)
  • 派生クラスのインスタンスがヒープに割り当てられているが、削除が最も派生したクラスへのポインターのみで発生する場合(例:がstd::unique_ptr<Derived>あり、ポリモーフィズムは非所有ポインターと参照のみで発生する場合)。別の例は、を使用してオブジェクトが割り当てられる場合std::make_shared<Derived>()です。std::shared_ptr<Base>最初のポインタがである限り、使用しても問題ありませんstd::shared_ptr<Derived>。これは、共有ポインタには、デストラクタ(削除者)に対する独自の動的ディスパッチがあり、仮想ベースクラスデストラクタに必ずしも依存しないためです。

もちろん、前述の方法でのみオブジェクトを使用するという慣習は簡単に破られます。したがって、Herb Sutterのアドバイスはこれまでと同じように有効です。「基本クラスのデストラクターは、パブリックで仮想のもの、または保護された非仮想のものでなければなりません。」そうすれば、誰かが非仮想デストラクタで基本クラスへのポインタを削除しようとすると、ほとんどの場合、コンパイル時にアクセス違反エラーを受け取ります。

さらに、(パブリック)基本クラスとして設計されていないクラスがあります。個人的にはfinal、C ++ 11以降で作成することをお勧めします。スクエアペグとして設計されている場合、ラウンドペグとしてはあまりうまく機能しない可能性があります。これは、基本クラスと派生クラスの間の明示的な継承コントラクト、NVI(非仮想インターフェイス)設計パターン、具体的な基本クラスではなく抽象クラス、および保護されたメンバー変数の嫌悪感に対する私の好みに関連しています、しかし、私はこれらの見解のすべてがある程度議論の余地があることを知っています。


1

デストラクタの宣言はvirtualclass継承可能にする場合にのみ必要です。通常、標準ライブラリのクラス(などstd::string)は仮想デストラクタを提供しないため、サブクラス化を目的としていません。


3
その理由は、サブクラス化+ポリモーフィズムの使用です。仮想デストラクタが必要なのは、動的な解決が必要な場合のみです。つまり、マスタークラスへの参照/ポインタ/その他が実際にサブクラスのインスタンスを参照する場合があります。
ミシェルビロー

2
@MichelBillaudは、実際には仮想dtorなしでもポリモーフィズムを保持できます。仮想dtorは、ポリモーフィック削除、つまりdelete基本クラスへのポインターの呼び出しにのみ必要です。
chbaker0

1

vtableを作成するためのコンストラクターにオーバーヘッドがあります(他の仮想関数がない場合、その場合は必ずではありませんが、仮想デストラクターも必要です)。また、他の仮想関数がない場合は、オブジェクトが他の仮想関数よりも1ポインターサイズ大きくなります。明らかに、サイズの増加は小さなオブジェクトに大きな影響を与える可能性があります。

vtableを取得し、それを介して関数indirectoryを呼び出すための追加のメモリ読み取りがあります。これは、デストラクタが呼び出されたときの非仮想デストラクタのオーバーヘッドです。そしてもちろん、結果として、デストラクタの呼び出しごとに少し余分なコードが生成されます。これは、コンパイラが実際の型を推測できない場合です。実際の型を推測できる場合、コンパイラはvtableを使用せず、デストラクタを直接呼び出します。

あなたは必要がありますあなたのクラスは基本クラスとして意図されている場合、それを作成することができるならば/あなたが仮想デストラクタを必要とし、それは創造であるタイプを知っているコードよりも他のいくつかのエンティティによって破壊され、特に、仮想デストラクタを持っています。

不明な場合は、仮想デストラクタを使用してください。「適切なデストラクタが呼び出されない」ことによって引き起こされるバグを見つけようとするよりも、問題として表示される場合は、virtualを削除する方が簡単です。

要するに、次の場合には、仮想デストラクタを使用するべきではありません。1.仮想機能がない。2.クラスから派生しないでください(finalC ++ 11でマークします。この方法で、派生した場合にコンパイラが通知します)。

ほとんどの場合、「大量のコンテンツ」がない限り、作成と破棄は特定のオブジェクトの使用に費やされる時間の大部分ではありません(1MBの文字列の作成には明らかに時間がかかります。現在の場所からコピーしてください)。1MBの文字列を破壊することは、150Bの文字列を破壊することより悪くありません。どちらも文字列ストレージの割り当てを解除する必要があります。 「毒パターン」-しかし、それは実際のアプリケーションを実稼働で実行する方法ではありません。

要するに、小さなオーバーヘッドがありますが、小さなオブジェクトの場合、違いが生じる可能性があります。

また、コンパイラは場合によっては仮想ルックアップを最適化することができるため、ペナルティにすぎないことにも注意してください

パフォーマンス、メモリフットプリントなどに関しては、いつものように、ベンチマークとプロファイルと測定を行い、結果を他の選択肢と比較し、時間/メモリのほとんどが費やされる場所を調べ、90%の最適化を試みないでください。あまり実行されないコード[ほとんどのアプリケーションには、実行時間に大きな影響を与えるコードの約10%と、まったく影響のないコードの90%があります]。高度な最適化レベルでこれを行うと、コンパイラーが良い仕事をするという利点をすでに得られます!繰り返し、もう一度確認し、段階的に改善します。その特定の種類のアプリケーションで多くの経験を積んでいない限り、賢くなり、何が重要で何が重要でないかを理解しようとしないでください。


1
「vtableを作成するためのコンストラクタのオーバーヘッド」 -vtableは通常、コンパイラによってクラスごとに「作成」されます。コンストラクタは、構築中のオブジェクトインスタンスにポインタを格納するオーバーヘッドのみを持ちます。
トニー

さらに...私はすべて、時期尚早な最適化を回避することについてですが、逆に、You **should** have a virtual destructor if your class is intended as a base-class過度の単純化と、時期尚早な悲観化です。これは、だれでもベースへのポインターを介して派生クラスを削除できる場合にのみ必要です。多くの場合、そうではありません。あなたはそれがわかっている場合は、その後、必ず、オーバーヘッドが発生します。実際の呼び出しがコンパイラによって静的に解決できる場合でも、これは常に追加されます。そうでなければ、あなたが適切に人々があなたのオブジェクトで何ができるかを制御するとき、それは価値がありません
underscore_d
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.