KoreanFoodie's Study

Effective C++ | 항목 39 : private 상속은 심사숙고해서 구사하자 본문

Tutorials/C++ : Advanced

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

GoldGiver 2022. 10. 25. 16:29

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. private 상속의 의미는 is-implmented-in-terms-of(...는 ...를 써서 구현됨) 이다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있다.
2. 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO) 를 활성화시킬수 있다. 이는 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 한다.


public 상속을 private 상속으로 고친 예시를 보자.

class Person {};
class Student: private Person {};

void eat(const Person& p) {}
void study(const Student& s) {}

int main()
{
	Person p;
	Student s;

	eat(p);
	// 에러! Student 는 Person 의 일종이 아니다
	// Student 가 public 상속을 하면 정상 동작한다
	eat(s);
}

public 상속과 달리, 상속 관계가 private 이면 컴파일러는 파생 클래스 객체를 기본 클래스 객체로 변환하지 않는다. private 상속의 의미는 '구현은 물려받을 수 있다. 인터페이스는 국물도 없다' 는 뜻이다. private 상속은 소프트웨어 설계 도중에는 아무런 의미도 갖지 않으며, 단지 소프트웨어 구현(implementation) 중에만 의미를 가진다.
private 상속의 의미는 is-implemented-in-terms-of 인데, 언제 객체 합성을 쓰고 언제 private 상속을 써야 할까? 답은, 되도록이면 객체 합성을 사용하고, 꼭 해야 하는 경우 private 상속을 하는 것이 좋다. 꼭 해야 하는 경우는, 비공개 멤버를 접근하거나 가상 함수를 재정의할 경우가 주로 이에 속한다.
이런 상황을 가정해 보자. Widget 클래스가 있고, 해당 Widget 클래스가 얼마나 사용되었는지를 추적하고 싶다고 가정하겠다. 이때, 우리가 미리 만들어 놓은 Timer 클래스를 재활용하면, 다음과 같이 사용할 수 있다.

class Timer
{
public:
	explicit Timer(int tickFrequency);
	// 일정 시간이 경과할 때마다 자동으로 호출
	virtual void onTick() const;
};

// Widget 은 Timer 와 is-a 관계가 아니므로,
// public 상속이 아니라 private 상속을 사용함!
class Widget: private Timer
{
private:
	// Widget 사용 자료 등을 수집
	virtual void onTick() const;
};


물론 객체 합성을 이용할 수 도 있다. 다음과 같이 말이다.

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

	WidgetTimer timer;
};

위와 같은 방식은, 첫째, Widget 클래스를 설계하는 데 있어 파생은 가능하게 하되, 파생 클래스에서 onTick 을 재정의할 수 없도록 설계 차원에서 막고 싶을 때 유용하다. 또한, Widget 의 컴파일 의존성을 최소화 할 수 있다. 위의 WidgetTimer 클래스의 정의를 Widget 에서 빼내고 Widget 이 WidgetTimer 객체에 대한 포인터만 갖도록 만들어 놓으면, Timer 에 관련된 어떤 것도 #include 할 필요가 없어진다.

객체 합성 보다 private 상속을 선호할 수밖에 없는, 소위 공간 최적화가 얽힌 '만약의 경우'를 한 번 보자.
C++ 는 "독립 구조(freestanding)의 객체는 반드시 크기가 0을 넘어야 한다" 라는 금기사항이 있다. 예시 코드를 보자.

// 정의된 데이터가 없으므로, 객체는 메모리를 사용하지 않아야 한다
// 하지만 원칙상 최소 1 바이트를 할당해야 한다
// 실제로는 바이트 정렬(alignment) 때문에 padding 을 끼워 넣는다
// 그래서 4바이트 크기 정도로 늘어난다 
class Empty {};

// sizeof(HoldsAnInt) > sizeof(int)
class HoldsAnInt
{
private:
	int x;
	Empty e;
};

그런데 위의 금기사항은 "상속을 한 객체" 에 대해서는 적용되지 않는다. 즉, 다음과 같이 쓰면 sizeof(HoldsAnInt) 가 sizeof(int) 와 같아지게 된다!

// sizeof(HoldsAnInt) == sizeof(int)
class HoldsAnInt : private Empty
{
private:
	int x;
};

이 공간 절약 기법은 공백 기본 클래스 최적화(empty base optimization: EBO) 라고 알려져 있으며, 대부분의 컴파일러에서 구현하고 있다. EBO 는 일반적으로 단일 상속하에서만 적용되어, 기본 클래스를 두 개 이상 갖는 파생 클래스에는 EBO 가 적용될 수 없다.
결론적으로 "private 상속을 심사숙고해서 구사하자" 라는 말은 섣불리 이것을 쓸 필요가 없다는 생각을 갖고 주어진 상황에서 두 클래스 사이의 관계를 나타낼 가장 좋은 방법이 private 상속이라는 결론이 나면 쓰라는 뜻이다!

Comments