継承と合成 | Programming Place Plus C++編【言語解説】 第28章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


合成

第26章第27章と続けて継承に関する話題を扱いましたが、本章では、いったん継承から離れて、別の概念を説明した後、再び継承の話題に戻ります。

継承は強力な機能ではありますが、少々強力過ぎるという指摘もあります。問題なのは、継承は、基底クラスに対する依存性が非常に高いことです。派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできる訳ですから、それらのメンバに依存するコードを書けるということになります。そのため、派生クラスが作られた後では、基底クラスの「公開」や「限定公開」のメンバを変更することが容易では無くなる可能性があります。

そこで、もう少し緩やかな関係性を構築する手法が、合成(コンポジション、包含)です。合成は、C++ の言語機能というわけではなく、設計手法の領域になりますが、重要なので取り上げておきます。

合成は、あるクラスが別のクラス(のオブジェクト)を保持している、あるいは使用して実装しているという関係性です。公開継承の関係性を is-a関係と呼んでいたように、前者の場合を has-a関係(AはBを持っている)、後者の場合を is-implemented-in-terms-of関係(AはBを使用して実装されている)と呼びます。しかしながら、形としては特に変わりはありません。

実のところ、合成はこれまでにも、あまり意識せずに使っていると思います。たとえば、メンバ変数として std::string を使っていれば、そのクラスは std::string のオブジェクトを使って実装されている訳ですから、合成の一種だと言えます。

class Student {
private:
    std::string  mName;
};

この場合、「Student は string を持っている」という関係性になります。合成という概念は言ってしまえば、これだけのことです。

また、合成される側のオブジェクトが実体であっても、ポインタや参照であっても、合成の形にはなっており、同じことです。ただし、ポインタや参照の場合は、その実体がどこか別のクラスや、クラスの外にあるかも知れず、管理責任の有無で、違いがあるとも言えます。

class Student {
private:
    Pen*  mPen;
};

この場合、「Student は Pen を持っている」という関係性ですが、このクラスの他の部分の作りによっては、 この Pen の管理責任を Student自身が持っているのか、Student以外の誰かが持っているのか、分かれるところです。たとえば、そのペンが生徒の私物であるのなら前者でしょうし、貸し与えられたものなら後者かもしれません。

公開継承だと、基底クラスの「公開」「限定公開」のメンバへの依存性が生まれますが、合成の場合は、合成されるクラスの「公開」メンバへの依存性のみが生まれます。このように、合成の方が依存性を少なくできるので、一般的に、公開継承よりも合成を優先して使うべきとされています。

このような観点で考えると、相手クラスの「公開」「限定公開」「非公開」のすべてのメンバへアクセスできる、フレンド関係(第25章)が、もっとも依存性が高い関係性であると言えます。もちろん、もっとも避けるべき関係性です。


非公開継承 (private継承)

実は C++ では、合成を、継承の形で表現することが可能です。これはかなり異質で、C++以外の言語ではあまり見かけない形です。

合成を継承の形で表現するには、非公開継承(private継承)を行います。これまでに登場した、公開継承は、基底クラスを指定する際に publicキーワードを用いていましたが、ここを private に変えるだけです。

class クラス名 {};
class クラス名 : private 基底クラス名 {};

class の場合であれば、この場面での privateキーワードは省略でき、同じ意味になります。struct の場合は、省略すると「公開継承」になるので、同じ意味にはなりません。

class Base {};
class Derived1 : Base {};  // 非公開継承

struct Derived2 : Base {};  // 公開継承

非公開継承では、基底クラス側のすべてのメンバが、派生クラス側では「非公開」扱いになります。したがって、外部からは基底クラスのメンバにはアクセスできません(フレンドを除く)。

派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできます。この点は、公開継承の場合と同様です。

何が OK で、何がエラーになるのか確認しておきましょう。

class Base {
public:
    void f1() {}

protected:
    void f2() {}

private:
    void f3() {}
};

class Derived : private Base {
public:
    void g()
    {
        f1();  // 「公開」は OK
        f2();  // 「限定公開」は OK
        f3();  // 「非公開」なのでエラー
    }
};

int main()
{
    Derived d;

    d.g();
    d.f1();  // 基底クラス側で「公開」だが、非公開継承なのでエラー
    d.f2();  // 基底クラス側で「限定公開」。当然、エラー
    d.f3();  // 基底クラス側で「非公開」。当然、エラー
}

通常の合成との大きな違いとして、以下の2点あります。

これらのいずれかの特性が必要になる場面では、非公開継承を活用できますが、逆に、これらが必要なければ、普通の合成を使うべきです。

派生クラスからアクセスできる範囲は、公開継承でも非公開継承でも違いがないので、「合成」の項で説明したとおり、依存性が強いことにも違いはありません。

非公開継承は、is-a関係にならないことをあらためて注意してください。意味としては合成に他ならないので、結ばれる関係性は has-a関係や is-implemented-in-terms-of関係になります。実際、非公開継承では、派生クラスのオブジェクトを、基底クラスの型で扱うことができません

class Base {};
class Derived : private Base {};

void func(Base* b) {}

int main()
{
    Derived d;
    func(&d);  // エラー

    Base* b = new Derived();  // エラー
}

限定公開継承 (protected継承)

publicキーワードを使った公開継承、privateキーワードを使った非公開継承とくれば、protectedキーワードを使った限定公開継承(protected継承)もあります。限定公開継承が一番使いどころが難しく、正直かなり分かりづらい機能です。

限定公開継承では、基底クラス側の「公開」のメンバが、派生クラス側では「限定公開」扱いになります。それ以外のメンバのアクセス指定は、そのまま引き継がれます。したがって、外部からは基底クラスのメンバにはアクセスできません(フレンドを除く)。

派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできます。この点は、どのタイプの継承でも同様です。

限定公開継承の最大のポイントは、派生クラスのそのまた派生クラスからは、基底クラスの「公開」「限定公開」のメンバにアクセスできる点です。

非公開継承の場合、基底クラスの「限定公開」メンバは、派生クラスで「非公開」にされてしまうため、さらなる派生クラスを公開継承で作ったとしても、そのメンバはもう「非公開」なので、アクセスできませんが、限定公開継承なら、「限定公開」のままなので、アクセスできるという訳です。

この複雑な特徴を言い換えると、外部からのアクセスを防ぎつつ(公開継承と異なる点)、さらなる派生クラスで「限定公開」メンバへアクセスすることを許す(非公開継承と異なる点)ということです。

class Base {
public:
    void f1() {}

protected:
    void f2() {}

private:
    void f3() {}
};

class Derived : protected Base {
public:
    void g()
    {
        f1();  // 「公開」は OK
        f2();  // 「限定公開」は OK
        f3();  // 「非公開」なのでエラー
    }
};

class Derived2 : public Derived {
public:
    void g2()
    {
        f1();  // Base の「公開」は、ここでは「限定公開」なので OK
        f2();  // Base の「限定公開」は、ここでも「限定公開」なので OK
        f3();  // Base の「非公開」は、ここでも「非公開」なのでエラー
    }
};

int main()
{
    Derived2 d2;

    d2.g2();
    d2.f1();  // 基底クラス側で「公開」だが、限定公開継承なのでエラー
    d2.f2();  // 基底クラス側で「限定公開」。当然、エラー
    d2.f3();  // 基底クラス側で「非公開」。当然、エラー
}

限定公開継承が構築する関係性は、非公開継承と同じく has-a関係や is-implemented-in-terms-of関係になります。やはり、is-a関係ではないことに注意してください。

class Base {};
class Derived : protected Base {};

void func(Base* b) {}

int main()
{
    Derived d;
    func(&d);  // エラー

    Base* b = new Derived();  // エラー
}


練習問題

問題① 第26章の練習問題で、標準ライブラリの bitset(【標準ライブラリ】第13章)に、すべてのビットが 1 になっているかどうかを判定するメンバ関数を、公開継承を使って追加するという題材を扱いました。同様のことを、普通の合成、非公開継承、限定公開継承のそれぞれを使って行ってください。


解答ページはこちら

参考リンク


更新履歴

’2016/4/2 新規作成。



前の章へ (第27章 仮想関数)

次の章へ (第29章 抽象クラスとインターフェース)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る