エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

隠蔽、多重定義、上書き

気になると深追いしたくなるid:E_Mattsanです。どーも。

基底クラスの関数が呼び出されるんじゃないんですね。ビビりました。

  • 参考: 『C++プログラミング入門』第5章「包含と継承を用いた階層構造」

この章と、同じ本の12章「仮想関数を用いた多態性」を理解すれば、一つ前の記事に書いた疑問から、抜け出せるかも。


メモ: 継承による関数名の隠蔽 - 虎塚


どんなときにどういう振る舞いになるか、個別の状況に対して説明はできるものの、定義をきちんと把握していなかったので原典にあたって自分なりに再確認してみました。

プログラミング言語C++ (アスキーアジソンウェスレイシリーズ―Ascii Addison Wesley programming series)

プログラミング言語C++ (アスキーアジソンウェスレイシリーズ―Ascii Addison Wesley programming series)

隠れるのか多重定義になるのか

4.9.4 スコープ

(中略)ブロック内で名前を宣言すると、外側のブロックで行われた宣言や大域名を隠すことができる。つまり、ブロック内では異なるものを参照するために名前を再定義できるのである。ブロックから抜けると、名前は元の意味を回復する。たとえば、次の通り。

int x;          // 大域名 x

void f()
{
    int x;      // 局所名 x が大域名 x を隠す
    x = 1;      // 局所名 x への代入
    {
        int x;  // 最初の局所名 x を隠す
        x = 2;  // 局所名 x への代入
    }
    x = 3;      // 最初の局所名 x への代入
}

int* p = &x;    // 大域名 x のアドレスを代入

(中略)隠されている大域名は、スコープ解決演算子::を使って参照できる。たとえば、次の通り。

int x;

void f2()
{
    int x = 1;  // 大域 x を隠蔽
    ::x = 2;    // 大域 x への代入
}


プログラミング言語C++ 第3版 p.117〜118

7.4.2 多重定義とスコープ

異なる非名前空間スコープで宣言された関数は多重定義されない。
(中略)

void f(int);

void g()
{
    void f(double);

    f(1); // f(double) を実行
}

f(1)にもっともよく一致するのは、明らかにf(int)だが、スコープにあるのはf(double)だけである。


p.194

8.2.1 限定子付きの名前

名前空間はスコープである。名前空間でも通常のスコープの規則が適用されるので、名前空間(それを囲むスコープ)ですでに宣言された名前は、それ以上面倒な手続きを踏むことなく使うことができる。ほかの名前空間で宣言された名前でも、名前空間によって限定すれば使うことができる。
(中略)


8.2.2 using 宣言

名前空間の外で頻繁に使われる名前があるとき、名前空間でいちいちその名前を限定するのは面倒である。(中略)この冗長性は(中略)using-declarationを使えば取り除ける。


p.215〜216


これらを踏まえて。宣言のしかたによってどのように関数の呼ばれ方が変わるのか、調べた結果がこれ。

#include <iostream>

void f(int n)
{
    std::cout << "f(int n) : n = " << n << std::endl;
}

void f(double r)
{
    std::cout << "f(double r) : r = " << r << std::endl;
}

void sample1()
{
    f(1); // f(int) と f(double) の両方が見えているが、引数がマッチする f(int) が呼ばれる
}

void sample2()
{
    void f(double); // f(int) を隠す

    f(1); // f(int) は隠れて見えないので f(double) が呼ばれる
}

void sample3()
{
    void f(double); // f(int) を隠す

    ::f(1); // スコープ解決演算子を使うことで f(int) を呼べるようになる
}

void sample4()
{
    void f(double); // f(int) を隠す

    using ::f; // using 宣言によって大域名の f をこのブロック内に持ち込む
               // ( f(int) も f(double) も見えるようにする)

    f(1);   // 引数がマッチする f(int) が呼ばれる
    f(1.1); // 引数がマッチする f(double) が呼ばれる
}

int main(int, char* [])
{
    std::cout << "sample 1\n";
    sample1();

    std::cout << "\nsample 2\n";
    sample2();

    std::cout << "\nsample 3\n";
    sample3();

    std::cout << "\nsample 4\n";
    sample4();

    return 0;
}


実行結果。

sample 1
f(int n) : n = 1

sample 2
f(double r) : r = 1

sample 3
f(int n) : n = 1

sample 4
f(int n) : n = 1
f(double r) : r = 1.1

継承において、隠れるのか多重定義になるのか上書きになるのか

12.2.6 仮想関数

(中略)基底クラスの仮想関数と同じ名前、同じ引数型を持つ派生クラスの関数は、基底クラスバージョンの仮想関数を上書き: override あるいはオーバーライドすると表現される。どのバージョンの仮想関数を呼び出すべきかを明示的に示した場合(Employee::print()のように)を除き、呼び出し対象のオブジェクトにとってもっとも適切な上書き関数が選択される。


p.366


「基底クラスの仮想関数と同じ名前、同じ引数型を持つ派生クラスの関数は、基底クラスバージョンの仮想関数をオーバーライドする」ということは、逆に言えば、

  1. 派生クラスの関数が、基底クラスの関数と同じ名前、同じ引数型であっても、基底クラスの関数が仮想関数でないばあい
  2. 派生クラスの関数が、基底クラスの仮想関数と違う名前のばあい
  3. 派生クラスの関数が、基底クラスの仮想関数と違う引数型を持つばあい

といったばあいはにはオーバーライドされないということになります。
名前が違う場合は自明として。ではのこりふたつのばあいはどうなるのか。


そのばあいには「4.9.4 スコープ」の「ブロック内で名前を宣言すると、外側のブロックで行われた宣言や大域名を隠すことができる」が適用される模様。

仮想関数でなければオーバーライドされない
#include <iostream>

class Base
{
public:
    void f(int n) { std::cout << "Base::f(int n) : n = " << n << std::endl; }
};

class Derived : public Base
{
public:
    void f(int n) { std::cout << "Derived::f(int n) : n = " << n << std::endl; }
};

int main(int, char* [])
{
    std::cout << "sample 5\n";

    Derived d;

    d.f(1); // Derived::f(int) が呼ばれる

    Base& b = d;

    b.f(1); // Base::f(int) は仮想関数でないためオーバーライドされない、そのため Base::f(int) が呼ばれる

    return 0;
}


実行結果。

sample 5
Derived::f(int n) : n = 1
Base::f(int n) : n = 1
同じ名前、異なる引数型のばあい隠される
#include <iostream>

class Base
{
public:
    void f(int n) { std::cout << "Base::f(int n) : n = " << n << std::endl; }
    virtual void g(int n) { std::cout << "Base::g(int n) : n = " << n << std::endl; }
};

class Derived : public Base
{
public:
    void f(double r) { std::cout << "Derived::f(double r) : r = " << r << std::endl; }
    void g(double r) { std::cout << "Derived::g(double r) : r = " << r << std::endl; }
};

int main(int, char* [])
{
    std::cout << "sample 6\n";

    Derived d;

    d.f(1); // Base::f(int) は隠蔽されて見えないので Derived::f(double) が呼ばれる
    d.g(1); // Base::g(int) は隠蔽されて見えないので Derived::g(double) が呼ばれる

    Base& b = d;

    b.f(1); // Base::f(int) が呼ばれる
    b.g(1); // Derived::g(double) は引数型が違うためオーバーライドされない、そのため Base::g(int) が呼ばれる

    d.Base::f(1); // 限定子を付ければ Derived からでも Base::f(int) を呼べる

    return 0;
}


実行結果。

sample 6
Derived::f(double r) : r = 1
Derived::g(double r) : r = 1
Base::f(int n) : n = 1
Base::g(int n) : n = 1
Base::f(int n) : n = 1
using宣言で名前を持ち込むと多重定義できる
#include <iostream>

class Base
{
public:
    void f(int n) { std::cout << "Base::f(int n) : n = " << n << std::endl; }
};

class Derived : public Base
{
public:
    using Base::f; // using 宣言を使うことで Base::f をこのスコープに持ち込む

    void f(double r) // 同じスコープ内で同じ名前の関数なのでオーバーロードになる
    {
        std::cout << "Derived::f(double r) : r = " << r << std::endl;
    }
};

int main(int, char* [])
{
    std::cout << "sample 7\n";

    Derived d;

    d.f(1);   // Base::f(int) が呼ばれる
    d.f(1.1); // Derived::f(double) が呼ばれる

    return 0;
}


実行結果。

sample 7
Base::f(int n) : n = 1
Derived::f(double r) : r = 1.1

だいたいこんな感じかと。

ここに書いた内容では、スコープ、ブロック、名前空間、クラスの区別が厳密でないので(きちんと調べきれてない…)、そのあたりは気をつけて読んでください。