[C++] 항목 39: private 상속은 심사숙고해서 구사하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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 상속을 사용하는 경우:
- 비공개(protected) 멤버를 접근할 때
- 가상 함수를 재정의할 경우
- 공백 기본 클래스 최적화(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)가 필요할 때
🧐 정리
- private 상속의 의미는
is-implemented-in-terms-of(…는 …를 써서 구현됨)이다.
public 상속의 is-a와는 완전히 다르다. - private 상속을 사용해야 하는 경우는 다음과 같다:
- 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근할 때
- 상속받은 가상 함수를 재정의해야 할 경우
- *공백 기본 클래스 최적화(EBO)**를 활성화시켜야 하는 경우
- 웬만해서는
public 상속 + 객체 합성을 사용하자.
private 상속은 최후의 수단으로 고려하자. - 객체 합성은 설계 유연성과 컴파일 의존성 측면에서 유리하다.
특별한 이유가 없다면 객체 합성을 선택하자.
댓글남기기