C++の関数オーバーライドとは
C++における関数のオーバーライドとは、基底クラスで宣言された仮想関数を、派生クラスで再定義する仕組みです。
オーバーライドを使用すると、基底クラスのポインタや参照を通して関数を呼び出した場合でも、実際に参照しているオブジェクトの型に応じた処理を実行できます。
この仕組みは、C++における動的ポリモーフィズムの中心となる機能です。
オーバーライドの基本例
#include <iostream>
class Animal {
public:
virtual void speak() const {
std::cout << "Animal makes a sound\n";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "Dog says: Woof!\n";
}
};
int main() {
Dog dog;
dog.speak();
return 0;
}
実行結果は次のとおりです。
Dog says: Woof!
Animalクラスのspeak()は仮想関数として宣言されています。
virtual void speak() const
一方、Dogクラスでは、同じ形式のspeak()を再定義しています。
void speak() const override
このDog::speak()が、Animal::speak()をオーバーライドしています。
オーバーライドを使用する目的
オーバーライドを使用すると、複数の派生クラスを共通の基底クラス型として扱いながら、それぞれ異なる処理を実行できます。
例えば、犬、猫、鳥をすべてAnimal型として管理しつつ、鳴き声だけを動物ごとに変更できます。
#include <iostream>
#include <memory>
#include <vector>
class Animal {
public:
virtual void speak() const {
std::cout << "Animal makes a sound\n";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow!\n";
}
};
class Bird : public Animal {
public:
void speak() const override {
std::cout << "Tweet!\n";
}
};
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<Bird>());
for (const auto& animal : animals) {
animal->speak();
}
return 0;
}
実行結果は次のようになります。
Woof!
Meow!
Tweet!
コンテナに格納されている型は、すべてstd::unique_ptr<Animal>です。
しかし、実際に保持しているオブジェクトがDog、Cat、Birdのどれであるかによって、呼び出されるspeak()が変わります。
virtualの役割
virtualは、メンバ関数を仮想関数として宣言するために使用します。
class Base {
public:
virtual void show() const {
std::cout << "Base\n";
}
};
派生クラスでは、同じ形式の関数を定義することでオーバーライドできます。
class Derived : public Base {
public:
void show() const override {
std::cout << "Derived\n";
}
};
基底クラスのポインタから呼び出してみます。
Derived derived;
Base* basePointer = &derived;
basePointer->show();
実行結果は次のとおりです。
Derived
basePointerの宣言上の型はBase*ですが、実際に指しているオブジェクトはDerivedです。
そのため、仮想関数呼び出しによってDerived::show()が実行されます。
一度仮想関数になれば仮想性は継続する
基底クラスで一度virtualが付けられた関数は、派生クラス側でvirtualを省略しても仮想関数のままです。
class A {
public:
virtual void execute() {
}
};
class B : public A {
public:
void execute() {
}
};
class C : public B {
public:
void execute() override {
}
};
B::execute()にはvirtualもoverrideも書かれていませんが、A::execute()をオーバーライドしているため仮想関数です。
ただし、書き間違いを防ぐため、派生クラス側ではoverrideを付けることが推奨されます。
virtualがない場合の動作
基底クラスの関数が仮想関数でない場合、基底クラスのポインタから呼び出す関数は、ポインタの型に基づいて決定されます。
#include <iostream>
class Base {
public:
void show() const {
std::cout << "Base\n";
}
};
class Derived : public Base {
public:
void show() const {
std::cout << "Derived\n";
}
};
int main() {
Derived derived;
Base* basePointer = &derived;
basePointer->show();
return 0;
}
実行結果は次のとおりです。
Base
この場合、Derived::show()はBase::show()をオーバーライドしていません。
派生クラス側で同名関数を定義したことにより、基底クラスの関数を隠している状態です。
ただし、継承階層のさらに上位で仮想関数として宣言されている場合は、直前のクラスにvirtualが書かれていなくても仮想関数として扱われます。
override指定子の役割
overrideは、派生クラスの関数が基底クラスの仮想関数をオーバーライドすることを明示する指定子です。
class Derived : public Base {
public:
void show() const override {
std::cout << "Derived\n";
}
};
overrideを付けた関数が、実際には基底クラスの仮想関数をオーバーライドしていない場合、コンパイルエラーになります。
constの付け忘れを検出する例
class Base {
public:
virtual void show() const {
}
};
class Derived : public Base {
public:
void show() override {
}
};
基底クラスの関数は次の形式です。
virtual void show() const
派生クラスの関数は次の形式です。
void show()
派生クラス側にはconstがないため、両者は異なる関数です。
overrideを付けているため、コンパイラはオーバーライドが成立していないことを検出します。
正しくは次のように記述します。
class Derived : public Base {
public:
void show() const override {
}
};
オーバーライドが成立する主な条件
派生クラスの関数が基底クラスの仮想関数をオーバーライドするには、主に次の条件を満たす必要があります。
- 対応する基底クラスの関数が仮想関数である
- 通常のメンバ関数では関数名が一致している
- パラメータ型リストが対応している
constやvolatileなどのcv修飾が対応している- 参照修飾子の
&、&&、指定なしが対応している - 戻り値型が同一、または許可された共変戻り値型である
- 例外指定が基底クラス側の保証を弱めていない
引数の型が異なる場合
class Base {
public:
virtual void process(int value) {
}
};
class Derived : public Base {
public:
void process(double value) {
}
};
Base::process(int)とDerived::process(double)では、引数の型が異なります。
そのため、Derived::process(double)はBase::process(int)をオーバーライドしていません。
overrideを付けるとコンパイルエラーになります。
class Derived : public Base {
public:
void process(double value) override {
}
};
戻り値型だけを変更することはできない
戻り値型だけが異なる関数を定義しても、別のオーバーロードとして扱うことはできません。
class Base {
public:
virtual double calculate() const {
return 0.0;
}
};
class Derived : public Base {
public:
int calculate() const override {
return 0;
}
};
このコードでは、戻り値型が互換性を持たないためコンパイルエラーになります。
戻り値型は原則として同じ型にする必要があります。
ただし、クラス型へのポインタや参照には、共変戻り値型という例外があります。
仮想デストラクタは名前が異なってもオーバーライドされる
通常のメンバ関数では、関数名が一致している必要があります。
ただし、デストラクタは例外です。
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
~Derived() override = default;
};
~Base()と~Derived()では名前が異なりますが、DerivedのデストラクタはBaseの仮想デストラクタをオーバーライドします。
オーバーライドとオーバーロードの違い
オーバーライドとオーバーロードは名称が似ていますが、異なる仕組みです。
オーバーライド
オーバーライドは、継承関係にあるクラス間で、基底クラスの仮想関数を派生クラスが再定義する仕組みです。
class Base {
public:
virtual void print() {
}
};
class Derived : public Base {
public:
void print() override {
}
};
オーバーロード
オーバーロードは、同じスコープ内で、引数の型や個数が異なる同名関数を複数定義する仕組みです。
#include <string>
class Printer {
public:
void print(int value) {
}
void print(double value) {
}
void print(const std::string& value) {
}
};
オーバーライドとオーバーロードの比較
| 項目 | オーバーライド | オーバーロード |
|---|---|---|
| 主な目的 | 派生クラスで処理を変更する | 異なる引数形式に対応する |
| 継承関係 | 必要 | 不要 |
基底クラスのvirtual | 必要 | 不要 |
| 引数 | オーバーライド条件を満たす必要がある | 型や個数を変える |
| 関数の選択 | 仮想呼び出しでは動的型に応じて決まる | 原則としてコンパイル時に決まる |
基底クラスのポインタと参照
オーバーライドによる動的ポリモーフィズムは、基底クラスのポインタまたは参照を使用した場合に特に重要です。
ポインタを使用する場合
Derived derived;
Base* pointer = &derived;
pointer->show();
参照を使用する場合
Derived derived;
Base& reference = derived;
reference.show();
基底クラスの関数が仮想関数であれば、どちらもDerived::show()を呼び出します。
オブジェクトスライシングとは
派生クラスのオブジェクトを基底クラス型の変数へ値として代入すると、派生クラス固有の部分が失われます。
この現象をオブジェクトスライシングと呼びます。
#include <iostream>
class Base {
public:
virtual void show() const {
std::cout << "Base\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() const override {
std::cout << "Derived\n";
}
};
int main() {
Derived derived;
Base copied = derived;
copied.show();
Base& reference = derived;
reference.show();
return 0;
}
実行結果は次のとおりです。
Base
Derived
copiedはBase型の独立したオブジェクトであり、Derived部分は保持していません。
一方、referenceは元のDerivedオブジェクトを参照しているため、Derived::show()が呼び出されます。
値渡しでもスライシングが発生する
次のように、関数の引数へ値渡しした場合もオブジェクトスライシングが発生します。
void process(Base object) {
object.show();
}
ポリモーフィズムを維持したい場合は、参照またはポインタを使用します。
void process(const Base& object) {
object.show();
}
仮想デストラクタの必要性
基底クラスのポインタを通して派生クラスのオブジェクトを削除する可能性がある場合、基底クラスのデストラクタは仮想関数にする必要があります。
class Base {
public:
virtual ~Base() = default;
};
次のようなコードを考えます。
#include <iostream>
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor\n";
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor\n";
}
};
int main() {
Base* pointer = new Derived();
delete pointer;
return 0;
}
実行結果は次のようになります。
Derived destructor
Base destructor
まず派生クラスのデストラクタが呼ばれ、その後に基底クラスのデストラクタが呼ばれます。
基底クラスのデストラクタが仮想関数でない状態で、基底クラスのポインタから派生クラスのオブジェクトを削除すると、未定義動作になる可能性があります。
そのため、ポリモーフィズム用途の基底クラスでは、一般的に次のように記述します。
virtual ~Base() = default;
ただし、基底クラスのポインタから削除させない設計では、デストラクタをprotectedにする方法もあります。
class Base {
protected:
~Base() = default;
};
純粋仮想関数と抽象クラス
基底クラス側で具体的な処理を持たず、派生クラス側で実装させたい場合は、純粋仮想関数を使用します。
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
末尾の= 0が純粋仮想関数を表します。
純粋仮想関数を持ち、その最終オーバーライダーが純粋なままのクラスは抽象クラスです。
抽象クラス自体のオブジェクトは生成できません。
Shape shape;
このコードはコンパイルエラーになります。
派生クラスで純粋仮想関数を実装する
#include <iostream>
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Rectangle : public Shape {
private:
double width_;
double height_;
public:
Rectangle(double width, double height)
: width_(width), height_(height) {
}
double area() const override {
return width_ * height_;
}
};
int main() {
Rectangle rectangle(5.0, 3.0);
Shape& shape = rectangle;
std::cout << shape.area() << '\n';
return 0;
}
実行結果は次のとおりです。
15
実装しない派生クラスは抽象クラスのままになる
派生クラスが純粋仮想関数を実装しなくても、そのクラス自身が抽象クラスのままであれば問題ありません。
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class AbstractShape : public Shape {
};
AbstractShapeはarea()を実装していないため、引き続き抽象クラスです。
さらに派生したクラスで実装できます。
class Rectangle : public AbstractShape {
public:
double area() const override {
return 10.0;
}
};
純粋仮想関数にも定義を持たせられる
純粋仮想関数は、必要に応じて関数本体を定義することもできます。
class Base {
public:
virtual void execute() = 0;
};
void Base::execute() {
// 共通処理
}
ただし、= 0が指定されているため、Baseは抽象クラスのままです。
派生クラスから修飾名を使って呼び出せます。
void Derived::execute() {
Base::execute();
}
final指定子の役割
finalを使用すると、それ以上のオーバーライドを禁止できます。
class Base {
public:
virtual void execute() {
}
};
class Middle : public Base {
public:
void execute() final {
}
};
さらに派生したクラスでexecute()をオーバーライドしようとすると、コンパイルエラーになります。
class Derived : public Middle {
public:
void execute() override {
}
};
クラス自体の継承を禁止する
クラス名の後ろにfinalを付けると、そのクラスを継承できなくなります。
class FinalClass final {
};
次のコードはコンパイルエラーになります。
class Child : public FinalClass {
};
基底クラスの関数を直接呼び出す方法
派生クラスでオーバーライドした関数から、基底クラス側の実装も利用したい場合は、スコープ解決演算子::を使用します。
#include <iostream>
class Base {
public:
virtual void initialize() {
std::cout << "Base initialization\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void initialize() override {
Base::initialize();
std::cout << "Derived initialization\n";
}
};
int main() {
Derived object;
object.initialize();
return 0;
}
実行結果は次のとおりです。
Base initialization
Derived initialization
Base::initialize()と明示すると、仮想ディスパッチは行われず、基底クラスの実装が直接呼び出されます。
共変戻り値型
通常、オーバーライドする関数の戻り値型は、基底クラスの関数と同じである必要があります。
ただし、クラス型へのポインタまたは参照を戻す場合は、派生クラス側でより具体的な型を返せることがあります。
この仕組みを共変戻り値型と呼びます。
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived* clone() const override {
return new Derived(*this);
}
};
基底クラスではBase*を返していますが、派生クラスではDerived*を返しています。
DerivedがBaseの派生クラスであるため、この戻り値型は認められます。
スマートポインタには直接適用できない
次の基底関数に対して、
virtual std::unique_ptr<Base> clone() const;
派生クラス側を次のようにはできません。
std::unique_ptr<Derived> clone() const override;
std::unique_ptr<Base>とstd::unique_ptr<Derived>は異なる型であり、共変戻り値型の対象にはならないためです。
派生クラス側も同じ戻り値型にします。
std::unique_ptr<Base> clone() const override {
return std::make_unique<Derived>(*this);
}
アクセス指定子とオーバーライド
オーバーライド関係の成立とアクセス制御は、別の仕組みです。
そのため、基底クラスでpublicだった仮想関数を、派生クラスでprivateとしてオーバーライドすることもできます。
#include <iostream>
class Base {
public:
virtual void run() {
std::cout << "Base\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
private:
void run() override {
std::cout << "Derived\n";
}
};
int main() {
Derived derived;
Base& base = derived;
base.run();
return 0;
}
実行結果は次のとおりです。
Derived
baseの静的型はBase&であり、Base::run()はpublicなので呼び出せます。
その後、仮想関数の仕組みによってDerived::run()が実行されます。
一方、次の直接呼び出しはできません。
derived.run();
Derived::run()がprivateだからです。
基底クラスの関数がprivateでもオーバーライドできる
基底クラスの仮想関数がprivateでも、派生クラス側でオーバーライドできます。
class Base {
private:
virtual void execute() {
}
};
class Derived : public Base {
public:
void execute() override {
}
};
派生クラスからBase::execute()を直接呼び出すことはできませんが、オーバーライド関係は成立します。
関数の名前隠蔽
派生クラスで同じ名前の関数を定義すると、基底クラスにある同名関数が隠れることがあります。
#include <iostream>
class Base {
public:
virtual void print(int value) {
std::cout << "Base int: " << value << '\n';
}
void print(double value) {
std::cout << "Base double: " << value << '\n';
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void print(int value) override {
std::cout << "Derived int: " << value << '\n';
}
};
int main() {
Derived object;
object.print(3.14);
return 0;
}
Derivedでprintという名前の関数を定義したため、通常の名前探索ではBase::print(double)が隠れます。
その結果、3.14がintへ変換され、Derived::print(int)が呼び出されます。
using宣言で基底クラスの関数を見えるようにする
基底クラスのオーバーロードも派生クラス側で利用したい場合は、using宣言を使用します。
class Derived : public Base {
public:
using Base::print;
void print(int value) override {
std::cout << "Derived int: " << value << '\n';
}
};
これにより、Base::print(double)もオーバーロード候補に含まれます。
デフォルト引数と仮想関数
仮想関数では、呼び出される関数本体は実際のオブジェクト型に応じて決まります。
一方、デフォルト引数は、呼び出し側で使用している静的型に基づいて決まります。
#include <iostream>
class Base {
public:
virtual void show(int value = 10) const {
std::cout << "Base: " << value << '\n';
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show(int value = 20) const override {
std::cout << "Derived: " << value << '\n';
}
};
int main() {
Derived derived;
Base& base = derived;
derived.show();
base.show();
return 0;
}
実行結果は次のようになります。
Derived: 20
Derived: 10
derived.show()では、静的型がDerivedなので、デフォルト引数の20が使用されます。
base.show()では、関数本体はDerived::show()ですが、静的型がBaseなので、デフォルト引数には10が使用されます。
混乱を防ぐため、仮想関数では派生クラスごとに異なるデフォルト引数を指定しない方が安全です。
コンストラクタとデストラクタ内の仮想関数呼び出し
コンストラクタやデストラクタ内でも仮想関数を呼び出すことはできます。
ただし、通常の完成済みオブジェクトに対する仮想呼び出しとは動作が異なります。
#include <iostream>
class Base {
public:
Base() {
show();
}
virtual void show() const {
std::cout << "Base\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() const override {
std::cout << "Derived\n";
}
};
int main() {
Derived object;
return 0;
}
実行結果は次のとおりです。
Base
Baseのコンストラクタが実行されている時点では、Derived部分はまだ構築されていません。
そのため、Baseのコンストラクタ内から呼び出したshow()では、Base::show()が実行されます。
デストラクタでも同様に、破棄が進んだ後の派生クラス側へ仮想ディスパッチされることはありません。
コンストラクタやデストラクタ内で仮想関数を呼ぶこと自体は禁止されていませんが、最派生クラスの処理が呼ばれることを期待する設計は避ける必要があります。
noexceptとオーバーライド
基底クラスの仮想関数がnoexceptである場合、派生クラス側も原則としてnoexceptでオーバーライドする必要があります。
class Base {
public:
virtual void execute() noexcept {
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void execute() noexcept override {
}
};
派生クラス側からnoexceptを外すと、基底クラスの「例外を送出しない」という保証を弱めることになります。
class Derived : public Base {
public:
void execute() override {
}
};
このコードはコンパイルエラーになります。
参照修飾子とオーバーライド
メンバ関数には、左辺値オブジェクト専用の&や、右辺値オブジェクト専用の&&を指定できます。
class Base {
public:
virtual void process() & {
}
virtual ~Base() = default;
};
派生クラス側でも同じ参照修飾子が必要です。
class Derived : public Base {
public:
void process() & override {
}
};
次の関数はオーバーライドになりません。
void process() &&;
&と&&は異なる形式のメンバ関数だからです。
静的メンバ関数はオーバーライドできない
staticメンバ関数は、特定のオブジェクトに属する関数ではありません。
そのため、仮想関数にはできず、オーバーライドもできません。
class Base {
public:
static void show() {
}
};
次のような宣言はできません。
virtual static void show();
派生クラスで同名の静的メンバ関数を定義することはできますが、仮想関数としてのオーバーライドではありません。
メンバ関数テンプレートは仮想関数にできない
メンバ関数テンプレートを仮想関数として宣言することはできません。
class Base {
public:
template <typename T>
virtual void process(T value);
};
このコードはコンパイルエラーになります。
仮想関数は実行時に呼び出す実装を選択する仕組みですが、関数テンプレートはコンパイル時に必要な型ごとの関数を生成する仕組みです。
両者は直接組み合わせられないため、必要な型ごとに通常の仮想関数を用意するなど、別の設計が必要です。
オーバーライドでよくある間違い
基底クラスの関数が仮想関数になっていない
class Base {
public:
void show() {
}
};
この場合、基底クラスのポインタや参照を通しても、動的な呼び分けは行われません。
派生クラス側でoverrideを付けていない
class Derived : public Base {
public:
void show() {
}
};
正しくオーバーライドできている可能性はありますが、引数やconstの違いによるミスをコンパイラに検出させにくくなります。
次のようにoverrideを付けるのが安全です。
void show() override;
constを付け忘れている
class Base {
public:
virtual void show() const {
}
};
class Derived : public Base {
public:
void show() {
}
};
この場合、Derived::show()はBase::show() constをオーバーライドしていません。
基底クラスのデストラクタが仮想関数でない
class Base {
public:
~Base() = default;
};
基底クラスのポインタから派生クラスのオブジェクトを削除する可能性がある場合は危険です。
class Base {
public:
virtual ~Base() = default;
};
オブジェクトを値で保持している
std::vector<Base> objects;
objects.push_back(Derived{});
Baseが非抽象クラスであれば、このコード自体は成立しますが、Derived固有の部分が失われ、オブジェクトスライシングが発生します。
ポリモーフィズムを利用する場合は、スマートポインタで保持します。
std::vector<std::unique_ptr<Base>> objects;
objects.push_back(std::make_unique<Derived>());
実践的なオーバーライドの書き方
ポリモーフィズムを前提とした基底クラスは、次のように設計できます。
class Base {
public:
virtual void execute() = 0;
virtual ~Base() = default;
};
派生クラスではoverrideを付けます。
class Derived : public Base {
public:
void execute() override {
// 派生クラス固有の処理
}
};
さらに継承させる必要がない場合は、クラスまたは関数にfinalを付けます。
class Derived final : public Base {
public:
void execute() override {
}
};
オーバーライドを使用した完成例
#include <iostream>
#include <memory>
#include <vector>
class Employee {
public:
virtual void work() const = 0;
virtual ~Employee() = default;
};
class Engineer final : public Employee {
public:
void work() const override {
std::cout << "Engineer writes code.\n";
}
};
class Designer final : public Employee {
public:
void work() const override {
std::cout << "Designer creates designs.\n";
}
};
int main() {
std::vector<std::unique_ptr<Employee>> employees;
employees.push_back(std::make_unique<Engineer>());
employees.push_back(std::make_unique<Designer>());
for (const auto& employee : employees) {
employee->work();
}
return 0;
}
この例には、オーバーライドを安全に利用するための基本が含まれています。
- 基底クラスの関数を仮想関数または純粋仮想関数にする
- 基底クラスのデストラクタを仮想関数にする
- 派生クラスの関数に
overrideを付ける - 継承不要なクラスに
finalを付ける - ポリモーフィズムの対象をポインタや参照で扱う
- 所有権を持つ場合はスマートポインタを使用する
C++の関数オーバーライドのまとめ
C++の関数オーバーライドは、基底クラスの仮想関数を派生クラスで再定義し、実際のオブジェクト型に応じて処理を切り替える仕組みです。
基本的な形は次のとおりです。
class Base {
public:
virtual void function() {
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void function() override {
}
};
オーバーライドを正しく利用するためには、次の点が重要です。
- 継承階層の最初の関数を
virtualで宣言する - 派生クラス側では
overrideを付ける - 引数、cv修飾、参照修飾子などを正しく一致させる
- ポリモーフィズム用途の基底クラスでは仮想デストラクタを検討する
- オブジェクトスライシングを避ける
- 値ではなくポインタや参照で扱う
- 仮想関数のデフォルト引数やコンストラクタ内での呼び出しに注意する
実務では、派生クラス側の関数にoverrideを付け、コンパイラにオーバーライドが成立していることを確認させる書き方が特に重要です。
以上、C++の関数のオーバーライドについてでした。
最後までお読みいただき、ありがとうございました。
