- 상속받은 비가상 함수를 재정의하는 일은 절대로 하지 마라.
상속받은 비가상 함수 재정의 금지
상속받은 비가상 함수를 재정의하는 일은 절대로 해서는 안된다.
class B {
public:
void mf();
...
};
class D: public B { ... };
D 클래스가 B 클래스로부터 public 상속되어 파생되었고, B 클래스에는 mf라는 이름의 public 멤버 함수가 정의되어 있다고 가정해 보자.
D x; // D타입 객체
B *pB = &x;
pB->mf(); // B::mf
문제없이 동작할 것이다.
하지만, mf함수를 상속한다면 문제가 발생한다.
class D: public B {
public:
void mf();
...
};
D x;
B *pB = &x;
pB->mf(); // B::mf 호출
D *pD = &x;
pD->mf(); // D::mf 호출
두 경우모두 x객체로 부터 mf를 호출하는데 서로 다른 함수를 호출하게 된다.
이렇데 동작하는 이유는 비가상 함수는 정적 바인딩으로 묶이기 때문이다.
pB는 B에 대한 포인터 타입이다. pB를 통해 호출되는 비가상 함수는 항상 B 클래스에 정의되어 있을 것이라고 결정해 버린다.
반면, 가상 함수는 동적 바인딩으로 묶인다. 만약 mf 함수가 가상 함수였다면, pB이든 pD이든 항상 D::mf가 호출된다.
pB와 pD가 진짜로 가리키는 대상은 D타입 객체이기 때문이다.
만약 D 클래스를 만드는 도중에 B 클래스로부터 물려받은 비가상 함수인 mf를 재정의해 버리면 D 클래스는 일관성 없는 동작을 보이는 이상한 클래스가 된다.
분명히 D 객체인데도, 이 객체에서 mf 함수를 호출하면 B와 D 중 어느 것을 호출할지 모른다.
심지어 실행할 함수를 결정하는 요인이 객체가 아니라 포인터 타입에 의해 결정된다.
public 상속의 의미는 is-a이다. 비가상 멤버 함수는 클래스 파생에 관계없는 불변동작을 정해 두는 것이다.
이 두 가지 사실을 기반으로 해석해 보면 다음과 같다.
- B객체에 해당되는 모든 것들이 D객체에 그대로 적용된다. D객체는 B객체의 일종이기 때문이다.
- B에서 파생된 클래스는 mf 함수의 인터페이스와 구현을 모두 물려받게 된다. mf는 B클래스의 비가상 멤버 함수이다.
D에서 mf를 재정의하는 것은 설계에 모순이 생기는 것이다.
만약 mf를 B와 다르게 구현하는 것을 원하고, B에서 파생된 클래스는 모두 B의 mf를 사용해야 한다는 말 자체가 모순이 된다.
이런 상황이라면, D는 B로부터 public 상속을 받으면 안 된다.
D는 B로부터 public 상속을 받아 파생시켜야 하고, D에서 mf 함수를 B의 mf와 다르게 구현해야 한다면 mf는 클래스 파생에 상관없이 B에 대한 불변동작을 나타낸다는 점도 모순이다.
이런 경우라면 mf를 가상 함수로 만드는 것이 맞다.
마지막으로, 만약 모든 D가 B의 일종이고 정말 mf가 클래스 파생에 관계없는 B의 불변동작에 해당한다면, D는 mf를 재정의해서는 안된다.
정리하자면, 비가상 함수로 설계된 함수는 절대로 재정의해서는 안된다.