7. クラス

7.1. アクセス修飾子

7.1.1. 規約事項 41

クラス内でpublicまたはprotectedなメンバ変数を宣言しないでください。 以下の理由から、publicなメンバ変数の使用は推奨されません。

  1. publicなメンバ変数は、オブジェクト指向プログラミングの基本原則の1つ、 すなわちデータのカプセル化に違反しています。 たとえば、AccountBalanceがpublicなメンバ変数となっているBankAccount型のクラスでは、 AccountBalance変数の値は、そのクラスの利用者によって変更されてしまうでしょう。 メンバ変数がprivateで宣言されていれば、その値はクラスのメンバ関数以外からは変更されないことが保証されます。
  2. プログラム内の任意の関数がpublicなメンバ変数を変更すると、見つけにくいバグが発生する可能性があります。
  3. publicなメンバ変数を禁止することで、そのクラスの利用者にコードを修正させることなく、クラス内部の実装を変更することができます。 クラス設計の原則は、クラスのpublicなインタフェースの安定性を維持することです。 クラスの内部がどう実装されているか、クラスの利用者に意識させるべきではありません。

派生クラスから参照可能となるため、クラス内でのprotectedなメンバ変数の使用は推奨されません。 protectedなメンバ変数が存在する場合、派生クラスがその変数を使用している可能性があるため、変数の名前や型を変更できません。 何らかの理由で派生クラスが基底クラスのデータにアクセスする必要がある場合、 privateメンバ変数の値を返すprotectedなアクセサを、基底クラスに作成してください。 アクセサをインラインで定義することで、パフォーマンスの低下を避けることができます。

構造体はpublicなデータのみを含むため、使用は推奨しません。 しかし、他の言語(Cなど)とのインタフェースでは、構造体を使用せざるを得ない場合もあるでしょう。

7.2. インライン関数

7.2.1. 推奨事項 20

メンバ変数の値を単に返す関数(アクセサ)はインラインで定義します。

7.2.2. 推奨事項 21

転送関数はインラインで定義します。

7.2.3. 推奨事項 22

コンストラクタとデストラクタはインラインであってはなりません。 関数をインラインで宣言する通常の理由は、そのパフォーマンスを向上させることです。

クラスのメンバの値を返すだけのアクセス関数や、 別の関数を呼び出すいわゆる転送関数などの小さな関数は、 通常はインラインでなければなりません。

インライン関数を正しく使用すると、コードサイズも小さくなります。 他のインライン関数を呼び出す関数は、見た目が明らかに小さくても、 コンパイラにとっては複雑すぎてインライン化できないことがあります。

7.3. フレンドキーワード

7.3.1. 推奨事項 23

フレンドは、クラスの外に定義することが設計上最も望ましい関数から、 クラス内のprivateおよびprotectedなメンバを呼び出すために使用されます。 しばしば、オブジェクトに対する操作は、クラスおよび関数のコレクションによって提供されます。

フレンドは、そのクラスの非publicメンバにアクセスできるクラス外の関数です。 フレンドは、クラスのデータカプセル化を整然と整える方法を提供します。 フレンドクラスは、非publicなメンバへの直接的なアクセスを要する、別のクラスを作成する際に有効です。

クラスを反復処理するために内部リスト要素へのポインタを必要とするリストクラスがあるとします。 このポインタは、リスト上の他の操作には必要ありません。 その場合、現在のリスト要素へのポインタを格納しないリストオブジェクトに対して、 より小さいリストオブジェクトを取得するなどの理由があり、 必要に応じてそのようなポインタを含むイテレータを作成することがあります。

この解決策の1つの問題は、イテレータクラスは通常、 リストを表すために使用されるデータ構造にアクセスできないことです。 (privaetなメンバ変数が推奨されるため)

イテレータクラスをフレンドとして宣言することで、この問題はデータカプセル化の原則に違反することなく回避されます。 フレンドを適切に使用することで、オブジェクト志向の原則と実装のミスマッチを解消することができます。 しかし、フレンドの多用はクラスの独立性の低下に繋がります。

7.4. constメンバ関数

7.4.1. 規約事項 42

オブジェクトの状態(メンバ変数)を変更しないメンバ関数には、constを宣言します。

7.4.2. 規約事項 43

オブジェクトの動作がオブジェクト外部のデータに依存する場合、 そのデータをconstメンバ関数で変更してはいけません。

constとして宣言されたメンバ関数は、メンバデータを変更してはならず、constオブジェクトで呼び出される唯一の関数です。 (constメソッドのないconstオブジェクトは、使い道がありません) const宣言は、オブジェクトが変更されてはならないときに変更(変更)されないという優れた保険です。 C++の大きなメリットは、関数のオーバーロード機能です。 2つのメンバ関数は、一方がconstで他方がconstでない同じ名前を持つことがあります。

非constメンバ関数は、値が格納される場所として、いわゆる “左辺値”として呼び出されることがあります。 constメンバー関数は決して “左辺値”として呼び出されることはありません。

オブジェクトの動作は、オブジェクトの外部のデータの影響を受ける可能性があります。 そのようなデータは、constメンバ関数によって変更されてはなりません。

まれに、constオブジェクトのメンバ変数を変更する必要性が生じることがあります。 そのようなメンバ変数は、mutableとして宣言しなければなりません。

7.5. コンストラクタ・デストラクタ

7.5.1. 規約事項 44

インスタンス変数としてポインタを持つすべてのクラスは、コピーコンストラクタ(および代入演算子)を宣言する必要があります。 コピーコンストラクタはprivateにすることができます。その場合、実装を行わなくても構いません。

7.5.2. 規約事項 45

基本クラスとして使用できるすべてのクラスは、仮想デストラクタを定義する必要があります。

7.5.3. 規約事項 46

クラスに少なくとも1つの仮想関数がある場合は、仮想デストラクタも必要です。

7.5.4. 規約事項 47

すべてのクラスには、デフォルト(引数なし)のコンストラクタとデストラクタが必要です。 デフォルトコンストラクタがプライベートでない限り、デフォルトのコンストラクタに対して 実装(空の場合でも)を提供する必要があります。 デストラクタでは、実装を常に提供する必要があります。

7.5.5. 規約事項 48

デストラクタは例外を送出してはいけません。 C++11では、デストラクタはnoexcept(true)として暗黙指定されています。 したがって、例外を送出するデストラクタは、プログラムを直ちに終了させます。

7.5.6. 推奨事項 24

デストラクタが例外を送出する可能性がある場合、try ... catch(...)ブロックでコードをラップしてください POCOは、スローされる可能性のある、エラーのヒントかもしれない例外を黙って握り潰さないように、 例外ハンドラで使用されるpoco_unexpected()マクロを提供しています。 デバッグビルドでは、標準出力にメッセージが書き込まれ、デバッガで実行されている場合はブレークポイントがトリガされます。

例9
MyClass::~MyClass
{
    try
    {
        cleanup();
    }
    catch (...)
    {
        poco_unexpected();
    }
}

7.5.7. 推奨事項 25

メンバ変数の宣言と初期化は、同じ順序で行います。 また、初期化はコンストラクタで行います。

7.5.8. 推奨事項 26

コンストラクタおよびデストラクタからは、仮想関数を呼び出してはいけません。

7.5.9. 推奨事項 27

コンストラクタおよびデストラクタでは、グローバルオブジェクトの使用を避けてください。

7.5.10. 推奨事項 28

コピーコンストラクタでない単一引数のコンストラクタには、explicitを使用します。 これは、暗黙的な型変換によって引き起こされるあいまいさを回避します。

コピーコンストラクタは、オブジェクトが同じ型のオブジェクトを使用して初期化されるときの驚きを避けるために推奨されます。 オブジェクトがヒープ上のオブジェクトの割り当てと割り当て解除を管理する場合 (管理オブジェクトはクラスのコンストラクタによって作成されるオブジェクトへのポインタを持ちます)、 ポインタの値だけがコピーされます。 これにより、(ヒープ上の)同じオブジェクトに対するデストラクタの2回の呼び出しが発生し、 おそらく実行時エラーが発生する可能性があります。

代入演算子( “=”)に対応する問題が存在します。

バーチャルファンクションを持つクラスが仮想デストラクタを持たないクラスをベースクラスとして使用する場合、 そのクラスへのポインタが使用されると驚くかもしれません。 このようなポインタが派生クラスのインスタンスに割り当てられ、 このポインタでdeleteが使用されると、基本クラスのデストラクタのみが呼び出されます。 プログラムが呼び出される派生クラスのデストラクタに依存する場合、プログラムは失敗します。

静的に割り当てられたオブジェクトの初期化に関連して、他の静的オブジェクト(グローバルオブジェクトなど)が初期化されることは保証されません。 これは、異なるコンパイル単位で定義された静的オブジェクトの初期化の順序が言語定義で定義されていないためです。

メンバ変数の初期化の例を以下に示します。

例10
class Property
{
public:
    Property(const std::string& name, const std::string& value);
    //...
private:
    std::string _name;
    std::string _value;
};
// Good:
Property::Property(const std::string& name, const std::string& value):
_name(name),
_value(value)
{
}
// Bad:
Property::Property(const std::string& name, const std::string& value)
{
    _name = name;
    _value = value;
}

7.6. 代入演算子

7.6.1. 規約事項 49

インスタンス変数としてポインタを持つすべてのクラスは、コピーコンストラクタと共に代入演算子を宣言する必要があります。 代入演算子は何の実装が提供されていない場合には、プライベートでもよいです。

7.6.2. 規約事項 50

破壊的なアクションを実行する代入演算子は、そのオブジェクトの実行中に呼び出されないように保護する必要があります。

7.6.3. 推奨事項 29

代入演算子は、代入するオブジェクトに非const参照を返さなければならない。

7.6.4. 推奨事項 30

可能であれば、代入演算子の実装は、swap()演算を使用して強力な例外安全性を提供しなければならない。 代入は他の演算子のように継承されません。代入演算子が明示的に定義されていない場合は、代入演算子が自動的に定義されます。 このような代入演算子は、メンバーデータのビット単位のコピーを実行しません。 代わりに、メンバーデータの特定のタイプごとの代入演算子(定義されている場合)が呼び出されます。 ビット単位のコピーは、プリミティブ型を有するメンバデータに対してのみ実行される。 その結果、ポインタ型のメンバデータに対してビット単位のコピーが行われる。 オブジェクトがポインター・メンバーが指すオブジェクトのインスタンスの割り振りを管理する場合、 これはおそらく問題につながります。つまり、管理対象オブジェクトのデストラクタを2回以上呼び出すか、 割り振り解除されたオブジェクトを使用しようとします。 代入演算子がオーバーロードされている場合、プログラマは、基本クラスおよびメンバー代入演算子が実行されていることを確認する必要があります。 一般的なエラーは、オブジェクトをそれ自身に割り当てることです。通常、ヒープに割り当てられたインスタンスのデストラクタは、 代入が行われる前に呼び出されます。オブジェクトが自身に割り当てられている場合、インスタンス変数の値は割り当てられる前に失われます。 これにより、ランタイムエラーが発生する可能性があります。

swap関数を使用した代入演算子の実行例を以下に示します。

例11
Foo& Foo::operator = (const Foo& foo)
{
    Foo tmp(foo);
    swap(tmp);
    return *this;
}

7.7. 演算子のオーバーロード

7.7.1. 推奨事項 31

演算子のオーバーロードは乱用すべきではありません。 使用する場合は、一貫した規約を遵守してください。

7.7.2. 推奨事項 32

反対の意味を持つ演算子がある場合、(==に対する!=など)両方を実装してください。

7.7.3. 推奨事項 33

演算子&&,||,および.(カンマ)はオーバーロードしてはいけません。

7.7.4. 推奨事項 34

キャスト演算子をオーバーロードしないでください。 演算子のオーバーロードには長所と短所の両方があります。 1つの利点は、オーバーロードされた演算子を持つクラスを使用するコードを、 よりコンパクトに(より読みやすく)書くことができることです。 もう1つの利点は、シンプルで自然なセマンティクスを実現できるということです。 演算子をオーバーロードする際の1つの欠点は、 演算子に対して通常連想できないような実装がオーバーロードされている場合、 演算子が誤用されることです。 極端な例ですが、プラス演算子がマイナスを意味し、マイナス演算子がプラスを意味するように再定義された場合、 ひどい結果を生むでしょう。 クラスライブラリを設計することは、言語を設計することと同じです! 演算子のオーバーロードを使用する場合は、一貫した規約に基づいて使用してください。 誤解を招きやすい場合は使用すべきではありません。

7.8. メンバ関数の戻り値

7.8.1. 規約事項 51

パブリックメンバ関数は、メンバデータへの非constな参照またはポインタを返さないでください。 パブリックメンバ関数は、オブジェクトが他のオブジェクトとデータを共有していない限り、 オブジェクト外部に、データへの非constな参照またはポインタを返さないでください。 ユーザーがオブジェクトのprivateメンバーデータに直接アクセスできると、 クラスの設計者が意図していない方法でデータが変更される可能性があります。 これにより、設計者のコードの信頼性が低下する可能性があります。回避すべき状況です。

7.9. 継承

7.9.1. 推奨事項 35

基底クラスと、派生クラスが is one ofの関係にない限り、継承を避けてください。 継承ではなくコンポジションで解決すべきです。

7.9.2. 推奨事項 36

派生クラスから基底クラスのデータメンバにアクセスさせたい場合、 基底クラスにprotectedなアクセサを実装してください。

部分的な関係に複数の継承を使用するのはよくある間違いです (オブジェクトが複数の他のオブジェクトで構成される場合、インスタンス変数を使用する代わりに継承されます)。 C ++では、指定された型の任意の数のインスタンスが存在する可能性があります。 継承が使用されている場合、クラスからの直接継承は一度しか使用できません。

有用なメンバ関数を作成するために、派生クラスは基本クラスメンバデータへのアクセスを必要とすることがよくあります。 保護されたメンバ関数を使用する利点は、基本クラスメンバデータの名前が派生クラスで見えず、したがって変更できることです。 このようなアクセス関数は、メンバーデータの値(読み取り専用アクセス)のみを返す必要があります。 指針は、継承を使用する人は、このデータを名前で参照するのではなく、 プライベートメンバーデータを正しく使用できるように基本クラスについて十分に知っているということです。 これにより、基底クラスと派生クラスの結合が減少します。