C++編で扱っている C++ は 2003年に登場した C++03
という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14
-> C++17 -> C++20 -> C++23 と更新されています。
なかでも 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:
* mPen;
Pen};
この場合、「Student は Pen を持っている」という関係性ですが、このクラスの他の部分の作りによっては、 この Pen の管理責任を Student自身が持っているのか、Student以外の誰かが持っているのか、分かれるところです。たとえば、そのペンが生徒の私物であるのなら前者でしょうし、貸し与えられたものなら後者かもしれません。
公開継承だと、基底クラスの「公開」「限定公開」のメンバへの依存性が生まれますが、合成の場合は、合成されるクラスの「公開」メンバへの依存性のみが生まれます。このように、合成の方が依存性を少なくできるので、一般的に、公開継承よりも合成を優先して使うべきとされています。
このような観点で考えると、相手クラスの「公開」「限定公開」「非公開」のすべてのメンバへアクセスできる、フレンド関係(第25章)が、もっとも依存性が高い関係性であると言えます。もちろん、もっとも避けるべき関係性です。
実は 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()
{
(); // 「公開」は OK
f1(); // 「限定公開」は OK
f2(); // 「非公開」なのでエラー
f3}
};
int main()
{
;
Derived d
.g();
d.f1(); // 基底クラス側で「公開」だが、非公開継承なのでエラー
d.f2(); // 基底クラス側で「限定公開」。当然、エラー
d.f3(); // 基底クラス側で「非公開」。当然、エラー
d}
通常の合成との大きな違いとして、以下の2点あります。
これらのいずれかの特性が必要になる場面では、非公開継承を活用できますが、逆に、これらが必要なければ、普通の合成を使うべきです。
派生クラスからアクセスできる範囲は、公開継承でも非公開継承でも違いがないので、「合成」の項で説明したとおり、依存性が強いことにも違いはありません。
非公開継承は、is-a関係にならないことをあらためて注意してください。意味としては合成に他ならないので、結ばれる関係性は has-a関係や is-implemented-in-terms-of関係になります。実際、非公開継承では、派生クラスのオブジェクトを、基底クラスの型で扱うことができません。
class Base {};
class Derived : private Base {};
void func(Base* b) {}
int main()
{
;
Derived d(&d); // エラー
func
* b = new Derived(); // エラー
Base}
publicキーワードを使った公開継承、privateキーワードを使った非公開継承とくれば、protectedキーワードを使った限定公開継承(protected継承)もあります。限定公開継承が一番使いどころが難しく、正直かなり分かりづらい機能です。
限定公開継承では、基底クラス側の「公開」のメンバが、派生クラス側では「限定公開」扱いになります。それ以外のメンバのアクセス指定は、そのまま引き継がれます。したがって、外部からは基底クラスのメンバにはアクセスできません(フレンドを除く)。
派生クラスからは、基底クラスの「公開」と「限定公開」のメンバにアクセスできます。この点は、どのタイプの継承でも同様です。
限定公開継承の最大のポイントは、派生クラスのそのまた派生クラスからは、基底クラスの「公開」「限定公開」のメンバにアクセスできる点です。
非公開継承の場合、基底クラスの「限定公開」メンバは、派生クラスで「非公開」にされてしまうため、さらなる派生クラスを公開継承で作ったとしても、そのメンバはもう「非公開」なので、アクセスできませんが、限定公開継承なら、「限定公開」のままなので、アクセスできるという訳です。
この複雑な特徴を言い換えると、外部からのアクセスを防ぎつつ(公開継承と異なる点)、さらなる派生クラスで「限定公開」メンバへアクセスすることを許す(非公開継承と異なる点)ということです。
class Base {
public:
void f1() {}
protected:
void f2() {}
private:
void f3() {}
};
class Derived : protected Base {
public:
void g()
{
(); // 「公開」は OK
f1(); // 「限定公開」は OK
f2(); // 「非公開」なのでエラー
f3}
};
class Derived2 : public Derived {
public:
void g2()
{
(); // Base の「公開」は、ここでは「限定公開」なので OK
f1(); // Base の「限定公開」は、ここでも「限定公開」なので OK
f2(); // Base の「非公開」は、ここでも「非公開」なのでエラー
f3}
};
int main()
{
;
Derived2 d2
.g2();
d2.f1(); // 基底クラス側で「公開」だが、限定公開継承なのでエラー
d2.f2(); // 基底クラス側で「限定公開」。当然、エラー
d2.f3(); // 基底クラス側で「非公開」。当然、エラー
d2}
限定公開継承が構築する関係性は、非公開継承と同じく has-a関係や is-implemented-in-terms-of関係になります。やはり、is-a関係ではないことに注意してください。
class Base {};
class Derived : protected Base {};
void func(Base* b) {}
int main()
{
;
Derived d(&d); // エラー
func
* b = new Derived(); // エラー
Base}
問題① 第26章の練習問題で、標準ライブラリの bitset(【標準ライブラリ】第13章)に、すべてのビットが 1 になっているかどうかを判定するメンバ関数を、公開継承を使って追加するという題材を扱いました。同様のことを、普通の合成、非公開継承、限定公開継承のそれぞれを使って行ってください。
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |