[Effective C++シリーズ]-マルチステートベースクラスに対してVirtual構造関数を宣言する
5816 ワード
Declare destructors virtual in polymorphic base classes.
[原理] C++は、derived classオブジェクトがbase classタイプのポインタを介して削除されると、このbase classがnon-virtualの構造関数を持っている場合、その構造の結果は定義されていないと指摘しています.すなわち、通常、オブジェクトのbase class成分は析出されるが、derived class成分は破棄されず、derived classの析出関数さえ呼び出されない.
これにより、「ローカル破棄」されたオブジェクトが形成され、リソースが漏洩します.
〔例〕 例:
顧客コードで自動車オブジェクトを使用する場合、どの自動車を使用するかという詳細に関心がない場合は、新しく生成されたderived classオブジェクトを指すbase classポインタまたは参照を返す工場関数(または工場クラス)を設計して自動車オブジェクトを作成できます.
ファクトリ関数のルールを遵守するために、返されるオブジェクトはheapにある必要があります(そうでなければ、関数が返されるポインタは、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、関数が返されるオブジェクトを適切にdeleteするには、クライアントコードが必要です.
まず、上記の方法には2つの欠陥があることを説明する必要があります.
1.お客様コードに依存してdelete操作を実行する傾向があり、お客様はこのことを忘れてしまう可能性があります.
2.ファクトリ関数構造は、一般的な顧客コードエラーの予防を考慮する必要があります.
しかし、最も根本的な弱点は、クライアントコードが返されたderived classオブジェクトを完全に破棄できないことです.
簡単な方法はbase classのvirtual構造関数を定義することです.その後、derived classオブジェクトを削除すると、すべてのderived class成分を含むオブジェクトが破棄されます.
〔引申1〕 クラスがマルチステート(Polymorphism)として使用される必要がある場合は、クラスにvirtual構造関数を宣言する必要があります.すなわち、virtual関数がある限り、どのclassにもvirtual構造関数がほとんど決定されます.
ただし、classにvirtual関数がない場合、すなわちマルチステート用途として使用されない場合、通常はbase class(noncopyableクラスなどの特殊な場合を除く)として使用することを意図していないことを意味します.classがbase classとして使用されない場合は、構造関数の数を定義しないほうがいいです.
C++は関数をvirtualと定義するのに代価があるため,この代価は虚表ポインタvirtual table pointerである.
virtual関数を実装するには、オブジェクトが実行中にどのvirtual関数を呼び出すかを決定するための情報を携帯する必要があります.この情報は通常、いわゆるvptr(virtual table pointer)ポインタによって指摘される.vptrは関数ポインタからなる配列を指し、vtblとなる(virtual table).virtual関数を持つclassごとに対応するvtblがあります.オブジェクトに対してvirtual関数を呼び出すと、呼び出された関数は、オブジェクトのvptrが指すvtblに依存します.コンパイラは、適切な関数ポインタを探します.
したがって、virtual関数を定義するclassの各オブジェクトにはvptrが含まれます.これにより、virtual関数の存在によってオブジェクトのボリュームが増加します.
例:
32ビットシステムでは、intタイプが32 bitsを占めるため、pointオブジェクトは全部で64 bitsを占め、64 bitバッファに詰め込まれ、Cが生きているFORTRANのような他の言語に伝達される関数としてもよい.
しかしpointに構造関数が含まれている場合、pointオブジェクトが占有する空間は96 bits、(2つのintsに1つのvptr)になります.オブジェクト体積は64 bitsから96 bitsに増加します.
一方、64 bitコンピュータアーキテクチャでは、pointオブジェクトが128 bits(ポインタタイプが64 bitsを占めるため)を占有します.オブジェクトボリュームは64 bitsから128 bitsに増加します.
このようなオブジェクトは64ビットバッファに埋め込まれず、C++のpointオブジェクトも他の言語(例えばC)内の同じ宣言と同じ構造ではないため、他の言語で作成された関数に渡すことができず、移植性がなくなります.
したがって,多態用途ではないclassの構造関数をvirtualと宣言することは不合理である.class内に少なくとも1つのvirtual関数が含まれている場合にのみ、その構造関数をvirtualとして宣言する必要があります.
〔引申2〕 non-virtual構造関数を持つクラスを継承しようとしないでください.vector、list、set、unordered_などのすべてのSTLコンテナが含まれます.map、stringなど.これは資源の漏れを招くからです!
残念なことに、C++にはjavaのfinal classesやc#のsealed classesのような「派生禁止」メカニズムは提供されていません.
〔引申3〕 1つのclassを抽象class(pure virtual class)として定義したいが、pure virtual関数がない場合は、このclassにpure virtual構造関数を宣言するのが便利です.
ただし、このpure virtual構造関数の定義を指定する必要があります.
構造関数の動作方法は、最も深い派生であるからです.(most derived)のそのclassの構造関数が最初に呼び出され、次にbase classの各構造関数が呼び出されます.コンパイラはabstract_classのderived classesに~abstract_classの呼び出し動作を作成するので、~abstract_classに定義を提供する必要があります.そうしないと、リンクはエラーを報告します.
[まとめ] 1.polymorphic(多態性を有する)base classesはvirtual構造関数を宣言するべきである.classにvirtual関数がある場合、virtual構造関数を宣言するべきである.このようなbase classは「base classインタフェースを介してderived classオブジェクトを処理する」ことを目的として設計されているからである.
2.一部のclassはもともとbase classとして使用されるように設計されていないか、base classとしても多態性を備えていないため、virtual構造関数として宣言すべきではない.
[補足] デフォルトで生成される構造関数はpublicでnon-virtualです.
これにより、「ローカル破棄」されたオブジェクトが形成され、リソースが漏洩します.
class car
{
public:
car();
~car();
...
};
class diesel_car : public car {…};
class solar_car: public car {…};
class electric_car : public car {…};
顧客コードで自動車オブジェクトを使用する場合、どの自動車を使用するかという詳細に関心がない場合は、新しく生成されたderived classオブジェクトを指すbase classポインタまたは参照を返す工場関数(または工場クラス)を設計して自動車オブジェクトを作成できます.
car* get_car();
ファクトリ関数のルールを遵守するために、返されるオブジェクトはheapにある必要があります(そうでなければ、関数が返されるポインタは、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、stackにあるオブジェクトのライフサイクルが関数ドメインであるため、関数が返されるオブジェクトを適切にdeleteするには、クライアントコードが必要です.
car* p_car = get_car(); // car
… //
delete p_car; //
まず、上記の方法には2つの欠陥があることを説明する必要があります.
1.お客様コードに依存してdelete操作を実行する傾向があり、お客様はこのことを忘れてしまう可能性があります.
2.ファクトリ関数構造は、一般的な顧客コードエラーの予防を考慮する必要があります.
しかし、最も根本的な弱点は、クライアントコードが返されたderived classオブジェクトを完全に破棄できないことです.
簡単な方法はbase classのvirtual構造関数を定義することです.その後、derived classオブジェクトを削除すると、すべてのderived class成分を含むオブジェクトが破棄されます.
class car
{
public:
car();
virtual ~car();
...
};
ただし、classにvirtual関数がない場合、すなわちマルチステート用途として使用されない場合、通常はbase class(noncopyableクラスなどの特殊な場合を除く)として使用することを意図していないことを意味します.classがbase classとして使用されない場合は、構造関数の数を定義しないほうがいいです.
C++は関数をvirtualと定義するのに代価があるため,この代価は虚表ポインタvirtual table pointerである.
virtual関数を実装するには、オブジェクトが実行中にどのvirtual関数を呼び出すかを決定するための情報を携帯する必要があります.この情報は通常、いわゆるvptr(virtual table pointer)ポインタによって指摘される.vptrは関数ポインタからなる配列を指し、vtblとなる(virtual table).virtual関数を持つclassごとに対応するvtblがあります.オブジェクトに対してvirtual関数を呼び出すと、呼び出された関数は、オブジェクトのvptrが指すvtblに依存します.コンパイラは、適切な関数ポインタを探します.
したがって、virtual関数を定義するclassの各オブジェクトにはvptrが含まれます.これにより、virtual関数の存在によってオブジェクトのボリュームが増加します.
例:
class point
{
public:
point(int coord_x, int coord_y);
~point();
private:
int x, y;
};
32ビットシステムでは、intタイプが32 bitsを占めるため、pointオブジェクトは全部で64 bitsを占め、64 bitバッファに詰め込まれ、Cが生きているFORTRANのような他の言語に伝達される関数としてもよい.
しかしpointに構造関数が含まれている場合、pointオブジェクトが占有する空間は96 bits、(2つのintsに1つのvptr)になります.オブジェクト体積は64 bitsから96 bitsに増加します.
一方、64 bitコンピュータアーキテクチャでは、pointオブジェクトが128 bits(ポインタタイプが64 bitsを占めるため)を占有します.オブジェクトボリュームは64 bitsから128 bitsに増加します.
このようなオブジェクトは64ビットバッファに埋め込まれず、C++のpointオブジェクトも他の言語(例えばC)内の同じ宣言と同じ構造ではないため、他の言語で作成された関数に渡すことができず、移植性がなくなります.
したがって,多態用途ではないclassの構造関数をvirtualと宣言することは不合理である.class内に少なくとも1つのvirtual関数が含まれている場合にのみ、その構造関数をvirtualとして宣言する必要があります.
残念なことに、C++にはjavaのfinal classesやc#のsealed classesのような「派生禁止」メカニズムは提供されていません.
class abstract_class
{
public:
virtual ~abstract_class() = 0;
};
ただし、このpure virtual構造関数の定義を指定する必要があります.
abstract_class::~abstract_class(){}
構造関数の動作方法は、最も深い派生であるからです.(most derived)のそのclassの構造関数が最初に呼び出され、次にbase classの各構造関数が呼び出されます.コンパイラはabstract_classのderived classesに~abstract_classの呼び出し動作を作成するので、~abstract_classに定義を提供する必要があります.そうしないと、リンクはエラーを報告します.
2.一部のclassはもともとbase classとして使用されるように設計されていないか、base classとしても多態性を備えていないため、virtual構造関数として宣言すべきではない.