web-dev-qa-db-ja.com

いつ仮想デストラクタを使用しないのですか?

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

それから質問は:なぜデフォルトですべてのデストラクタをc ++が仮想に設定しないのですか?または他の質問:

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

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

必要がなくても仮想デストラクタを使用する場合のコストはいくらですか?

51
ggrr

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

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

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

  • 組み込みの仮想ポインタが、既知の入力または出力形式(たとえば、Price_Tick*は、着信UDPパケット内の適切に調整されたメモリに直接向けることができ、データの解析/アクセスまたは変更に使用できます。または、そのようなクラスの配置-newingを実行して、発信パケットにデータを書き込みます)

  • デストラクタ呼び出し自体-特定の条件下で、仮想的にディスパッチする必要があるため、アウトオブラインにする必要があります

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

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

そうではありません...多くのクラスにはそのような必要はありません。列挙するのは馬鹿げていると感じる必要のない例がたくさんありますが、標準ライブラリを調べたり、boostと言ったりすると、仮想デストラクタを持たないクラスの大部分が見られます。ブースト1.53では、494のうち72の仮想デストラクタを数えます。

41
Tony

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

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

ところで、

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

ポリモーフィック削除を含む基本クラスの場合。

25
songyuanyao

必要がなくても仮想デストラクタを使用すると、どのくらいのコストがかかりますか?

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

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メガバイトの同等のものより何倍も遅くなる可能性があります追加のキャッシュミスとページフォールトの結果としての仮想ポインターなし。

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

仮想ポインタは通常、オーバーヘッドの観点から最も差し迫った問題です。ただし、インスタンスごとの仮想ポインターに加えて、クラスごとのコストがかかります。仮想関数を含む各クラスは、仮想関数呼び出しが行われたときに実際に呼び出す関数(仮想/動的ディスパッチ)へのアドレスを格納するvtableをメモリに生成します。インスタンスごとに保存されたvptrは、このクラス固有のvtableを指します。通常、このオーバーヘッドはそれほど問題にはなりませんが、複雑なコードベースの1000のクラスでこのオーバーヘッドが不必要に支払われた場合、バイナリサイズが膨らみ、実行時のコストが少し増える可能性があります。コストのこのvtable側は、実際には、ミックス内の仮想関数が増えるにつれて比例して増加します。

Javaユーザー定義型は中央のobject基本クラスと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は意図的に仮想デストラクタを回避します)不格好なことに加えてobject slicing派生クラスが新しい状態を追加した場合の問題。

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

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

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

既存のコードを再利用するために単に継承したい場合は、多くの場合、合成が強く推奨されます(複合再利用の原則)。

16
user204677

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

9
M.L.

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

クラスに仮想関数が含まれていない場合、それは多くの場合、それが基本クラスとして使用されることを意図していないことを示しています。クラスを基本クラスとして使用することを意図していない場合、デストラクタを仮想化することは通常悪い考えです。 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などの他の言語で記述された関数に渡すことができます。ただし、ポイントのデストラクタを仮想化すると、状況が変化します。

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

6
basav

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

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

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

次に、(パブリック)基本クラスとして設計されていないクラスがあります。私の個人的な推奨は、C++ 11以降でfinalにすることです。正方形のペグになるように設計されている場合は、丸いペグほどうまく機能しない可能性があります。これは、基本クラスと派生クラスの間の明示的な継承コントラクト、NVI(非仮想インターフェイス)のデザインパターン、具象ベースクラスではなく抽象ベースクラス、および特に保護されたメンバー変数の嫌悪に関する私の好みに関連しています、しかし私はこれらの見解のすべてがある程度物議を醸していることを知っています。

3
Arne Vogel

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

1
Constantinius

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

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

あなたのクラスがベースクラスとして意図されている場合、特にそれが作成時の型を知っているコード以外のエンティティによって作成/破壊される可能性がある場合、あなたはすべき仮想デストラクタを持っている仮想デストラクタが必要です。

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

簡単に言うと、次の場合、仮想デストラクタを使用しますすべきではありません 1.仮想関数がない場合。 2.クラスから派生しないでください(C++ 11ではfinalとマークします。これにより、コンパイラがクラスから派生しようとしたかどうかがコンパイラに通知されます)。

ほとんどの場合、「大量のコンテンツ」がない限り、作成と破棄は特定のオブジェクトの使用に費やされる時間の主要な部分ではありません(1MBの文字列の作成には明らかに時間がかかるため、少なくとも1MBのデータは現在の場所からコピーされます)。 1MBの文字列の破棄は150Bの文字列の破棄よりも悪くはありません。どちらも文字列ストレージの割り当てを解除する必要があり、それ以外はそれほど多くないので、そこで費やされる時間は通常同じです(デバッグビルドで、割り当て解除でメモリがいっぱいになる場合を除きます) 「ポイズンパターン」-しかし、実際のアプリケーションを本番環境で実行する方法とは異なります]。

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

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

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

1
Mats Petersson