[C++] 항목 36: 상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물!

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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 상속이나 컴포지션을 고려하자.

🧐 정리

  1. 상속받은 비가상 함수를 재정의하는 일은 절대 하지 말자.
  2. 비가상 함수는 정적 바인딩되므로, 포인터 타입에 따라 다른 함수가 호출되는 모순이 발생한다.
  3. public 상속에서 비가상 함수를 재정의하면 is-a 관계불변동작 원칙을 모두 위반하게 된다.
  4. 파생 클래스에서 다른 동작이 필요하다면, 처음부터 가상 함수로 선언하거나 다른 설계 방식을 사용하자.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기