- 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서는 이런 이름 가림 현상은 바람직하지 않다.
- 가려진 이름을 다시 볼 수 있게 하는 방법으로, using 선언 혹은 전달 함수를 쓸 수 있다.
상속된 이름이 숨겨지는 것을 피해라
상속과 관련된 이름 가리기 현상을 보기 전, 이해를 돕기 위해 기본 변수에 대한 이름 가리기 규칙을 살펴보자.
int x;
void someFunc()
{
double x;
std::cin >> x;
}
값을 읽어 x에 넣는 위의 문장에서 실제로 참조하는 x는 전역 변수 x가 아니라 지역 변수 x이다.
이유는 안쪽 유효범위에 있는 이름이 바깥쪽 유효범위에 있는 이름을 가리기 때문이다.
컴파일러가 someFunc의 유효범위 안에서 x라는 이름을 만나면, 일단 그 컴파일러는 자신이 처리하고 있는 유효범위, 즉 지역 유효범위에서 같은 이름을 가진 것이 있는지 알아본다.
유효범위에 x라는 이름이 있기 때문에 다른 유효범위에 대해서는 더 이상 탐색하지 않는다.
C++의 이름 가리기 규칙은 타입과 상관없이 이름을 가린다.
기본 클래스에 속해 있는 것을 파생 클래스 멤버 함수 안에서 참조하는 문장이 있으면 컴파일러는 이 참조 대상을 바로 찾아낼 수 있다. 기본 클래스에 선언된 것은 파생 클래스가 모두 물려받기 때문이다.
이렇게 동작하는 이유는 파생 클래스의 유효범위가 기본 클래스의 유효범위 안에 중첩되어 있기 때문이다.
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();
...
};
데이터 멤버와 멤버 함수의 이름이 public으로 공개되거나 private로 숨겨진 상태가 섞여있다.
mf4가 파생 클래스에서 다음과 같이 구현되어 있다고 가정해 보자.
void Derived::mf4()
{
...
mf2();
...
}
컴파일러는 mf2라는 이름을 보고, mf2가 어느 것에 대한 이름인지를 파악해야 한다.
이때 컴파일러는 유효범위를 탐색하는 방법을 쓴다.
우선 지역 유효범위 내부에서 mf2를 찾지만, 선언된 것이 없다. 그래서 mf4의 유효범위 바깥에 있는 유효범위를 찾는다.
즉, Derived 클래스의 유효범위에서 mf2를 찾는다. 하지만, 여기서도 mf2를 찾을 수 없다.
그다음 컴파일러는 그 바깥 유효범위인 Base 클래스의 유효범위를 찾게 된다. 이제 mf2를 발견하고 탐색을 종료한다.
만약 Base 클래스 유효범위에서 mf2를 발견하지 못했다면 전역 유효범위에서 탐색을 진행한다.
이번에는 mf1, mf3를 오버로드하고, mf3의 오버로드 버전을 Derived에 추가했다 가정해 보자.
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
기본 클래스에 있는 함수들 중에 mf1, mf3 함수는 모두 파생 클래스에도 포함되어 있어 이름이 가려진다.
이름을 탐색하는 시점에서 보면, Base::mf1, Base::mf3는 상속이 되지 않게 된다.
Derived d;
int x;
d.mf1(); // OK, Derived::mf1
d.mf1(x); // Error, Base::mf1
d.mf2(); // OK, Base::mf2
d.mf3(); // OK, Derived::mf3
d.mf3(x); // Error, Base::mf3
기본 클래스와 파생클래스에 있는 이름이 같은 함수들이 받아들이는 매개변수 타입과 상관없이 가려진다.
심지어 가상 함수 여부도 상관이 없다.
어떤 라이브러리 혹은 응용프로그램 프레임워크를 이용하여 파생 클래스를 만들 때, 멀리 떨어져 있는 기본 클래스로부터 오버로드 버전을 상속시키는 경우를 막기 위해 설계되어 이러한 상황이 발생한다.
해결방법
이름이 가려지는 문제를 해결하기 위해서는 두 가지 방법이 있다.
using 선언을 쓰는 것이다.
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1; // Base에 있는 것들 중 mf1, mf3라는 이름을 가진 것들을 Derived에서 볼 수 있음
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
...
};
이제 예상대로 상속된다.
Derived d;
int x;
d.mf1(); // OK, Derived::mf1
d.mf1(x); // OK, Base::mf1
d.mf2(); // OK, Base::mf2
d.mf3(); // OK, Derived::mf3
d.mf3(x); // OK, Base::mf3
어떤 기본 클래스로부터 상속을 받으려고 하는데, 오버로드된 함수가 그 클래스에 들어 있고 이 함수들 중 몇 개만 재정의하고 싶다면, 각 이름에 대해 using 선언을 붙여 주어야 한다.
이렇게 하지 않으면 이름이 가려져 상속을 할 수 없게 된다.
기본 클래스가 가진 함수를 전부 상속했으면 하는 경우가 아니라면 using 선언으로 해결할 수 없다.
예를 들어 Derived가 Base로부터 private 상속이 이루어졌다고 가정해 보자.
그리고 Derived가 상속했으면 하는 함수가 mf1 함수 중 매개변수 없는 버전 하나밖에 없다고 하면, using 선언으로 해결할 수 없다.
using 선언을 이용하면 그 이름에 해당되는 것들이 모두 파생 클래스로 내려가기 때문이다.
이런 경우에는 전달 함수를 만들어 사용하면 된다.
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
...
};
class Dervied: private Base {
public:
virtual void mf1() { Base::mf1(); } // 전달 함수
...
};
...
Derived d;
int x;
d.mf1(); // OK, Derived::mf1()
d.mf1(x); // Error, Base::mf1()에 가려짐
이 방법은 mf1이 암시적 인라인 함수로 동작한다.
기본 클래스의 이름을 파생 클래스의 유효범위로 끌어와 사용하고 싶은데 using 선언을 지원하지 못하는 상황에서 우회적인 방법으로 해결할 수도 있다.
상속이 템플릿과 엮일 경우 전혀 다른 형태로 문제가 생긴다. 이는 다음에 다루기로 하자.