仮想関数の再考
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 を使用してください。これにより、意図が明確になり、実際に既存の仮想関数をオーバーライドしているかどうかをコンパイラが検証できるようになります。
チャレンジ
簡単仮想関数における override と final キーワードの威力を実証する通知システムを構築しましょう。通知ハンドラーの階層を作成し、一部のメソッドはさらにカスタマイズ可能にし、他のメソッドは変更を防ぐためにロックダウンします。
コードは3つのファイルに分けて構成します:
Notifier.h: あらゆる通知送信者を表すベースクラスNotifierを定義します:- protected メンバ
std::string recipient - 受信者名を受け取り保存するコンストラクタ
Notifying <recipient>: <message>と出力する仮想メソッドsend(const std::string& message)- 文字列
"Generic"を返す仮想メソッドgetType() - 仮想デストラクタ
- protected メンバ
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>と出力するようにします- 注意:
EmailNotifierでfinalが指定されているため、ここではgetType()をオーバーライドすることはできません
- private メンバ
main.cpp: 3つの入力(それぞれ別々の行)を読み込みます:- 受信者名
- メールのドメイン
- メッセージ本文
3つの通知オブジェクトを動的に作成します:ベースの
Notifier、EmailNotifier、およびUrgentEmailNotifier。すべて同じ受信者(および該当する場合はドメイン)を使用します。これらをNotifier*ポインタの配列に格納します。配列をループし、各通知オブジェクトについて
getType()を使用してそのタイプを出力し、次にメッセージを指定してsend()を呼び出します。各エントリを以下の形式で出力してください:Type: <type> <send output>各通知オブジェクトの間に空行を出力してください。終了時には、動的に割り当てたオブジェクトをクリーンアップしてください。
例えば、入力が Alice、company.com、Meeting 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 3pmUrgentEmailNotifier が send() をオーバーライドしてメッセージ形式をカスタマイズできている一方で、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;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス