[C++] 항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 6. 상속, 그리고 객체 지향 설계
👉🏻 항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!
✅ 비가상 함수의 기본 동작
다음은 일반적인 상속 구조에서 비가상 함수가 어떻게 동작하는지 보여준다:
class B {
public:
void mf();
...
};
class D : public B { ... };
int main() {
D x;
B* pB = &x;
pB->mf(); // B::mf() 호출
D* pD = &x;
pD->mf(); // B::mf() 호출
}
pB->mf()와pD->mf()모두B::mf()를 호출한다.- 포인터 타입과 관계없이 동일한 함수가 호출된다.
✅ 비가상 함수를 재정의하면 생기는 문제
이제 파생 클래스에서 비가상 함수를 재정의해보자:
class B { ... }; // 이전과 동일
class D : public B {
public:
void mf(); // B::mf()를 가림
...
};
int main() {
D x;
B* pB = &x;
pB->mf(); // B::mf() 호출
D* pD = &x;
pD->mf(); // D::mf() 호출
}
pB->mf()는B::mf()를 호출한다.pD->mf()는D::mf()를 호출한다.
문제: 같은 객체 x를 가리키는데도 포인터 타입에 따라 다른 함수가 호출된다!
✅ 왜 이런 일이 발생하는가?
원인은 바인딩 방식의 차이 때문이다:
- 비가상 함수는 정적 바인딩(static binding) 으로 묶인다.
- 컴파일 시점에 포인터/참조의 선언된 타입을 기준으로 함수가 결정된다.
- 가상 함수는 동적 바인딩(dynamic binding) 으로 묶인다.
- 런타임에 포인터/참조가 실제로 가리키는 객체의 타입을 기준으로 함수가 결정된다.
따라서 비가상 함수를 재정의하면, 객체를 어떤 타입의 포인터로 가리키느냐에 따라,
다른 함수가 호출되는 이상한 상황이 발생한다.
✅ 문제 1: is-a 관계 위반
public 상속은 “is-a(…는 …의 일종이다)” 관계를 나타낸다.
- 모든 D는 B의 일종이다.
- D 타입 객체는 어디서든 B 타입 객체처럼 동작해야 한다.
하지만 비가상 함수 mf()를 재정의하면:
D x;
B* pB = &x;
D* pD = &x;
pB->mf(); // B의 동작
pD->mf(); // D의 동작 (다름!)
- 같은 객체가 포인터 타입에 따라 다르게 동작한다.
- 이는 is-a 관계를 거짓으로 만든다.
✅ 문제 2: 불변동작 위반
항목 34에 따르면, 비가상 멤버 함수는 클래스 파생에 관계없는 불변동작을 정하는 것이다.
B::mf()는 모든 B 객체(파생 클래스 포함)에서 동일하게 동작해야 하는 불변동작이다.- 만약
D::mf()가B::mf()와 다른 동작을 한다면, 이는 불변동작이 아니다.
결론:
B::mf()와D::mf()는 서로 같은 동작(불변 동작) 이어야 한다.- 만약 다른 동작이 필요하다면, 애초에
mf()를 비가상 함수로 선언하지 말았어야 한다. mf()를 재정의하는 것은 이 조건을 거짓으로 만든다.
✅ 올바른 설계 방법
만약 파생 클래스에서 다른 동작이 필요하다면:
1. 가상 함수로 선언하라:
class B {
public:
virtual void mf(); // 가상 함수로 선언
...
};
class D : public B {
public:
virtual void mf() override; // 재정의 허용
...
};
이제 포인터 타입과 관계없이 실제 객체의 타입에 맞는 함수가 호출된다.
2. 또는 애초에 public 상속을 사용하지 말라:
- D가 B와 다른 동작을 해야 한다면, D는 B의 일종이 아닐 수 있다.
- 이 경우 private 상속이나 컴포지션을 고려하자.
🧐 정리
- 상속받은 비가상 함수를 재정의하는 일은 절대 하지 말자.
- 비가상 함수는 정적 바인딩되므로, 포인터 타입에 따라 다른 함수가 호출되는 모순이 발생한다.
- public 상속에서 비가상 함수를 재정의하면 is-a 관계와 불변동작 원칙을 모두 위반하게 된다.
- 파생 클래스에서 다른 동작이 필요하다면, 처음부터 가상 함수로 선언하거나 다른 설계 방식을 사용하자.
댓글남기기