[C++] 항목 39: private 상속은 심사숙고해서 구사하자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 6. 상속, 그리고 객체 지향 설계

👉🏻 항목 39: private 상속은 심사숙고해서 구사하자

✅ private 상속의 기본 특성

class Person { ... };
// private 상속
class Student : private Person { ... };

void eat(const Person& p);
void study(const Student& s);

int main() {
	Person p; // p는 Person의 일종
	Student s; // s는 Student의 일종

	eat(p); // ✅
	eat(s); // ❌ Student은 Person의 일종이 아니다!
}

private 상속의 특징:

  • private 상속이면, 컴파일러는 일반적으로 파생 클래스 객체(s)를
    기본 클래스 객체(p)로 변환하지 않는다
  • 기본 클래스로부터 물려받은 멤버는 파생 클래스에서 모조리 private 멤버가 된다

핵심:

private 상속의 의미는 is-implemented-in-terms-of(…는 …를 써서 구현된다)이다.
구현만 물려받을 수 있고, 인터페이스는 불가능하다.


📌 기본 원칙

할 수 있다면 항목 38의 객체 합성을 사용하고, 꼭 해야만 한다면 private 상속을 사용하자.

private 상속을 사용하는 경우:

  1. 비공개(protected) 멤버를 접근할 때
  2. 가상 함수를 재정의할 경우
  3. 공백 기본 클래스 최적화(EBO)를 활성화해야 할 때

✅ 예제: Widget 프로파일링

Widget 객체를 사용하는 응용프로그램에서 각 멤버가 몇 번 호출되는지 프로파일하고자 한다.

방법 1: 객체 합성 사용 (권장)

class Timer { ... };

class Widget {
private:
	class WidgetTimer : public Timer {
	public:
		virtual void onTick() const;
		...
	};

	WidgetTimer timer;
	...
};

장점:

  • 현실적으로 private 상속보단 public 상속 + 객체 합성이 자주 쓰인다

이유 1: 설계 제어

  • Widget 클래스 파생은 가능하지만, 파생 클래스에서 onTick 함수를 재정의할 수 없도록
    설계 차원에서 막을 수 있다
  • private 상속으로는 onTick 함수를 재정의할 수 없도록 막을 수 없다

이유 2: 컴파일 의존성 최소화

  • Widget의 컴파일 의존성을 최소화 하고 싶을 때 좋다
  • WidgetTimer의 정의를 Widget으로부터 빼내고,
    WidgetTimer 객체에 대한 포인터만 갖도록 만들면,
    컴파일 의존성을 피할 수 있다 (항목 31 참고)

✅ private 상속 사용 사례 1: 가상 함수 재정의

class Timer {
public:
	explicit Timer(int tickFrequency);

	// 일정 시간이 경과할 때마다,
	// 자동으로 onTick() 함수가 호출된다.
	virtual void onTick() const;
	...
};

class Widget : private Timer {
private:
	virtual void onTick() const;
	...
};

구현 방법:

  • private 상속을 한 후, onTick 함수를 재정의하면 된다

주의점:

onTick 함수를 public 영역에 두면 물거품이 된다


✅ private 상속 사용 사례 2: 공백 기본 클래스 최적화(EBO)

비정적 데이터 멤버가 없는 클래스를 사용해야 하는 경우(공백 클래스):

  • 가상 함수, 가상 기본 클래스 등등 모두 없어야 한다
  • 메모리가 없어야 하는 것이 맞다
  • C++의 규칙: “독립 구조의 객체는 반드시 크기가 0이 넘어야 한다”

🚨 문제 상황: 객체 합성 사용

// 공백 클래스. 메모리 사용하지 말아야 함
class Empty {};

// int 저장할 공간만 있어야 함
class HoldsAnInt {
private:
	int x;
	Empty e;
};

문제:

  • sizeof(HoldsAnInt) > sizeof(int) 가 됨
  • Empty 객체에 char 한 개가 들어감
  • 바이트 정렬이 필요하다 판단되면, 바이트 패딩 과정도 추가됨

✅ 해결: private 상속 사용

class HoldsAnInt : private Empty {
private:
	int x;
};

결과:

  • sizeof(HoldsAnInt) == sizeof(int) 가 됨
  • 공백 기본 클래스 최적화(EBO)라고 함

주의점:

단일 상속에만 적용된다 (기본 클래스 두 개 이상 상속 불가)

실제 활용:

  • STL에도 기술적으로 공백처리된 클래스가 존재한다
  • 예: unary_function, binary_function 등

📊 객체 합성 vs private 상속

특징 객체 합성 private 상속
is-a 관계
의미 has-a 또는 is-implemented-in-terms-of is-implemented-in-terms-of
설계 제어 재정의 방지 가능 재정의 방지 불가
컴파일 의존성 최소화 가능 증가
공백 클래스 최적화 불가 가능
protected 접근 불가 가능
가상 함수 재정의 중첩 클래스 필요 직접 가능

💡 선택 가이드

객체 합성을 사용해야 할 때 (기본):

  • 대부분의 경우
  • 설계 제어가 필요할 때
  • 컴파일 의존성을 최소화하고 싶을 때

private 상속을 사용해야 할 때 (예외적):

  • 기본 클래스의 protected 멤버에 접근해야 할 때
  • 가상 함수를 재정의해야 하는데, 객체 합성으로 복잡해질 때
  • 공백 기본 클래스 최적화(EBO)가 필요할 때

🧐 정리

  1. private 상속의 의미는 is-implemented-in-terms-of(…는 …를 써서 구현됨)이다.
    public 상속의 is-a와는 완전히 다르다.
  2. private 상속을 사용해야 하는 경우는 다음과 같다:
    • 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근할 때
    • 상속받은 가상 함수를 재정의해야 할 경우
    • *공백 기본 클래스 최적화(EBO)**를 활성화시켜야 하는 경우
  3. 웬만해서는 public 상속 + 객체 합성을 사용하자.
    private 상속은 최후의 수단으로 고려하자.
  4. 객체 합성은 설계 유연성과 컴파일 의존성 측면에서 유리하다.
    특별한 이유가 없다면 객체 합성을 선택하자.

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

댓글남기기