33: 상속된 이름을 숨기는 일은 피하자
int x;
void someFunc()
{
double x;
cin >> x;
}
안쪽 유효범위에 있는 이름이 바깥쪽 유효범위에 있는 이름을 가리기때문에 실제로 참조하는 x는 전역 변수 x가 아니라 지역 변수 x이다.
컴파일러가 someFunc의 유효범위 안에서 x를 만나면, 일단 그 컴파일러는 자신이 처리하고 있는 유효범위, 즉 지역 유효범위(local scope)를 뒤져서 같은 이름을 가진 것이 있는 가를 알아본다. x가 바로 있기 때문에, 이 이외의 유효범위에 대해서는 더 탐색하지 않는다.
파생 클래스 멤버 함수 안에서 참조하는 문장이 있으면 컴파일러는 이 참조 대상을 바로 찾아낼 수 있다. 기본 클래스에 선언된 것은 파생 클래스가 모두 물려받기 때문이다. 사실 이렇게 동작하는 이유는 파생 클래스의 유효범위가 기본 클래스의 유효범위 안에 중첩되어 있기 때문이다.
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
};
class Derived : public Base {
public:
virtual void mf1();
void mf4();
};
위의 예제에서 중요한 건 이름에 대한 것이다. 무엇의 이름이냐 하는 것은 신경 쓰지 않는다. mf4가 파생 클래스에서 다음과 같이 구현되어 있다고 가정해 보자.
void Derived::mf4()
{
mf2();
}
컴파일러는 이 mf2가 어느 것에 대한 이름인지를 파악해야 하는 것이 급선무이다. 컴파일러는 유효 범위를 탐색하는 방법을 쓴다. 우선 지역 유효범위 내부를 뒤져 보는데, mf2라 불리는 어떤 것도 선언된게 없다. 그래서 mf4의 유효범위 바깥에서 감싸고 있는 유효범위를 찾는다. 지금의 경우 Derived 클래스의 유효범위이다. 하지만 여전히 mf2라는 이름을 가진 것이 보이지 않으므로, 다음 유효범위인 Base 클래스의 유효범위로 옮겨 간다. 드디어 mf2를 찾아내고 탐색이 끝난다.
이제 다음의 예제를 보자.
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int a);
virtual void mf2();
void mf3();
void mf3(double a);
};
class Derived : public Base {
public:
virtual void mf1();
void mf3();
void mf4();
};
기본 클래스에 있는 함수들 중에 mf1 및 mf3이라는 이름이 붙은 것은 모두 파생 클래스에 들어 있는 mf1과 mf3에 의해 가려지고 만다. 이름 탐색의 시점에서 보면, 어처구니없게도 Base::mf1과 Base::mf3은 Derived가 상속한 것이 아니게 된다.
Derived d;
int x;
d.mf1();
d.mf1(x); // 에러!
d.mf2();
d.mf3();
d.mf3(x); // 에러!
이런 이름 가리기는 받아들이는 매개변수 타입이 다르거나 말거나 상관없다. 심지어 함수들이 가상 함수인지 비가상 함수인지의 여부에도 상관없이 이름이 가려진다.
이렇게 동작하는 이유가 있다. 멀리 떨어져 있는 기본 클래스로부터 오버로드 버전을 상속시키는 경우를 막겠다는 것이다. 일종의 실수로 간주하겠다는 것이다.
가려진 이름은 using 선언을 써서 끄집어낼 수 있다.
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int a);
virtual void mf2();
void mf3();
void mf3(double a);
};
class Derived : public Base {
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};
이제 예상한 대로 돌아가는 상속이 되었다.
Derived d;
int x;
d.mf1();
d.mf1(x); // 이제 Base::mf1을 호출한다.
d.mf2();
d.mf3();
d.mf3(x); // 이제 Base::mf3을 호출한다.
어떤 클래스로부터 상속을 받으려고 하는데, 오버로드된 함수가 그 클래스에 들어 있고 이 함수들 중 몇 개만 재정의하고 싶다면, 각 이름에 대해 using 선언을 붙여 주어야 한다는 것이다.
기본 클래스가 가진 함수를 전부 상속했으면 하는 것이 아닌 경우도 있다. 물론 이 경우는 public 상속은 생각하지 말아야 한다. 기본 클래스와 파생 클래스 사이의 is-a 관계가 깨져 버리기 때문이다. 하지만 private 상속을 사용한다면 이 경우가 말이 될 수 있다. 이때는 using 선언으로 해결할 수 없다. 그 이유는 using 선언을 내리면 그 이름에 해당되는 것들이 모두 파생 클래스로 내려가 버리기 때문이다. 간단한 전달 함수(forwarding function)을 만들어 해결한다.
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int a);
};
class Derived : private Base {
public:
virtual void mf1()
{
Base::mf1();
}
};
Derived d;
int x;
d.mf1(); // Derived::mf1 (매개변수 없는 버전)을 호출
d.mf1(x); // 에러! Base::mf1()은 가려져 있다.
이것만은 잊지 말자!
- 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서는 이런 이름 가림 현상은 바람직하지 않다.
- 가려진 이름을 다시 볼 수 있게 하는 방법으로, using 선언 혹은 전달 함수를 쓸 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 (1) | 2023.11.04 |
---|---|
[Effective C++] 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자 (0) | 2023.11.02 |
[Effective C++] 32: public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자 (1) | 2023.10.31 |
[Effective C++] 31: 파일 사이의 컴파일 의존성을 최대로 줄이자 (1) | 2023.10.31 |
[Effective C++] 30: 인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2023.10.29 |