Menu
Coddy logo textTech

仮想関数とVTable

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

virtualキーワードは、前のレッスンで見た問題を解決します。メソッドをvirtualとして宣言すると、C++はポインタの型ではなく、実行時の実際のオブジェクトの型に基づいて、どのバージョンを呼び出すかを決定します。

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

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

Dog d;
Animal* ptr = &d;
ptr->speak();  // 出力: Woof!

これで、正しいメソッドが呼び出されます!override キーワードは任意ですが、推奨されます。これは、仮想関数をオーバーライドする意図をコンパイラに伝え、シグネチャが一致しない場合にエラーを検出します。

その仕組みはどうなっているのでしょうか? クラスが仮想関数を持つ場合、コンパイラは仮想関数テーブル(vtable)を作成します。これは、クラスの仮想関数へのポインタを格納した隠しルックアップテーブルです。仮想関数を持つ各オブジェクトは、そのクラスの vtable への隠しポインタ(vptr)を保持します。

ポインタを介して仮想関数を呼び出すと、プログラムは実行時に vtable 内の正しい関数アドレスを検索します。これは 動的ディスパッチ と呼ばれます。通常の関数呼び出しと比較してわずかなオーバーヘッドが発生しますが、強力なポリモーフィックな振る舞いを可能にします。

重要なルールが1つあります。クラスが仮想関数を持ち、基底クラスとして使用される場合、そのデストラクタも仮想(virtual)にする必要があります。これにより、基底クラスのポインタを介して派生クラスのオブジェクトを削除する際に、適切なクリーンアップが行われることが保証されます。

class Animal {
public:
    virtual ~Animal() {}  // 仮想デストラクタ
    virtual void speak() {}
};
challenge icon

チャレンジ

簡単

仮想関数と実行時ポリモーフィズムの力を実証するメディアプレーヤーシステムを構築しましょう。メディアタイプの階層を作成し、それぞれが異なる方法で再生されるようにします。また、基底クラスのポインタを介してアクセスした場合でも、仮想関数によって正しい動作がどのように実現されるかを確認します。

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

  • Media.h: あらゆる再生可能なメディアを表す基底クラス Media を定義します:
    • protected メンバ std::string title
    • タイトルを受け取り、それを保存するコンストラクタ
    • Playing media: <title> と出力する virtual(仮想)な play() メソッド
    • Media [<title>] destroyed と出力する 仮想デストラクタ
  • AudioTrack.h: Media をパブリック継承する AudioTrack クラスを定義します:
    • private メンバ std::string artist
    • タイトルとアーティストを受け取り、タイトルを基底クラスに渡し、アーティストを保存するコンストラクタ
    • override キーワードを使用して play() をオーバーライドし、Playing audio: <title> by <artist> と出力します
    • AudioTrack [<title>] destroyed と出力するデストラクタ
  • main.cpp: 3つの入力(それぞれ別々の行)を読み取ります:
    1. オーディオトラックのタイトル (string)
    2. アーティスト名 (string)
    3. ビデオのタイトル (string)

    main.cpp 内で直接、Media をパブリック継承する VideoClip クラスを定義します:

    • private メンバ int duration(秒単位)
    • タイトルと再生時間を受け取るコンストラクタ(再生時間のデフォルト値は 120)
    • play() をオーバーライドして、Playing video: <title> (<duration>s) と出力します
    • VideoClip [<title>] destroyed と出力するデストラクタ

    ブロックスコープ内で3つの Media* ポインタの配列を作成します。以下を動的に割り当てます:

    • タイトルが "Generic Media" の基底 Media オブジェクト
    • 入力されたタイトルとアーティストを持つ AudioTrack オブジェクト
    • 入力されたビデオタイトルを持つ VideoClip オブジェクト

    配列をループして、各ポインタで play() を呼び出します。その後、すべてのオブジェクトを逆順で削除します。ブロックの後に、Playback complete! と出力します。

例えば、入力が Bohemian RhapsodyQueenNature Documentary の場合:

Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!

Media* ポインタを介して play() を呼び出すことで、仮想関数のおかげで正しい派生クラスのメソッドが呼び出されることに注目してください。また、仮想デストラクタによって適切なクリーンアップが保証されることも確認してください。基底ポインタを介して削除する場合、派生クラスと基底クラスの両方のデストラクタが実行されます。

チートシート

virtualキーワードは、ポインタの型ではなく、実際のオブジェクトの型に基づいて呼び出すメソッドを決定することで、実行時ポリモーフィズムを可能にします。

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

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

Dog d;
Animal* ptr = &d;
ptr->speak();  // 出力: Woof!

overrideキーワードは任意ですが、推奨されます。これは、仮想関数をオーバーライドする意図をコンパイラに伝え、シグネチャが一致しない場合にエラーを検出できるようにします。

仮想関数の仕組み: コンパイラは、クラスの仮想関数へのポインタを含む仮想関数テーブル(vtable)を作成します。仮想関数を持つ各オブジェクトは、そのクラスのvtableへの隠しポインタ(vptr)を保持します。ポインタを介して仮想関数を呼び出す際、プログラムは実行時にvtable内で正しい関数のアドレスを検索します。これは動的ディスパッチと呼ばれます。

仮想デストラクタ: クラスが仮想関数を持ち、基底クラスとして使用される場合、そのデストラクタも仮想である必要があります。これにより、基底クラスのポインタを介して派生オブジェクトを削除する際に、適切なクリーンアップが行われることが保証されます。

class Animal {
public:
    virtual ~Animal() {}  // 仮想デストラクタ
    virtual void speak() {}
};

自分で試してみよう

#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"

using namespace std;

// TODO: ここで Media を公開継承する VideoClip クラスを定義する
// - private な int 型の duration メンバ(秒単位)
// - コンストラクタは title と duration を受け取る(duration のデフォルト値は 120)
// - play() をオーバーライドして次を出力する: Playing video: <title> (<duration>s)
// - デストラクタは次を出力する: VideoClip [<title>] destroyed

class VideoClip : public Media {
private:
    int duration;

public:
    // TODO: コンストラクタを実装する
    VideoClip(const std::string& t, int d = 120) : Media(t) {
        // TODO: duration を格納する
    }

    // TODO: play() メソッドをオーバーライドする
    void play() override {
        // TODO: このメソッドを実装する
    }

    // TODO: デストラクタを実装する
    ~VideoClip() {
        // TODO: このデストラクタを実装する
    }
};

int main() {
    // 入力を読み込む
    string audioTitle;
    string artist;
    string videoTitle;
    
    getline(cin, audioTitle);
    getline(cin, artist);
    getline(cin, videoTitle);

    // TODO: 波括弧を使用してブロックスコープを作成する
    // ブロック内:
    // 1. 3つの Media* ポインタの配列を作成する
    // 2. 動的に割り当てる:
    //    - タイトルが "Generic Media" の基底 Media オブジェクト
    //    - 入力されたタイトルとアーティストを持つ AudioTrack オブジェクト
    //    - 入力されたビデオタイトルを持つ VideoClip オブジェクト
    // 3. 配列をループし、各ポインタで play() を呼び出す
    // 4. すべてのオブジェクトを逆順で削除する

    // TODO: ブロックの後で次を出力する: Playback complete!
    
    return 0;
}
quiz icon腕試し

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

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