抽象クラスとインターフェース | Programming Place Plus C++編【言語解説】 第29章

トップページC++編

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++編を作成中です。

この章の概要

この章の概要です。


抽象クラス

第27章で解説した仮想関数を使うと、処理の内容を派生クラスの側で書き換えることができました(オーバーライド)。これは便利な機能ですが、微妙な問題もあります。

それは、基底クラス側でも仮想関数の中身を用意しなければならないことです。基底クラスが、それ単体でも意味があるのならば問題にはならないのですが、継承して使うことが前提のようなクラスの場合、基底クラス側に何かしらの処理を記述することが難しくなります。

仕方がないので、基底クラス側では assertabort、例外(第31章)などでごまかすという手を取ることがありますが、これらの方法では、実行時にならないと問題が検出できません。できるだけ、コンパイルやリンクの時点で、問題を検出したいのです。

そこで、中身がない仮想関数を作る機能が利用できます。このような仮想関数を、純粋仮想関数と呼び、次のように使用します。

class Base {
private:
    virtual void func() = 0;
};

class Derived : public Base {
private:
    virtual void func() {}
};

純粋仮想関数は、virtual指定子を使うことは仮想関数と同様ですが、宣言の末尾に = 0 を付ける点が異なります。オーバーライドするときは = 0 は付けません。

変な構文ですが、「中身がない」=「ヌル (NULL)」と考えると、0 を使うことのニュアンスが分かるでしょうか? だからといって、NULL を使うことはできません。

【上級】さらに変な話に思えるかもしれませんが、純粋仮想関数の中身を書くことは、実は可能です。それが必要になることはまずないので取り上げませんが、次の項で見る純粋仮想デストラクタは、たまに使う機会があります。

純粋仮想関数を含んだクラスを、抽象クラスと呼びます。]抽象クラスは、インスタンス化できません。抽象クラスには定義がない関数(純粋仮想関数)が含まれているため、インスタンス化するには不完全な状態であるためです。抽象クラスに対して、インスタンス化できるクラスのことを具象クラスと呼ぶことがあります。

int main()
{
    Base b1;                   // エラー。抽象クラスはインスタンス化できない
    Base* b2 = new Base();     // エラー。抽象クラスはインスタンス化できない
    Base* b3 = new Derived();  // OK。インスタンス化したのは具象クラスである
}

もう少し具体的な例として、第27章で登場した、Penクラスを改造してみます。

class Pen {
public:
    // 色を表す型
    // RGB(赤・緑・青)をそれぞれ 16進数2桁 (0x00~0xff) で表し、
    // 0xff8000 のように指定する(この場合、R=0xff, G=0x80, B=0x00)
    typedef unsigned int Color_t;

public:
    explicit Pen(Color_t color) :
        mColor(color)
    {}

    virtual ~Pen()
    {}

    void DrawLine(int x1, int y1, int x2, int y2)
    {
        graphics::BeginDraw();
        DrawLineInner(x1, y1, x2, y2);
        graphics::EndDraw();
    }

protected:
    inline Color_t GetColor() const
    {
        return mColor;
    }

private:
    virtual void DrawLineInner(int x1, int y1, int x2, int y2) = 0;

private:
    Color_t  mColor;
};

class SolidPen : public Pen {
public:
    explicit SolidPen(Color_t color) :
        Pen(color)
    {}

protected:
    virtual void DrawLineInner(int x1, int y1, int x2, int y2)
    {
        // GetColor() の色を使って、(x1,y1) から (x2,y2) に向かって実線を描く
    }
};

class DotPen : public Pen {
public:
    explicit DotPen(Color_t color) :
        Pen(color)
    {}

protected:
    virtual void DrawLineInner(int x1, int y1, int x2, int y2)
    {
        // GetColor() の色を使って、(x1,y1) から (x2,y2) に向かって点線を描く
    }
};


int main()
{
//    Pen* pen1 = new Pen(0x000000);
    Pen* pen2 = new SolidPen(0x000000);
    Pen* pen3 = new DotPen(0x000000);

//    pen1->DrawLine(0, 0, 50, 50);
    pen2->DrawLine(20, 0, 70, 50);
    pen3->DrawLine(40, 0, 90, 50);
}

やりたいことは、Pen というクラスを抽象的(空想的、想像上のもの、など表現は何でも構いませんが、とにかく実在しないもの)な存在にするということです。そのためには、第27章のプログラムの Penクラスのように、実線を描く具体的なコードが含まれているのはおかしいと考えることもできます。

つまり、「Pen とは、(線種を問わず)直線を描くことができる仮想的な存在である」と考えたいので、線種に具体性を持たせたくない訳です。そこで、直線を描く処理にあたる DrawLineInnerメンバ関数を、純粋仮想関数にして、具体的な部分を派生クラスに任せます。

純粋仮想デストラクタ

稀な話ではありますが、抽象クラスにしたいが、純粋仮想関数にできるメンバ関数が1つもないという状況があり得ます。その場合は、無理に仮想関数をひねり出すよりも、デストラクタを純粋仮想にするのが得策です。このようなデストラクタは、純粋仮想デストラクタと呼ばれます。

宣言の方法は、通常のメンバ関数を純粋仮想関数にする場合と同様で、virtual指定子と、末尾の「= 0」を付けます。ただし、純粋仮想デストラクタは、中身は空でも良いので、定義を書かなければなりません

class Base {
public:
    virtual ~Base() = 0;
};

Base::~Base()
{
}

ちなみに、次のように一緒に書いてしまうことはできません。

class Base {
public:
    // エラー
    virtual ~Base() = 0
    {}
};

インターフェース

純粋仮想関数だけで構成されたクラスを、インターフェースと呼ぶことがあります。特別、C++ にそのような機能があるわけではなく、抽象クラスの特別な形ということです。

インターフェースは、抽象クラスから純粋仮想関数以外のメンバ関数を廃し、メンバ変数も無いものですが、さらに言い換えれば、の定義を持たず、宣言だけで構成されているクラスとも考えられます。つまり、具体的な定義は派生クラスに任されており、共通の型を提供するためだけにあるのが、インターフェースです

抽象クラスの場合だと、具体的な中身のあるメンバ関数を持つことができるので、派生クラスで共通して使いたい処理を、「限定公開」の形で用意しておけば、派生クラスの実装は簡単になります。

また、メンバ変数を持っているので、これもやはり、派生クラスで共通で使いたい情報を置いておけます(メンバ変数の場合は、「限定公開」ではなくて、つねに「非公開」にすべきであることは注意してください)。

このような、抽象クラスとの違いによって、当然、使い道にも違いが生まれます。抽象クラスは、公開継承して使うのであれば、結局のところ is-a関係を構築する機能です。一方、インターフェースは、can-do関係と呼ばれる関係性を構築します。これは「Derived can X」の関係性で、X にはメンバ関数の名前が入ると考えてください。つまり、インターフェースは、そこから派生したクラスが「何ができるか」を表現するものです。

抽象クラスのところで取り上げた Penクラスの例を、インターフェースに置き換えて考えてみましょう。インターフェースの場合、「何ができるか」という考え方なので、そもそも「Pen」という物体の名前を使うのはふさわしくありません。「何ができるか」=>「直線を描ける」というのであれば、LineDrawer のような名称の方がそれらしいでしょう。

そのままインターフェースの名前として採用してもいいですが、よくある命名規約として、インターフェースの名前の先頭に、Interface を表す「I」を付けるというものがあります。これを採用して「ILineDrawer」と命名します。

class ILineDrawer {
public:
    // 色を表す型
    // RGB(赤・緑・青)をそれぞれ 16進数2桁 (0x00~0xff) で表し、
    // 0xff8000 のように指定する(この場合、R=0xff, G=0x80, B=0x00)
    typedef unsigned int Color_t;

public:
    virtual ~ILineDrawer()
    {}

    virtual void DrawLine(int x1, int y1, int x2, int y2) = 0;
};

class SolidPen : public ILineDrawer {
public:
    explicit SolidPen(Color_t color) :
        mColor(color)
    {
    }

    virtual void DrawLine(int x1, int y1, int x2, int y2)
    {
        graphics::BeginDraw();
        // mColor の色を使って、(x1,y1) から (x2,y2) に向かって実線を描く
        graphics::EndDraw();
    }

private:
    Color_t  mColor;
};

class DotPen : public ILineDrawer {
public:
    explicit DotPen(Color_t color) :
        mColor(color)
    {}

    virtual void DrawLine(int x1, int y1, int x2, int y2)
    {
        graphics::BeginDraw();
        // mColor の色を使って、(x1,y1) から (x2,y2) に向かって点線を描く
        graphics::EndDraw();
    }

private:
    Color_t  mColor;
};


int main()
{
//    ILineDrawer* pen1 = new ILineDrawer(0x000000);
    ILineDrawer* pen2 = new SolidPen(0x000000);
    ILineDrawer* pen3 = new DotPen(0x000000);

//    pen1->DrawLine(0, 0, 50, 50);
    pen2->DrawLine(20, 0, 70, 50);
    pen3->DrawLine(40, 0, 90, 50);
}

インターフェースになったことで、メンバ変数 mColor は、それぞれの派生クラスに移動されました。これは、コードが重複するようになってしまったという見方もできますし、一方で、色に関しての裁量が派生クラスに任されたことで自由度が増したとも言えます。たとえば、色を複数持ったペンを作ることも容易にできます。

抽象クラスからインターフェースに変わったため、もはや is-a関係ではなくなりました。そのため、「ペン」である必要性も無くなったことは注目すべき点です。ペンでも、筆でも、コンピュータのペイントソフトの機能でも、校庭に線を引くライン引きでも、とにかく「直線を描ける」のなら何でも、ILineDrawerインターフェースから派生させられます。

インターフェースに変わっても、そこから派生クラスを定義する方法として、「公開継承」を選択したのなら、インターフェース型のポインタや参照を使って、派生クラスのインスタンスを扱えます。


練習問題

問題① 「飛ぶ (Fly)」という動作に注目して、「鳥 (Bird)」のクラスを定義してください。

問題② 「飛ぶ (Fly)」という動作は、鳥だけでなく、飛行機などにも適用できます。そのような観点で、インターフェースを設計してください。


解答ページはこちら

参考リンク


更新履歴

’2016/4/9 新規作成。



前の章へ (第28章 継承と合成)

次の章へ (第30章 多重継承)

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

Programming Place Plus のトップページへ



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