HOWTO · C++

C++ での関数のオーバーロード VS オーバーライド

この記事では、C++ で関数のオーバーロード VS オーバーライドを使用する方法を示します

このページの内容

C++の関数オーバーロードと関数オーバーライドは、それぞれ異なる問題を解決します。関数オーバーロードでは、同じスコープにある複数の関数に同じ名前を付け、引数リストを変えます。コンパイラは呼び出し時の引数に基づいて適切な関数を選びます。関数オーバーライドでは、派生クラスが基底クラスから継承した virtual 関数に一致する実装を用意し、基底クラスの動作を置き換えます。基底クラスの参照またはポインター経由で呼び出すと、実行時に派生クラスのオブジェクトに応じて関数が選択されます。

C++におけるオーバーロードとオーバーライドとは

オーバーロードは、引数が異なる複数の関数から適切なものを選ぶ仕組みです。オーバーライドは、継承階層の中で動作を置き換える仕組みです。戻り値の型だけではオーバーロードを区別できず、オーバーライドには基底クラスの virtual 関数が必要です。override 指定子を付けると、派生クラスで宣言した関数が基底クラスの仮想関数に本当に一致するか、コンパイラが確認します。

関数オーバーロードの仕組み

同じスコープで同じ名前の関数を宣言し、それぞれ異なる引数リストを指定します。引数リストは、引数の数または型を変えて区別できます。関数を呼び出すと、オーバーロード解決では渡された引数と候補関数の引数型が使われます。戻り値の型は選択に使われないため、戻り値の型だけが異なる2つの宣言は、有効なオーバーロードの組み合わせにはなりません。

次の例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

#include <iostream>

class Formatter {
public:
    void show(int value) const {
        std::cout << "integer: " << value << '\n';
    }

    void show(double value) const {
        std::cout << "decimal: " << value << '\n';
    }
};

int main() {
    Formatter formatter;
    formatter.show(7);
    formatter.show(2.5);
}
integer: 7
decimal: 2.5

提供された実行時の証拠から、この例が正常に実行され、表示された出力が得られたことを確認できます。整数と小数の引数によって、異なる show オーバーロードが選ばれます。オーバーロードを追加するときは、引数リストに明確な違いを持たせ、呼び出し先が曖昧にならないようにしてください。

関数オーバーライドの仕組み

基底クラスで関数を virtual として宣言し、派生クラスでは名前と引数リストが一致し、修飾子と戻り値の型に互換性がある関数を定義します。派生クラス側の宣言に override を付けると、一致していない場合にコンパイル時の診断を受けられます。基底クラスの参照またはポインター経由で関数を呼び出すと、仮想ディスパッチによって、そのオブジェクトで最終オーバーライドとなる関数が選ばれます。

次の例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

#include <iostream>

class Message {
public:
    virtual void print() const {
        std::cout << "message\n";
    }
};

class Alert : public Message {
public:
    void print() const override {
        std::cout << "alert\n";
    }
};

int main() {
    Alert alert;
    const Message& message = alert;
    message.print();
}
alert

提供された実行時の証拠から、この例が正常に実行され、表示された出力が得られたことを確認できます。message の型は基底クラスですが、Alert を参照しているため、仮想関数の呼び出しでは Alert::print が使われます。また、基底クラスでは = 0 を付けて純粋仮想関数を宣言できます。そのクラスは抽象クラスとなり、具象派生クラスが純粋仮想関数を実装するまでインスタンス化できません。

オーバーロードとオーバーライドの主な違い

観点 関数オーバーロード 関数オーバーライド
目的 引数に応じて同じ名前の処理を提供する 派生クラスで継承した動作を特化する
要件 同じスコープで異なる引数リストを指定する 継承関係と、基底クラスの virtual 関数に一致する宣言がある
選択方法 呼び出し時の引数に基づくコンパイル時のオーバーロード解決 基底クラスの参照またはポインター経由で行われる実行時の仮想ディスパッチ
役立つ確認方法 呼び出し先が意図したオーバーロードになるか確認する override を付けて宣言が一致するか確認する

オーバーロードは、継承や仮想関数がなくても使えます。オーバーライドは継承と仮想ディスパッチを前提とし、異なる引数リストを渡して選択するものではありません。

よくある間違いと見分け方

派生クラスで同じ名前の関数を宣言しても、自動的に基底クラスの関数をオーバーライドするわけではありません。基底クラスの関数が virtual でなければ、派生クラスの宣言は基底クラスの名前を隠すだけで、仮想ディスパッチは有効になりません。引数、const 修飾子、その他の関数型の要素が一致しないと、意図したオーバーライドにならないことがあります。override を付ければ、その不一致をコンパイル時に検出できます。

戻り値の型だけを変えてオーバーロードしようとしないでください。また、ポリモーフィックな動作を期待する場合は、派生クラスのオブジェクトを値渡ししないようにします。基底クラスのオブジェクトにコピーすると、派生クラス固有の部分がスライスされて失われます。動的ディスパッチを保つには、基底クラスの参照またはポインターを使います。通常の所有権管理には、自動変数またはスマートポインターを優先してください。基底クラスのポインター経由で派生オブジェクトを削除する場合、基底クラスには仮想デストラクターが必要です。