1. ホーム
  2. c++

[解決済み】派生クラスでオーバーライドされた関数が、ベースクラスの他のオーバーロードを隠してしまうのはなぜですか?

2022-04-03 22:16:07

質問

コードを考えてみましょう。

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

このエラーが発生しました。

>g++ -pedantic -Os test.cpp -o test
test.cpp: 関数 `int main()' 内にあります。
test.cpp:31: error: no matching function for call to `Derived::gogo(int)'.
test.cpp:21: 注意: 候補は以下の通り: virtual void Derived::gogo(int*) 
test.cpp:33:2: 警告: ファイル末尾に改行がない
>終了コードです。1

ここでは、Derivedクラスの関数が、ベースクラス内の同名(シグネチャではない)関数をすべて食ってしまっています。C++のこの挙動は、なぜか問題ないように見える。ポリモーフィックではない。

どうすればいい?

ご質問の文言から判断すると("hide"という単語を使っています)、ここで何が起こっているかはすでにお分かりでしょう。この現象は、「名前隠し」と呼ばれています。なぜか、誰かが質問するたびに なぜ と言って、その仕組みを説明するか(これはもう知っていることでしょう)、それを上書きする方法を説明するか(これは聞かれなかったことでしょう)ですが、誰も実際の "why" の質問を扱うことに関心がないようです。

名前隠しの決定、根拠、すなわち なぜ これは、継承されたオーバーロード関数が、与えられたクラスの現在のオーバーロード関数と混在することを許した場合、ある種の直感に反する、予期しない、潜在的に危険な動作を回避するために、C++に実際に設計されました。C++のオーバーロード解決は、候補のセットから最適な関数を選択することで行われることはご存知でしょう。これは、引数の型とパラメーターの型を照合することで行われる。このマッチングルールは時に複雑で、準備のないユーザーには非論理的と思われる結果を導くこともしばしばです。また、既存の関数群に新しい関数を追加すると、過負荷解消の結果が大幅に変化することがあります。

例えば、ベースクラスである B は、メンバ関数 foo という型のパラメータを受け取り、そのパラメータが void * へのすべての呼び出しは foo(NULL) に解決されます。 B::foo(void *) . 名前を隠していない状態で、この B::foo(void *) から下降する多くの異なるクラスで表示されます。 B . しかし、ある[間接的、遠隔的]な子孫において、例えば D のクラス B 関数 foo(int) が定義されています。さて、名前を隠さないまま D は両方の foo(void *)foo(int) 可視化され、オーバーロードの解決に参加します。への呼び出しはどの関数になるのでしょうか? foo(NULL) 型のオブジェクトを通して行われた場合、その解決は D ? に解決されます。 D::foo(int) というのは int の方が、積分ゼロにマッチしている(つまり NULL )は、どのポインタ型よりも優れています。そのため、階層全体を通して foo(NULL) は1つの関数に解決するのに対し D (以下)突然別のものに解決してしまうのです。

また、別の例として C++の設計と進化 77ページ

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived : public Base{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

このルールがないと、bの状態が部分的に更新され、スライスにつながる。

この挙動は、言語設計時に好ましくないと判断された。つまり、各クラスは宣言するメソッド名について「クリーンシート」から始めるという仕様です。この動作をオーバーライドするには、ユーザーによる明示的な操作が必要です。元々は継承されたメソッドの再宣言(現在は非推奨)でしたが、現在は明示的に using-declaration を使用するようになっています。

あなたが最初の投稿で正しく指摘したように(私は"Not polymorphic"の発言を参照しています)、この動作はクラス間のIS-A関係の違反とみなされるかもしれません。しかし、どうやら当時は、最終的に名前の隠蔽はより小さな悪であることが証明されると判断されたようです。