「Effective C++」条項14:ベースクラスに虚析構造関数があることを確定する


クラスがどれだけのオブジェクトが存在するかを追跡したい場合があります.簡単な方法は、オブジェクトの個数を統計する静的クラスメンバーを作成することです.このメンバーは0に初期化され,構造関数に1を加え,構造関数に1を減らす.(条項m 26では、この方法をどのクラスにも簡単に追加できるようにカプセル化する方法を説明しています.「my article on counting objects」は、この技術の他の改善を提供しています.
軍事アプリケーションには、敵の目標を表すクラスがあると想定されています.
class enemytarget {
public:
  enemytarget() { ++numtargets; }
  enemytarget(const enemytarget&) { ++numtargets; }
  ~enemytarget() { --numtargets; }

  static size_t numberoftargets()
  { return numtargets; }

  virtual bool destroy();       //   enemytarget   
                                //     

private:
  static size_t numtargets;     //      
};

//             ;
//       0
size_t enemytarget::numtargets;

 
この類はあなたのために政府の防御契約を勝ち取ることはできません.国防部の要求から遠すぎますが、ここで問題を説明する必要を満たすのに十分です.
敵の戦車は特殊な敵ターゲットであるため、公有継承方式でenemytargetから派生したクラスとして抽象化されることが自然に考えられる(条項35およびm 33参照).敵の目標の総数だけでなく、敵の戦車の総数にも関心を持つため、ベースクラスと同様に派生クラスにも上記のようなテクニックが採用されています.
class enemytank: public enemytarget {
public:
  enemytank() { ++numtanks; }

  enemytank(const enemytank& rhs)
  : enemytarget(rhs)
  { ++numtanks; }

  ~enemytank() { --numtanks; }

  static size_t numberoftanks()
  { return numtanks; }

  virtual bool destroy();

private:
  static size_t numtanks;         //        
};

(上記の2つのクラスのコードを書き終えると、この問題に対する条項m 26の汎用的な解決策がより理解できます.)
最後に、プログラムの他の場所でnewでenemytankオブジェクトを動的に作成し、deleteで削除するとします.
enemytarget *targetptr = new enemytank;
...
delete targetptr;

これまでやったことはすべて正常だったようです.2つのクラスは構造関数の中で構造関数の操作をクリアしました.アプリケーションにもエラーはなく、newで生成されたオブジェクトも最後にdeleteで削除されました.しかし、ここには大きな問題がある.プログラムの行為は予測できない--何が起こるか分からない.
 
c++言語標準この問題については、派生クラスのオブジェクトがベースクラスのポインタで削除され、ベースクラスに虚析関数がない場合、結果は確定しません.これは、コンパイラが生成したコードが好きなことをすることを意味します.ハードディスクを再フォーマットし、ボスに電子メールを送り、プログラムのソースコードを相手にファックスし、何事も起こり得ることを意味します.(実際に実行されると、派生クラスの構造関数は決して呼び出されません.この例では、targetptrが削除された場合、enemytankの数値は変更されません.では、敵の戦車の数は間違っています.正確な情報に高度に依存する部隊にとって、どのような結果になりますか?)
 
この問題を回避するには、enemytargetの構造関数をvirtualにするだけです.構造関数を虚と宣言すると、オブジェクトメモリが解放されると、enemytankとenemytargetの構造関数が呼び出されます.
 
ほとんどのベースクラスと同様に、enemytargetクラスには虚関数が含まれています.虚関数の目的は派生クラスに自分の動作をカスタマイズさせることである(条項36参照)ため、ほとんどのベースクラスには虚関数が含まれている.
 
クラスに虚関数が含まれていない場合は、一般的にベースクラスとして使用されないことを示します.クラスがベースクラスとして使用する準備ができていない場合、構造関数を虚にするのは一般的に悪い考えです.次の例を見てください.この例はarm(「the annotated c++reference manual」)という本のテーマ討論に基づいています.
//     2d   
class point {
public:
  point(short int xcoord, short int ycoord);
  ~point();

private:
  short int x, y;
};

short intが16ビットを占める場合、pointオブジェクトは32ビットのレジスタにちょうど適しています.また、1つのpointオブジェクトは、cやfortranなどの他の言語で書かれた関数に32ビットのデータとして渡すことができる.しかしpointの解析関数が虚であれば状況は変わる.
 
虚関数を実装するには、オブジェクトが実行時にどの虚関数を呼び出すかを決定できるように、オブジェクトに追加情報が必要です.ほとんどのコンパイラでは、この追加情報の具体的な形式はvptr(虚関数テーブルポインタ)と呼ばれるポインタです.vptrはvtbl(虚関数テーブル)と呼ばれる関数ポインタ配列を指す.虚関数のあるクラスごとにvtblが付属しています.オブジェクトの虚関数の呼び出しを要求すると、実際に呼び出される関数は、vtblを指すvptrに基づいてvtblに対応する関数ポインタを見つけて決定されます.
 
虚関数の実装の詳細は重要ではありません(もちろん、興味があれば、条項m 24を読むことができます)、重要なのは、pointクラスに虚関数が含まれている場合、そのオブジェクトの体積はいつの間にか倍増し、2つの16ビットのshortから2つの16ビットのshortに32ビットのvptrを加えることです!ポイントオブジェクトはもう32ビットレジスタに入れられません.また、c++のpointオブジェクトは、vptrがないため、他の言語がcで宣言したように同じ構造を持っていないように見えます.したがって、他の言語で書かれた関数でpointを渡すことも不可能であり、vptrを設計しない限り、それ自体が実装の詳細であり、コードが移植できない可能性があります.
 
だから基本的な1つは、理由もなく虚析構関数を宣言することは、永遠に宣言しないのと同じように間違っている.実際、多くの人は、クラスに少なくとも1つの虚関数が含まれている場合にのみ、虚解析関数を宣言するとまとめています.
 
これは良い準則であり、多くの場合適用されます.しかし残念なことに,クラスに虚関数がない場合,非虚析関数の問題ももたらす.例えば、条項13には、ユーザカスタム配列の下限を実現するクラステンプレートがある.(条項m 33の提案を無視して)派生クラステンプレートを書いて、命名可能な配列(すなわち、各配列に名前がある)を表すことを決定したとします.
template<class t>                //     
class array {                    // (    13)
public:
  array(int lowbound, int highbound);
  ~array();

private:
  vector<t> data;
  size_t size;
  int lbound, hbound;
};

template<class t>
class namedarray: public array<t> {
public:
  namedarray(int lowbound, int highbound, const string& name);
  ...

private:
  string arrayname;
};

アプリケーションのどこかでnamedarrayタイプへのポインタをarrayタイプのポインタに変換し、deleteでarrayポインタを削除すると、すぐに「不確定な動作」の罠に落ちます.
namedarray<int> *pna =
  new namedarray<int>(10, 20, "impending doom");

array<int> *pa;

...


pa = pna;                // namedarray<int>* -> array<int>*

...

delete pa;               //    !    ,pa->arrayname
                         //      ,  *pa namedarray
                         //        

 
現実には、あなたが想像していたよりも頻繁にこのような状況が現れています.既存のクラスに何をさせ、それから同じことを派生させ、特殊な機能を加えるのは現実的には珍しくない.namedarrayはarrayの動作を再定義していません.arrayのすべての機能を継承し、変更していません.追加の機能が追加されただけです.しかし、非虚析構造関数の問題は依然として存在する(他の問題もあり、m 33を参照)
 
最後に,いくつかのクラスで純粋な虚構造関数を宣言するのは便利であることを指摘するに値する.純粋な虚関数は、インスタンス化できないクラス(つまり、このタイプのオブジェクトを作成できない)の抽象クラスを生成します.あるクラスを抽象クラスにしたいときもありますが、ちょうど純粋な虚関数はありません.どうしよう?抽象クラスはベースクラスとして使用される準備ができているため、ベースクラスには虚析構関数が必要であり、純虚関数は抽象クラスを生成するので、方法は簡単です.抽象クラスになりたいクラスに純虚析構関数を宣言します.
ここでは、次の例を示します.
class awov {                // awov = "abstract w/o
                            // virtuals"
public:
  virtual ~awov() = 0;      //           
                            
};

このクラスには純粋な虚関数があるので抽象的であり、虚構造関数があるので、構造関数の問題は発生しません.しかし、ここでもう一つのことは、純粋な虚構造関数の定義を提供する必要があります.
awov::~awov() {}           //          

この定義は、最下位の派生クラスの構造関数が最初に呼び出され、各ベースクラスの構造関数が呼び出されるため、虚構造関数が動作する方法が必要です.すなわち,抽象クラスでもコンパイラは~awovに対する呼び出しを生成するので,関数体を提供することを保証する.そうしないとリンクが検出され、最後には戻って追加しなければなりません.
 
関数で何でもできますが、上記の例のように、何もしなくてもよくないわけではありません.この場合、空の関数の呼び出しによるオーバーヘッドを回避するために、構造関数をインライン関数として宣言することは自然に考えられます.これはいい方法ですが、一つはっきりしなければならないことがあります.構造関数が虚であるため、そのアドレスはクラスのvtblに入る必要があります(条項m 24を参照).しかし,インライン関数は独立した関数として存在しない(これが「インライン」の意味である)ため,特殊な方法でアドレスを得る必要がある.条項33では、仮想構造関数がinlineであることを宣言すると、呼び出し時に発生するオーバーヘッドは回避されますが、コンパイラは必ずどこかでこの関数のコピーを生成します.