ストラウストラップのプログラミング入門(23) 14.3.3「オーバーライド」補遺

ストラウストラップのプログラミング入門』を読む。今日は、14章の補遺です。

前回の読書日記にいただいたコメントを読んで、14.3.3「オーバーライド」を読み返しました。

ずっと考えていたら、オーバーライドがゲシュタルト崩壊してきたので、ちょっと文章で書いてみます。正直まだよく理解してない感があります。

こういうコードがあるとして。

#include <iostream>
#include "../../std_lib_facilities.h"

struct B{
    virtual void f() const { std::cout << "B::f " << std::endl; }
    void g() const { std::cout << "B::g " << std::endl; } // 仮想関数でない
};

struct D : B{
    void f() const { std::cout << "D::f " << std::endl; } // B::f()をオーバーライド
    void g() { std::cout << "D::g " << std::endl; }
};

struct DD : D{
    void f() { std::cout << "DD::f " << std::endl; }   // D::f()をオーバーライドしない
    void g() const { std::cout << "DD::g " << std::endl; }
};

void call (const B& b)
{
    b.f();
    b.g();
}

int main()
{
    B b;
    D d;
    DD dd;

    std::cout << "BのインスタンスをBクラスのオブジェクトとして扱う: \n";
    call(b);    // (0)
    
    std::cout << "DのインスタンスをBクラスのオブジェクトとして扱う: \n";
    call(d);    // (1)

    std::cout << "DDのインスタンスをBクラスのオブジェクトとして扱う: \n";
    call(dd);   // (2)
    
    std::cout << "Bのインスタンスを使う: \n";
    b.f();    // (3.1)
    b.g();    // (3.2)
    
    std::cout << "Dのインスタンスを使う: \n";
    d.f();    // (4.1)
    d.g();    // (4.2)
    
    std::cout << "DDのインスタンスを使う: \n";
    dd.f();    // (5.1)
    dd.g();    // (5.2)

    keep_window_open();
}

実行結果。

BのインスタンスをBクラスのオブジェクトとして扱う
B::f // (0.1)
B::g // (0.2)
DのインスタンスをBクラスのオブジェクトとして扱う
D::f // (1.1)
B::g // (1.2)
DDのインスタンスをBクラスのオブジェクトとして扱う
D::f // (2.1)
B::g // (2.2)
Bのインスタンスを使う
B::f // (3.1)
B::g // (3.2)
Dのインスタンスを使う
D::f // (4.1)
D::g // (4.2)
DDのインスタンスを使う
DD::f // (5.1)
DD::g // (5.2)

ここで、(1.2)と(2.2)の箇所が、「クラスにバインドされた非仮想関数が呼び出された結果」である、という理解をしています。

最初は、「call関数の引数にconstがあるから、関数宣言にconstがないD::g()でなく、constがあるスーパークラスのB::g()が呼び出された」のだと誤解していました。そうではないのですね。その理屈に従うのであれば、B::g()とDD::g()は戻り値も引数もconstの有無も同じ宣言ですから、(2.2)でDD:gが表示されて然るべきです。しかし、表示されたのは相変わらず B::gです。

(2.1)がDD::fでなくD::fになる部分こそが、「call関数の引数にconstがあるから、関数宣言にconstがないDD::f()ではなく、constがあるスーパークラスのD::f()が呼び出された」という話ですね。実際、D::f()の定義で、constを外すと、B::f()が出力されます。インスタンスにバインドされた関数が呼び出されます。

言語仕様を帰納法で理解しようとするなという話ですが、、、ひとまずこんな感じです。

で、分からないのは、「(4.2)が、B::gでなく、D::gなのは、const呼び出しではないためなのか?」ということ。

  • 仮想関数は、constが欠けるとオーバーライドしない。(例)DD::f()は、D::f()をオーバーライドしない。
  • 非仮想関数は、constが欠けていても、オーバーライドする。(例)D::g()は、B::g()をオーバーライドする。
  • ただし、const呼び出しが行われた場合は、それ相応の関数が呼び出される。

ということでしょうか。。。

根本的な問題として

検索しても、なかなか、なるほどネ!という解説が見つからないのですが・・・

冷静に考えて、複数の問題を同時に考えて、ややこしくしているだけでは?

  • 関数定義の違いによるオーバーライドの可否
  • const呼び出しとそうでない呼び出しの違い
  • 仮想関数と非仮想関数で呼び出される

たいへん情けないので、もうちょい考えることに。