항목 36 : 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
1
2
3
4
5
6
7
|
class B {
public :
void mf();
...
};
class D: public B {...};
|
B나 D, 혹은 mf에 대해 전혀 모르는 상태에서 D 타입의 객체인 x가 다음처럼 있다고 할 때,
1
|
D x; // x는 D 타입으로 생성된 객체입니다.
|
다음과 같이 작성한 코드가,
1
2
3
|
B *pB = &x; // x에 대한 포인터를 얻어냅니다.
pB->mf(); // 이 포인터를 통해 mf를 호출합니다.
|
다음처럼 동작하지 않으면 꽤나 황당할 것입니다.
1
2
3
|
D *pD = &x; // x에 대한 포인터를 얻어냅니다.
pD->mf(); // 이 포인터를 통해 mf를 호출합니다.
|
황당한 이유는 간단합니다. 양쪽의 경우에서 한결같이 x 객체로부터 mf 멤버 함수를 호출하고 있기 때문입니다. 함수도 똑같고 객체도 똑같으니, 동작도 같아야 한다고 생각을 할테니까요.
그런데 다를 수도 있다는 게 문제입니다. 특히, mf가 비가상 함수이고 D 클래스가 자체적으로 mf 함수를 또 정의하고 있으면 아래와 같은 황당한 동작이 나오게 됩니다.
1
2
3
4
5
6
7
8
9
|
class D: public B {
public :
void mf(); // B::mf를 가려 버립니다. 항목 33 참조
...
};
pB->mf(); // B::mf를 호출합니다.
pD->mf(); // D::mf를 호출합니다.
|
이렇게 다른 동작을 하는 이유는 B::mf 및 D::mf 등의 비가상 함수는 정적 바인딩으로 묶이기 대문입니다. pB는 'B에 대한 포인터' 타입으로 선언되었기 때문에, pB를 통해 호출되는 비가상 함수는 항상 B 클래스에 정의되어 있을 것이라고 결정해 버린다는 말입니다. 심지어 B에서 파생된 객체를 pB가 가리키고 있다 해도 마찬가지입니다.
반면, 가상 함수의 경우엔 동적 바인딩으로 묶입니다. 비가상 함수와 같은 문제로 골머리를 썩을 이유가 없습니다. 만약 mf 함수가 가상 함수였다면, mf가 pB에서 호출되든 pD에서 호출되는 D::mf가 호출됩니다. pB 및 pD가 진짜로 가리키는 대상은 D 타입의 객체이니까요.
public 상속의 의미는 "is-a 입니다". 그리고 비가상 멤버 함수는 클래스 파생에 관계없는 불변동작을 정해 두는 거라고 이야기 했었습니다. 이 두가지 포인트를 B, D 클래스 및 비가상 멤버 함수인 B::mf에 그대로 가져가면, 이렇게 풀 수 있습니다.
1. B 객체에 해당되는 모든 것들이 D 객체에 그대로 적용됩니다. 왜냐하면 모든 D 객체는 B 객체의 일종이기 때문입니다.
2. B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받게 됩니다. mf는 B 클래스에서 비가상 멤버 함수이기 때문입니다.
이제 D에서 mf를 재정의를 하게되면 그 순간 설계에 모순이 생깁니다. mf를 B와 다르게 구현한 것이 원해서 그런것이고 B 및 B의 파생 클래스로부터 만들어진 모든 객체가 B의 mf 구현을 사용해야 한다고 정한 것이 진짜라면, mf의 재정의로 인해 '모든 D는 B의 일종'이란 명제는 거짓이 됩니다.
D는 B로부터 public 상속을 받아 파생시킬 수밖에 없는 사정이 있고, 진짜로 D에서 mf 함수를 B의 그것과 다르게 구현해야 한다면, 'mf는 클래스 파생에 상관없이 B에 대한 불변동작을 나타낸다'라는 점도 참이 아니게 됩니다.
마지막으로, 모든 D가 B의 일종이고 정말 mf가 클래스 파생에 상관없는 B의 불변동작에 해당한다면, D에서는 mf를 재정의할 생각도 할 수 없습니다.
실제적이든 이론적이든, 받아들이는 입장에서는 문제일 수밖에 없습니다. 그리고 어떤 상황에서도 상속받은 비가상 함수를 재정의하는 것은 절대 금물입니다.
꼭 잊지 말아야 할 것!
상속받은 비가상 함수를 재정의하는 일은 절대로 하지 맙시다.
'언어 > C++' 카테고리의 다른 글
[Effective C++] "has-a(...는...를 가짐)" 혹은 "is-implemented-in-terms-of(...는...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 (0) | 2020.02.07 |
---|---|
[Effective C++] 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 (0) | 2020.02.06 |
[Effective C++] 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 (0) | 2020.02.04 |
[Effective C++] 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자 (0) | 2020.02.03 |
[Effective C++] 상속된 이름을 숨기는 일은 피하자 (0) | 2020.02.02 |