파생 클래스에서 재정의 된 함수가 기본 클래스의 다른 오버로드를 숨기는 이유는 무엇입니까?


220

코드를 고려하십시오 :

#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.cpp :`int main () '함수에서 :
test.cpp : 31 : 오류 : 'Derived :: gogo (int)'호출에 대한 일치하는 함수가 없습니다.
test.cpp : 21 : 참고 : 후보자는 virtual void Derived :: gogo (int *) 
test.cpp : 33 : 2 : 경고 : 파일 끝에 줄 바꿈 없음
종료 코드 : 1

여기서 파생 클래스의 함수는 기본 클래스에서 동일한 이름 (서명 아님)의 모든 함수를 이클립스에서 처리합니다. 어떻게 든이 C ++의 동작이 제대로 보이지 않습니다. 다형성이 아닙니다.



8
화려한 질문, 나도 이것을 최근에 발견했다
Matt Joiner

11
Bjarne (Mac이 게시 한 링크에서)은 "C ++에서는 범위 전체에 오버로드가 없습니다. 파생 클래스 범위는이 일반적인 규칙의 예외가 아닙니다."
sivabudh

7
@Ashish 링크가 끊어졌습니다. 다음은 올바른 것입니다 (현재) -stroustrup.com/bs_faq2.html#overloadderived
nsane

3
또한 obj.Base::gogo(7);숨겨진 함수를 호출하여 여전히 작동하는 것을 지적하고 싶었습니다 .
forumulator

답변:


406

당신의 질문의 말로 판단하면 ( "숨기기"라는 단어를 사용했습니다), 당신은 이미 여기서 무슨 일이 일어나고 있는지 알고 있습니다. 이 현상을 "이름 숨기기"라고합니다. 어떤 이유로 누군가가 이름 숨기기가 발생 하는 이유 에 대해 질문 할 때마다 응답하는 사람들은 "이름 숨기기"라고 말하고 작동 방식 (아마 이미 알고 있음)을 설명하거나 무시하는 방법 ( 에 대해 묻지 않았지만 실제 "이유"질문을 다루는 사람은 아무도 없습니다.

이름 숨김의 근거, 즉 실제로 C ++로 설계된 이유에 대한 결정 은 상속 된 오버로드 된 함수 집합이 현재의 주어진 클래스에서 과부하. C ++에서 과부하 해결은 후보 세트에서 최상의 기능을 선택하여 작동한다는 것을 알고있을 것입니다. 이는 인수 유형을 매개 변수 유형과 일치시켜 수행됩니다. 일치 규칙은 때때로 복잡 할 수 있으며 종종 준비되지 않은 사용자에 의해 비논리적 인 것으로 인식 될 수 있습니다. 기존 기능 세트에 새 기능을 추가하면 과부하 해결 결과가 다소 급격히 변할 수 있습니다.

예를 들어, 기본 클래스 에 유형의 매개 변수를 사용 B하는 멤버 함수 foovoid *있고에 대한 모든 호출 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)하나의 함수 로 해결하도록 요청하는 반면, 내부 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-declaration을 사용합니다.

원래 게시물에서 올바르게 관찰 한 것처럼 ( "다형성이 아님"설명 참조)이 동작은 클래스 간 IS-A 관계 위반으로 간주 될 수 있습니다. 이것은 사실이지만, 분명히 그 당시에는 숨기는 것이 더 악한 것으로 판명되었습니다.


22
예, 이것은 질문에 대한 실제 답변입니다. 감사합니다. 나도 궁금했다.
Omnifarious

4
좋은 대답입니다! 또한 실제적으로 이름 검색이 매번 맨 위로 이동해야한다면 컴파일 속도가 훨씬 느려질 수 있습니다.
Drew Hall

6
(오래된 대답입니다.) 이제 nullptr" void*버전 을 호출하려면 포인터 유형을 사용해야합니다 "라고 말하여 예제에 반대 할 것 입니다. 이것이 좋지 않은 다른 예가 있습니까?
GManNickG

3
숨어있는 이름은 실제로 악하지 않습니다. "is-a"관계는 여전히 존재하며 기본 인터페이스를 통해 사용할 수 있습니다. 따라서 d->foo()"Is-a Base"는 얻지 static_cast<Base*>(d)->foo() 못할 것이지만 동적 디스패치를 ​​포함하여 의지 할 것 입니다.
커렉 SB

12
주어진 예제가 숨기거나 숨기지 않고 동일하게 동작하기 때문에이 답변은 도움이되지 않습니다.
Richard Wolf

46

이름 확인 규칙에 따르면 이름 조회는 일치하는 이름이있는 첫 번째 범위에서 중지됩니다. 이때 과부하 해결 규칙은 사용 가능한 기능 중 가장 일치하는 것을 찾기 위해 시작됩니다.

이 경우 gogo(int*)파생 클래스 범위에서 (단독) 발견되며 int에서 int *로 표준 변환이 없으므로 조회가 실패합니다.

해결책은 Derived 클래스의 using 선언을 통해 Base 선언을 가져 오는 것입니다.

using Base::gogo;

... 이름 조회 규칙에서 모든 후보를 찾을 수 있으므로 예상대로 과부하 해결이 진행됩니다.


10
OP : "파생 클래스에서 재정의 된 함수가 기본 클래스의 다른 과부하를 숨기는 이유는 무엇입니까?" 이 답변은 "그렇기 때문에"입니다.
Richard Wolf

12

이것은 "디자인으로"입니다. C ++ 에서이 유형의 메소드에 대한 과부하 해결은 다음과 같이 작동합니다.

  • 참조 유형에서 시작하여 기본 유형으로 이동하여 "gogo"라는 메소드가있는 첫 번째 유형을 찾으십시오.
  • 해당 유형에서 "gogo"라는 메소드 만 고려하면 일치하는 과부하를 찾습니다.

파생에는 "gogo"라는 일치하는 함수가 없으므로 과부하 해결에 실패합니다.


2

이름 숨기기는 이름 확인의 모호성을 방지하므로 의미가 있습니다.

이 코드를 고려하십시오.

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

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

Derived dobj;

Derived에서 Base::func(float)가려지지 않은 경우 float을 double로 승격 할 수는 있지만 Derived::func(double)호출 할 때 기본 클래스 함수를 호출 dobj.func(0.f)합니다.

참조 : http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.