お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

抽象基本クラス(Abstract Base Class : ABC)の復習

ファクトリーメソッドを復習しようと思ったら、抽象基本クラスの復習をする羽目になったので、 調べて学んだことをメモ書きします。

抽象クラスとは

概念的なもの。このクラスから実態を持つことは出来ないが、抽象クラス型へのポインタと参照は使用できる。だそうです。

抽象クラス (C++)

実態を持たないので、継承しないと意味がありません。なので、抽象基本クラスとも言うそうです(自分が読んだ本には、抽象基本クラスと書いてあった)。今後、抽象基本クラスは ABC(Abstract Base Class) と表記します。

じゃあ、どういうものが ABC かというと、純粋仮想メンバ関数を持つクラスのことを ABC といいます。純粋仮想メンバ関数というのは、以下の様なものです。

virtual void Method() = 0;

純粋仮想メンバは、継承先で必ずオーバーライドしないといけません。つまり継承先で必ず実装しないといけません。ですが、ABC 側で実装をしてはいけないという意味ではありません。Method().cpp ファイルで実装するとコンパイルは通り、呼ぶことが可能です。

ABC は実態を持つことは出来ませんが、ABC 型へのポインタを作ることが可能です。
このポインタを使って、ABC を継承した様々なクラスのインスタンスへのポインタを入れることが出来ます。つまり、以下のように書きます。

IBase *instance = new Derived();

IBase は、ABC です。Derived は、 IBase を継承したクラスです。
instance->method() のように使えば、継承したインスタンスを呼ぶことが出来ます。上記は、Derived のみですが、他に IBase を継承したクラスがあれば、同じように使うことが出来ます。

コンストラクタ、デストラクタ

コンストラクタを仮想化することは出来ませんが、デストラクタは可能です。 つまり以下のように書けます。

virtual ~IBase();

これは、ABC で必ず必要になります。ABC は、実態を持つことはないので、必ず継承されて使われます。しかし、書かなくてもコンパイルは通ります。このときコンパイラは、デフォルトデストラクタを作成しますが、この状態で ABC を継承した Derived 型のポインタを delete すると、大変なことが起こります。コードで示すと以下のようになります(コンパイルは通らないかもしれません……)。

// 抽象基本クラス
class IBase
{
public:
    virtual void Method() = 0;
};
// 抽象基本クラスを継承したクラス
class Derived : public IBase
{
public:
    ~Derived(){};
    void Method() {};
};

int main()
{
    IBase *instance = new Derived();
    instance->Method();
    delete instance;
    
    return 0;
}

このとき、delete で呼ばれるデストラクタは、IBase のデフォルトデストラクタです。継承先のデストラクタは呼ばれません。どうしてかは詳しく説明できないですが、IBase のデストラクタが実態を持っていることが原因であることは分かると思います。 ですので、抽象基本クラスを作るときは、必ずデストラクタに virtual を付けるように気を付けましょう。

キーワード:override

デストラクタには virtual を付けるように気をつけましょうと言いましたが、このミスをコンパイル時に見つける方法があります。それが、override 指定子です。
こいつを付けることで、コンパイラに「今からオーバーライドするよ」と教えることが出来ます。つまり以下のようになります。

// 抽象基本クラスを継承したクラス
class Derived : public IBase
{
public:
    ~Derived() override {};
    void Method() {};
};

~Derived() の後に override を付けることで、コンパイル時にエラーが発生するようになります(Visual Studio 2017 だとコンパイルする前に赤い波線が出ました)。
もちろんデストラクタだけでなく、メソッドにも付けることが可能です。オーバーライドをするときには、必ず override を付けるようにしましょう。

override 指定子

キーワード:final

今度は、逆にオーバーライドをして欲しくないときの対処法です。override と同じようにメソッドの後ろに final を付けます。以下のようになります。

virtual int value() final { return value_; };

このように書くことで、継承先がオーバーライドをしようとすると、コンパイラはエラーを発します。処理が今後変わることがないときは、 final を付けるようにしましょう。

final 指定子

キーワード:delete

最後に delete を紹介します。これは、delete 演算子ではありません。何と言うのかちょっと調べただけでは分からないですが、このキーワードを使うことで、メソッドやコンストラクタ、デストラクタを削除することが出来ます。 以下のように書きます。

public:
IBase() = delete;

public 側で書くことをお勧めします。これはコンパイラが delete であるかどうかよりもアクセスの可否を先に見るためです。private で delete されたメソッドをユーザが使おうとすると メソッドが private であることだけをエラーとして吐くそうです。また他にも利点があるのですが、ここでのメインの話ではないので省きます。詳細は、Effective Modern C++ の項目 11 を見てください。

Effective Modern C++ ―C++11/14プログラムを進化させる42項目

Effective Modern C++ ―C++11/14プログラムを進化させる42項目

おわり

以上、抽象基本クラスの復習でした。 C++11 から使えるようになったキーワードを混ぜることで、安全に継承をすることが出来るようになりそうです。

ここで記載したコードは、以下に上げています。

github.com

最後に、参考にした(復習する羽目になった)書籍を貼っておきます。

C++のためのAPIデザイン

C++のためのAPIデザイン