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


219

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

#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:注:候補は次のとおりです:virtual void Derived :: gogo(int *) 
test.cpp:33:2:警告:ファイルの終わりに改行がありません
>終了コード:1

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



8
素晴らしい質問、私も最近これだけを発見しました
Matt Joiner

11
私はBjarne(Macが投稿したリンクから)を1文で最もよく表現していると思います。
sivabudh 2010

7
@Ashishそのリンクは壊れています。これが正しいものです(現時点
nsane

3
また、obj.Base::gogo(7);hidden関数を呼び出すことで機能することを指摘したかったのです。
18:30

回答:


406

質問の文言(「隠す」という言葉を使用した)から判断すると、ここで何が行われているのかはすでにわかっています。この現象は「名前の非表示」と呼ばれます。何らかの理由で、名前の非表示が発生する理由について誰かが質問するたびに、応答する人は、これが「名前の非表示」と呼ばれ、それがどのように機能するかを説明します(おそらくすでに知っています)、またはそれを上書きする方法を説明します(質問されたことはありません)が、実際の「なぜ」の質問に対処する気がないようです。

名前非表示の背後にある根拠、つまり実際に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{
    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リレーションシップの違反と見なされる可能性があります。これは本当ですが、どうやら当時、名前を隠すことはそれほど悪ではないことが判明することが判明しました。


22
はい、これは質問に対する本当の答えです。ありがとうございました。私も気になった。
全面的2009年

4
正解です。また、実際問題として、名前の検索を毎回最上位に移動する必要がある場合、コンパイルはおそらくかなり遅くなります。
ドリューホール

6
(古い答えですが、わかりました。)では、nullptrvoid*バージョンを呼び出したい場合は、ポインタ型を使用する必要があります」と言って、あなたの例に反対します。これが良くない別の例はありますか?
GManNickG

3
名前を隠すことは本当に悪ではありません。「is-a」関係はまだ存在しており、基本インターフェースを介して使用できます。そのd->foo()ため、「Is-a Base」を取得できない場合がありますstatic_cast<Base*>(d)->foo() 、動的ディスパッチを含めます。
Kerrek SB、2014

12
与えられた例は非表示があってもなくても同じように動作するため、この回答は役に立ちません。D:: foo(int)は、より適切な一致であるか、B:foo(int)が非表示であるために呼び出されます。
リチャードウルフ

46

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

この場合、gogo(int*)はDerivedクラススコープで(単独で)見つかります。intからint *への標準的な変換がないため、検索は失敗します。

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

using Base::gogo;

...名前の検索ルールですべての候補を検索できるため、オーバーロードの解決は期待どおりに進みます。


10
OP:「派生クラスでオーバーライドされた関数が基本クラスの他のオーバーロードを隠すのはなぜですか?」この答え:「そうするから」。
リチャードウルフ

12

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

  • 参照のタイプから始めて、ベースタイプに移動して、「gogo」という名前のメソッドを持つ最初のタイプを見つけます。
  • そのタイプで「gogo」という名前のメソッドのみを検討すると、一致するオーバーロードが見つかります

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


2

名前の非表示は、名前解決のあいまいさを防ぐので意味があります。

このコードを考えてみましょう:

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

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

Derived dobj;

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.