Menu
Coddy logo textTech

仮想関数の再考

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 58/104。

コンパイル時ポリモーフィズムと実行時ポリモーフィズムの違いを理解したところで、仮想関数と、それらをより安全に使用できるようにする override キーワードについて詳しく見ていきましょう。

基底クラスで関数を virtual とマークすると、派生クラスで独自の定義を提供できるようになります。override 指定子は、仮想関数をオーバーライドする意図があることをコンパイラに明示的に伝えます。

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

override を使用することは、コンパイル時にエラーをキャッチできるため非常に重要です。関数名を誤って入力したり、間違ったパラメータを使用したりした場合、コンパイラは黙って新しい関数を作成するのではなく、警告を表示します:

class Cat : public Animal {
public:
    void speek() override {  // コンパイルエラー:オーバーライドする関数がありません
        std::cout << "Meow!" << std::endl;
    }
};

final 指定子は、それ以上のオーバーライドを禁止します。派生クラスが特定の関数をオーバーライドする最後のものである必要がある場合に使用します:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // エラー: final 関数をオーバーライドできません
};

仮想関数をオーバーライドするときは、常に override を使用してください。これにより、意図が明確になり、実際に既存の仮想関数をオーバーライドしているかどうかをコンパイラが検証できるようになります。

challenge icon

チャレンジ

簡単

仮想関数における overridefinal キーワードの威力を実証する通知システムを構築しましょう。通知ハンドラーの階層を作成し、一部のメソッドはさらにカスタマイズ可能にし、他のメソッドは変更を防ぐためにロックダウンします。

コードは3つのファイルに分けて構成します:

  • Notifier.h: あらゆる通知送信者を表すベースクラス Notifier を定義します:
    • protected メンバ std::string recipient
    • 受信者名を受け取り保存するコンストラクタ
    • Notifying <recipient>: <message> と出力する仮想メソッド send(const std::string& message)
    • 文字列 "Generic" を返す仮想メソッド getType()
    • 仮想デストラクタ
  • EmailNotifier.h: Notifier を継承する EmailNotifier クラスを定義します:
    • private メンバ std::string domain
    • 受信者とドメインを受け取り、受信者をベースクラスに渡すコンストラクタ
    • send() をオーバーライドして Emailing <recipient>@<domain>: <message> と出力するようにします
    • getType() をオーバーライドし、final を指定します。これは "Email" を返す必要があります

    次に、EmailNotifier を継承する UrgentEmailNotifier クラスを定義します:

    • 受信者とドメインを受け取り、両方を EmailNotifier に渡すコンストラクタ
    • send() をオーバーライドして [URGENT] Emailing <recipient>@<domain>: <message> と出力するようにします
    • 注意:EmailNotifierfinal が指定されているため、ここでは getType() をオーバーライドすることはできません
  • main.cpp: 3つの入力(それぞれ別々の行)を読み込みます:
    1. 受信者名
    2. メールのドメイン
    3. メッセージ本文

    3つの通知オブジェクトを動的に作成します:ベースの NotifierEmailNotifier、および UrgentEmailNotifier。すべて同じ受信者(および該当する場合はドメイン)を使用します。これらを Notifier* ポインタの配列に格納します。

    配列をループし、各通知オブジェクトについて getType() を使用してそのタイプを出力し、次にメッセージを指定して send() を呼び出します。各エントリを以下の形式で出力してください:

    Type: <type>
    <send output>

    各通知オブジェクトの間に空行を出力してください。終了時には、動的に割り当てたオブジェクトをクリーンアップしてください。

例えば、入力が Alicecompany.comMeeting at 3pm の場合:

Type: Generic
Notifying Alice: Meeting at 3pm

Type: Email
Emailing Alice@company.com: Meeting at 3pm

Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pm

UrgentEmailNotifiersend() をオーバーライドしてメッセージ形式をカスタマイズできている一方で、getType()final が指定されていたため、EmailNotifier から "Email" タイプを継承していることに注目してください。コンパイル時にシグネチャの不一致を検出できるよう、すべてのオーバーライドされたメソッドに override キーワードを使用してください。

チートシート

override 指定子は、基底クラスの仮想関数をオーバーライドする意図があることを明示的にコンパイラに伝えます。

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

override を使用すると、コンパイル時にエラーを検出できます。関数名を間違えたり、引数が異なっていたりする場合、コンパイラが通知します。

class Cat : public Animal {
public:
    void speek() override {  // コンパイルエラー:オーバーライドする関数がありません
        std::cout << "Meow!" << std::endl;
    }
};

final 指定子は、派生クラスでのそれ以上のオーバーライドを禁止します。

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // エラー:final 関数をオーバーライドできません
};

仮想関数をオーバーライドする際は、意図を明確にし、コンパイラによる検証を有効にするために、常に override を使用するようにしてください。

自分で試してみよう

#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"

using namespace std;

int main() {
    // 入力を読み込む
    string recipient;
    string domain;
    string message;
    
    getline(cin, recipient);
    getline(cin, domain);
    getline(cin, message);
    
    // TODO: 3つの要素を持つ Notifier* ポインタの配列を作成する
    
    // TODO: 以下を動的に作成する:
    // - recipient を持つベースの Notifier
    // - recipient と domain を持つ EmailNotifier
    // - recipient と domain を持つ UrgentEmailNotifier
    
    // TODO: 配列をループで回し、各通知オブジェクトに対して以下を行う:
    // - "Type: " に続いて getType() の結果を出力する
    // - message を引数にして send() を呼び出す
    // - 通知オブジェクトの間に空行を出力する(最後の要素の後には出力しない)
    
    // TODO: 動的に割り当てられたオブジェクトをクリーンアップする
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン