로또
36. 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물! 본문
다루는 내용
상속받은 비가상 함수를 재정의할 때 발생하는 문제점을 알아보자.
비가상 함수의 재정의 문제
1. 일관성 없는 동작
#include <iostream>
using namespace std;
class Base{
public:
void MF(){ cout << "Base::MF()" << endl; }
};
class Derived: public Base{
public:
void MF(){ cout << "Derived::MF()" << endl; }
};
int main(){
Derived d;
Base* pb = &d;
Derived* pd = &d; // pb == pd
pb->MF();
pd->MF();
return 0;
}
위 코드의 실행 결과를 짐작해보자.
Base* pb와 Derived* pd 는 모두 Dervied 객체인 d의 주소값을 취하고 있다. pb와 pd 모두 x 객체의 MF 함수를 호출하고 있으니 두 함수 호출의 결과는 동일해야 할 것이다.
하지만 실제 실행 결과는 다음과 같다.

위와 같은 원인은 MF()와 같은 비가상 함수는 정적 바인딩(static binding)으로 결정되기 때문이다. 즉, 컴파일 타임에 실행될 함수가 결정되어 Base* 타입 변수인 pb는 Base::MF()를, Derived* 타입 변수인 pd는 Derived::MF()를 호출하게 된다.
결론적으로 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 파생 클래스가 일관성없는 동작을 보이는 이상한 클래스로 만드는 것이다.
분명히 Derived 객체임에도 함수를 호출했을 때, Base 클래스의 함수를 호출할 수도, Derived 클래스의 함수를 호출할 수도 있다는 것이다. 특히, 어떤 클래스의 함수를 호출할 지 결정하는 요인이 해당 객체 자신이 아니라 객체를 가리키는 포인터의 타입에 의해 좌우된다는 것은 문제를 일으킬 여지가 분명하다.
포인터가 아닌 참조자 또한 이와 동일하다.
만약, MF()가 가상 함수였다면 동적 바인딩(dynamically binding)으로 결정되어 Base*에 의해 호출되든 Derived*에 의해 호출되든 일관성을 가져 pb및 pd가 실제로 가리키는 객체의 타입에 맞게 Derived::MF()가 호출될 것이다.
2. 설계의 모순
앞서 다음 두 가지 내용을 다루었을 것이다.
- public 상속의 의미는 is-a(...는 ...의 일종이다) 이다. (항목 32)
- 비가상 멤버 함수는 클래스 파생에 관계없는 불변 동작을 정해 두는 것이다. (항목 34)
이를 위 코드에 적용시켜 보면, Dervied 객체는 Base 객체의 일종이며, MF 함수는 비가상 멤버 함수이므로 클래스에 관계 없이 동일한 동작을 보여야 한다.
하지만 Derived에서 MF()를 재정의하는 순간, 설계에 모순이 발생한다. MF 함수를 Base와 다르게 구현할 필요가 있었다면 모든 Derived는 Base의 일종이라는 말은 거짓이 되며, MF 함수는 더 이상 클래스 파생에 관계없는 불변 동작이 아니다. 이러한 경우에는 Derived는 Base로부터 public 상속을 사용해선 안된다.
이것만은 잊지 말자!
- 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 말자.
'책 > Effective C++' 카테고리의 다른 글
| 38. has-a(...는 ...를 가짐) 혹은 is-implemented-in-terms-of(...는 ...를 써서 구현됨)를 모형화할 때는 객체 합성을 사용하자 (1) | 2023.12.07 |
|---|---|
| 37. 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자. (0) | 2023.12.06 |
| 35. 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자. (0) | 2023.12.04 |
| 34. 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자. (1) | 2023.12.02 |
| 33. 상속된 이름을 숨기는 일은 피하자. (0) | 2023.12.01 |