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

StackOverflow https://stackoverflow.com/questions/1628768

  •  06-07-2019
  •  | 
  •  

質問

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

#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: In function `int main()':
test.cpp:31: error: no matching function for call to `Derived::gogo(int)'
test.cpp:21: note: candidates are: virtual void Derived::gogo(int*) 
test.cpp:33:2: warning: no newline at end of file
>Exit code: 1

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

役に立ちましたか?

解決

質問の文言(「隠す」という言葉を使用しました)から判断すると、ここで何が起こっているかはすでにわかっています。この現象を「名前隠し」といいます。どういうわけか、誰かが質問するたびに、 なぜ 名前の隠蔽が起こると、それに応じる人は、これを「名前の隠蔽」と呼んで、それがどのように機能するかを説明するか(おそらくすでに知っているでしょう)、またはそれを上書きする方法を説明します(これについて尋ねたことはありません)。しかし、誰も対処しようとはしていないようです。実際の「なぜ」の質問。

その決定、名前を隠す背後にある理論的根拠、つまり なぜ これは実際には 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 関係の違反とみなされる可能性があります。これは事実ですが、どうやら当時は名前を隠すことが最終的にはそれほど悪ではないことが判明するだろうと判断されていたようです。

他のヒント

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

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

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

using Base::gogo;

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

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

  • 参照のタイプから開始してベースタイプに移動し、&quot; gogo&quot;という名前のメソッドを持つ最初のタイプを見つけます
  • &quot; gogo&quot;という名前のメソッドのみを考慮するそのタイプで、一致するオーバーロードを見つけます

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

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

このコードを検討してください:

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)によって隠されていなかった場合、 dobj.func(0.f)(floatをdoubleに昇格できる場合でも)。

参照: http://bastian.rieck.ru/blog/posts/ 2016 / name_hiding_cxx /

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top