web-dev-qa-db-ja.com

派生クラスのオーバーライドされた関数が、基本クラスの他のオーバーロードを隠すのはなぜですか?

コードを考慮してください:

#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:エラー:一致なし`Derived :: gogo(int) '
 test.cpp:21への呼び出しのための関数:注:候補は次のとおりです:仮想void Derived :: gogo(int *)
 test.cpp:33: 2:警告:ファイルの終わりに改行なし
>終了コード:1 

ここで、派生クラスの関数は、基本クラスの同じ名前(シグネチャではない)のすべての関数を隠しています。どういうわけか、このC++の動作は問題ないように見えます。ポリモーフィックではありません。

211
Aman Aggarwal

質問の言葉遣いから判断すると(「非表示」という言葉を使用した)、ここで何が起こっているかは既にわかっています。この現象は「名前の隠蔽」と呼ばれます。何らかの理由で、誰かがwhy名前の隠蔽について質問するたびに、応答する人はこれを「名前の隠蔽」と呼び、それがどのように機能するかを説明します(おそらく既に知っています)。それをオーバーライドする方法(あなたは決して尋ねなかった)、しかし誰も実際の「なぜ」質問に取り組むことを気にしないようだ。

why実際にC++に設計された名前非表示の背後にある決定は、継承されたオーバーロードされた関数のセットが許可された場合に発生する可能性のある特定の直感に反する、予期せぬ、潜在的に危険な動作を回避することです指定されたクラスのオーバーロードの現在のセットと混合します。 C++では、候補のセットから最適な関数を選択することでオーバーロード解決が機能することをご存知でしょう。これは、引数のタイプをパラメーターのタイプに一致させることにより行われます。マッチングルールは時々複雑になる可能性があり、多くの場合、準備ができていないユーザーからは非論理的であると認識される可能性のある結果につながります。以前の一連の既存の関数に新しい関数を追加すると、オーバーロード解決の結果が大幅に変化する可能性があります。

たとえば、基本クラスBには、void *型のパラメーターを取るメンバー関数fooがあり、foo(NULL)へのすべての呼び出しはB::foo(void *)。名前の非表示はなく、このB::foo(void *)Bから派生する多くの異なるクラスで表示されるとしましょう。ただし、[間接的なリモート]下位クラスDのクラスBで、関数foo(int)が定義されているとします。現在、名前を非表示にしない場合、Dにはfoo(void *)foo(int)の両方が表示され、オーバーロード解決に参加しています。タイプDのオブジェクトを介して行われた場合、foo(NULL)の呼び出しはどの関数に解決されますか? intは、どのポインター型よりも整数ゼロ(つまりNULL)に適しているため、D::foo(int)に解決されます。したがって、階層全体でfoo(NULL)の呼び出しは1つの関数に解決されますが、D(およびその下)で​​は突然別の関数に解決されます。

別の例はC++の設計と進化、77ページにあります:

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

class Derived{
    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宣言を明示的に使用しています。

元の投稿で正しく観察したように(「ポリモーフィックではない」という発言を参照しています)、この動作はクラス間のIS-A関係の違反と見なされる場合があります。これは事実ですが、どうやら当時は名前の隠蔽がより小さな悪であると判明することが明らかになりました。

392
AnT

名前解決の規則では、名前の検索は、一致する名前が見つかった最初のスコープで停止するとされています。その時点で、使用可能な機能の最適な一致を見つけるために、オーバーロード解決ルールが開始されます。

この場合、gogo(int*)がDerivedクラススコープで(単独で)検出され、intからint *への標準変換がないため、ルックアップは失敗します。

解決策は、Derivedクラスのusing宣言を使用してBase宣言を取り込むことです。

using Base::gogo;

...名前ルックアップルールですべての候補を見つけることができるため、オーバーロードの解決は期待どおりに進みます。

43
Drew Hall

これは「設計による」です。 C++では、このタイプのメソッドのオーバーロード解決は次のように機能します。

  • 参照の型から開始して、次に基本型に移動し、「gogo」という名前のメソッドを持つ最初の型を見つけます
  • そのタイプの「gogo」という名前のメソッドのみを考慮して、一致するオーバーロードを見つけます

Derivedには「gogo」という名前の一致する関数がないため、オーバーロードの解決は失敗します。

13
JaredPar

名前の非表示は、名前解決のあいまいさを防ぐため、理にかなっています。

次のコードを検討してください。

_class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;
_

Base::func(float)がDerivedのDerived::func(double)によって隠されていない場合、floatをdoubleに昇格できる場合でも、dobj.func(0.f)を呼び出すときに基本クラス関数を呼び出します。

リファレンス: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

2
Sandeep Singh