notにクラスの仮想デストラクタを宣言する正当な理由はありますか?いつ書くのを避けるべきですか?
以下のいずれかに該当する場合、仮想デストラクタを使用する必要はありません。
あなたが本当にメモリを切望されていない限り、それを避ける特別な理由はありません。
質問に明示的に答えるには、つまり、いつnot仮想デストラクタを宣言する必要があります。
C++ '98/'03
仮想デストラクタを追加すると、クラスが POD(プレーンな古いデータ) *から変更されるか、非PODに集約される場合があります。これは、クラスタイプがどこかで初期化された集約である場合、プロジェクトのコンパイルを停止する可能性があります。
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
極端な場合、そのような変更は、PODを必要とする方法でクラスが使用されている場合、未定義の動作を引き起こす可能性もあります。 Ellipsisパラメーター経由で渡すか、memcpyで使用します。
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* PODタイプは、メモリレイアウトについて特定の保証があるタイプです。標準では、PODタイプのオブジェクトからchars(またはunsigned chars)の配列にコピーして、元に戻す場合、結果は元のオブジェクトと同じになるというだけです。]
最新のC++
C++の最近のバージョンでは、PODの概念はクラスレイアウトとその構築、コピー、および破壊に分けられました。
Ellipsisの場合、未定義の動作ではなくなり、実装定義のセマンティクスで条件付きでサポートされるようになりました(N3937-〜C++ '14-5.2.2/7):
...非自明なコピーコンストラクタ、非自明な移動コンストラクタ、または自明でないデストラクタを持ち、対応するパラメータがないクラスタイプの潜在的に評価された引数を渡すことは、実装で条件付きでサポートされます。定義されたセマンティクス。
=default
以外のデストラクタを宣言することは、些細なことではないことを意味します(12.4/5)
...デストラクタは、ユーザーが提供していなければ簡単です...
Modern C++に対するその他の変更により、コンストラクターを追加できるため、集約の初期化問題の影響が軽減されます。
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
仮想メソッドがある場合にのみ、仮想デストラクターを宣言します。仮想メソッドを作成したら、ヒープ上でインスタンス化したり、基本クラスへのポインターを保存したりすることを避けようとは思いません。これらはどちらも非常に一般的な操作であり、デストラクタが仮想として宣言されていない場合、多くの場合静かにリソースをリークします。
クラスの型を持つサブクラスのオブジェクトへのポインタでdelete
が呼び出される可能性がある場合は常に、仮想デストラクタが必要です。これにより、コンパイル時にコンパイラがヒープ上のオブジェクトのクラスを知る必要なく、実行時に正しいデストラクタが確実に呼び出されます。たとえば、B
がA
のサブクラスであると仮定します。
A *x = new B;
delete x; // ~B() called, even though x has type A*
コードのパフォーマンスが重要でない場合は、安全のために、作成するすべての基本クラスに仮想デストラクタを追加するのが妥当です。
ただし、タイトなループで多数のオブジェクトをdelete
ingしていることに気付いた場合、仮想関数(空の関数でも)を呼び出すことによるパフォーマンスのオーバーヘッドが顕著になる可能性があります。コンパイラは通常、これらの呼び出しをインライン化することはできません。また、プロセッサはどこへ行くかを予測するのが難しい場合があります。これがパフォーマンスに大きな影響を与えることはまずありませんが、言及する価値があります。
すべてのC++クラスが、動的多態性を持つ基本クラスとしての使用に適しているわけではありません。
クラスを動的多態性に適したものにしたい場合、そのデストラクターは仮想でなければなりません。さらに、サブクラスがオーバーライドする可能性のあるメソッド(すべてのパブリックメソッドに加えて、内部で使用される可能性のある保護されたメソッドを意味する可能性があります)は仮想でなければなりません。
クラスが動的なポリモーフィズムに適していない場合、デストラクタを仮想としてマークしないでください。そうすることは誤解を招くためです。それは単にあなたのクラスを間違って使用することを人々に奨励します。
以下は、デストラクタが仮想であっても、動的なポリモーフィズムに適さないクラスの例です。
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
このクラスの重要なポイントは、RAIIのスタックに座ることです。このクラスのオブジェクトへのポインターを渡す場合、そのサブクラスは言うまでもなく、それは間違っています。
仮想関数とは、割り当てられたすべてのオブジェクトが仮想関数テーブルポインターによってメモリコストを増加させることを意味します。
したがって、プログラムで非常に多くのオブジェクトを割り当てる必要がある場合は、オブジェクトごとに追加の32ビットを節約するために、すべての仮想関数を回避する価値があります。
その他の場合はすべて、dtorを仮想化するために不幸なデバッグを保存します。
デストラクタを仮想として宣言しない適切な理由は、これによりクラスに仮想関数テーブルが追加されないようにすることであり、可能な限り回避する必要があります。
多くの人は、デストラクタを常に安全であると宣言することを好むことを知っています。ただし、クラスに他の仮想関数がない場合は、仮想デストラクタを使用しても意味がありません。クラスから他のクラスを派生させる他の人にクラスを提供したとしても、クラスにアップキャストされたポインターでdeleteを呼び出す理由はありません。そうする場合、これをバグと見なします。
さて、1つの例外があります。つまり、クラスが派生オブジェクトのポリモーフィック削除を実行するために(誤って)使用される場合ですが、あなたまたは他の人たちは、これには仮想デストラクタが必要であることを願っています。
別の言い方をすれば、クラスに非仮想デストラクタがある場合、これは非常に明確なステートメントです。「派生オブジェクトの削除に私を使用しないでください!」
多数のインスタンスを持つ非常に小さなクラスがある場合、vtableポインターのオーバーヘッドにより、プログラムのメモリ使用量に違いが生じる可能性があります。クラスに他の仮想メソッドがない限り、デストラクタを非仮想にすることでオーバーヘッドを節約できます。
クラスにvtableがないことを絶対に確実に確認する必要がある場合は、仮想デストラクタも含めることはできません。
これはまれなケースですが、実際に起こります。
これを行うパターンの最もよく知られた例は、DirectX D3DVECTORおよびD3DMATRIXクラスです。これらは構文糖の関数ではなくクラスメソッドですが、これらのクラスは多くの高性能アプリケーションの内部ループで特に使用されるため、関数のオーバーヘッドを回避するためにクラスには意図的にvtableがありません。
通常、デストラクタは仮想として宣言しますが、内部ループで使用されるパフォーマンスが重要なコードがある場合は、仮想テーブルのルックアップを避けたいかもしれません。これは、衝突チェックなどの場合に重要になることがあります。ただし、継承を使用する場合は、これらのオブジェクトを破棄する方法に注意してください。そうしないと、オブジェクトの半分しか破棄されません。
そのオブジェクトのanyメソッドが仮想の場合、オブジェクトの仮想テーブル検索が発生することに注意してください。したがって、クラスに他の仮想メソッドがある場合、デストラクタの仮想仕様を削除しても意味がありません。
基本クラスで実行され、仮想的に動作する必要がある操作では、仮想にする必要があります。基本クラスインターフェイスを介して削除を多態的に実行できる場合、仮想的に動作する必要があります。
クラスから派生するつもりがない場合、デストラクタは仮想である必要はありません。そして、たとえそうだとしても、保護された非仮想デストラクタは、基本クラスポインタの削除が必要ない場合と同じくらい良いです。