파생 클래스
파생 클래스 (Derived classes)
클래스 타입은 (class든 struct든) 하나 이상의 기반 클래스(base class)로부터 파생될 수 있어요. 기반 클래스는 다시 자기 자신의 기반으로부터 파생될 수 있고, 이렇게 해서 상속 계층(inheritance hierarchy) 이 형성돼요. 파생 클래스를 선언하면 그 객체는 기반 클래스의 부분 객체(subobject)를 포함하게 돼요.
출처: cppreference
본문
문법 (Syntax)
기반 클래스들의 목록은 클래스 선언 문법의 기반 절(base-clause) 에 제공돼요. 기반 절은 콜론 : 뒤에 콤마로 구분된 하나 이상의 기반 지정자(base-specifier)로 이루어져요.
| 표기 | 의미 |
|---|---|
attr(선택) class-or-computed |
(1) 기본 멤버 접근성을 가진 비가상 상속 |
attr(선택) virtual class-or-computed |
(2) 기본 멤버 접근성을 가진 가상 상속 |
attr(선택) access-specifier class-or-computed |
(3) 주어진 멤버 접근성을 가진 비가상 상속 |
attr(선택) virtual access-specifier class-or-computed |
(4) 주어진 멤버 접근성을 가진 가상 상속 |
attr(선택) access-specifier virtual class-or-computed |
(5) (4)와 같음. virtual과 access-specifier는 어떤 순서로든 올 수 있음 |
attr— (C++11부터) 임의 개수의 속성.access-specifier—private,public,protected중 하나.class-or-computed—nested-name-specifier(선택) type-name,nested-name-specifier template simple-template-id,decltype-specifier(C++11부터),pack-index-specifier(C++26부터) 중 하나.
문법 제약 때문에 정교한 타입 지정자(elaborated type specifier)는 class-or-computed로 직접 나타날 수 없어요. (C++11부터) 기반 절의 base-specifier는 팩 확장(pack expansion) 일 수 있고, final로 선언된 클래스·구조체는 class-or-computed로 나타낼 수 없어요.
access-specifier를 생략하면, struct class-key로 선언된 파생 클래스에서는 public, class class-key로 선언된 파생 클래스에서는 private이 기본이 돼요.
struct Base
{
int a, b, c;
};
// Derived 타입의 모든 객체는 Base를 부분 객체로 포함
struct Derived : Base
{
int b;
};
// Derived2 타입의 모든 객체는 Derived와 Base를 부분 객체로 포함
struct Derived2 : Derived
{
int c;
};
기반 절에 나열된 class-or-computed가 가리키는 클래스들은 직접 기반 클래스(direct base class) 예요. 그들의 기반들은 간접 기반 클래스(indirect base class) 예요. 같은 클래스를 직접 기반으로 두 번 이상 지정할 수는 없지만, 한 클래스가 직접 기반이면서 동시에 간접 기반일 수는 있어요.
각 직접·간접 기반 클래스는 파생 클래스의 객체 표현 안에 기반 클래스 부분 객체로, ABI에 따라 정해지는 오프셋에 존재해요. 빈 기반 클래스는 빈 기반 최적화(empty base optimization) 덕분에 보통 파생 객체의 크기를 늘리지 않아요. 기반 클래스 부분 객체의 생성자는 파생 클래스의 생성자가 호출해요. 그 생성자들에 인자를 주려면 멤버 이니셜라이저 목록(member initializer list)에서 제공하면 돼요.
가상 기반 클래스 (Virtual base classes)
virtual로 지정된 각각의 서로 다른 기반 클래스에 대해, 가장 파생된(most derived) 객체는 그 타입의 기반 클래스 부분 객체를 단 하나만 포함해요. 심지어 그 클래스가 상속 계층에 여러 번 나타나도(매번 가상으로 상속되기만 한다면) 마찬가지예요.
struct B { int n; };
class X : public virtual B {};
class Y : virtual public B {};
class Z : public B {};
// AA 타입의 모든 객체는 X, Y, Z와 B를 두 개 갖습니다:
// 하나는 Z의 기반이고, 하나는 X와 Y가 공유
struct AA : X, Y, Z
{
AA()
{
X::n = 1; // 가상 B 부분 객체의 멤버 수정
Y::n = 2; // 같은 가상 B 부분 객체의 멤버 수정
Z::n = 3; // 비가상 B 부분 객체의 멤버 수정
std::cout << X::n << Y::n << Z::n << '\n'; // 223 출력
}
};
가상 기반 클래스를 가진 상속 계층의 대표적인 예는 표준 라이브러리의 iostream 계층이에요. std::istream과 std::ostream은 std::ios를 가상 상속으로 파생해요. std::iostream은 std::istream과 std::ostream 둘 다에서 파생되므로, std::iostream의 모든 인스턴스는 std::ostream 부분 객체 하나, std::istream 부분 객체 하나, 그리고 std::ios 부분 객체는 단 하나(결과적으로 std::ios_base도 하나)를 포함해요.
모든 가상 기반 부분 객체는 어떤 비가상 기반 부분 객체보다 먼저 초기화돼요. 그래서 가장 파생된 클래스만 멤버 이니셜라이저 목록에서 가상 기반의 생성자를 호출해요.
struct B
{
int n;
B(int x) : n(x) {}
};
struct X : virtual B { X() : B(1) {} };
struct Y : virtual B { Y() : B(2) {} };
struct AA : X, Y { AA() : B(3), X(), Y() {} };
// AA의 기본 생성자는 X와 Y의 기본 생성자를 호출하지만,
// 그 생성자들은 B가 가상 기반이므로 B의 생성자를 호출하지 않음
AA a; // a.n == 3
// X의 기본 생성자는 B의 생성자를 호출
X x; // x.n == 1
가상 상속이 관여하면 클래스 멤버에 대한 미한정 이름 검색(qualified/unqualified name lookup) 에 특별한 규칙이 적용돼요(간혹 지배 규칙(dominance) 이라고 불려요).
public 상속 (Public inheritance)
클래스가 public 접근 지정자로 기반을 상속하면, 기반 클래스의 모든 public 멤버는 파생 클래스의 public 멤버로, 모든 protected 멤버는 파생 클래스의 protected 멤버로 접근 가능해져요 (기반의 private 멤버는 friend가 아닌 한 절대 접근할 수 없어요).
public 상속은 객체지향 프로그래밍의 하위 타입(subtyping) 관계를 모델링해요. 파생 클래스 객체는 기반 클래스 객체다(IS-A)라는 관계예요. 파생 객체에 대한 참조·포인터는 그 public 기반들 중 하나에 대한 참조·포인터를 기대하는 어떤 코드에서든 쓸 수 있을 것으로 기대돼요(리스코프 치환 원칙 LSP 참고). 또는 design-by-contract로 말하면, 파생 클래스는 public 기반의 클래스 불변식(class invariant)을 유지해야 하고, 재정의하는 멤버 함수의 전제 조건을 강화하거나 후제 조건을 약화해서는 안 돼요.
#include <iostream>
#include <string>
#include <vector>
struct MenuOption { std::string title; };
// Menu는 MenuOption의 vector입니다: 옵션을 넣고 빼고 순서를 바꿀 수 있고,
// title을 가집니다.
class Menu : public std::vector<MenuOption>
{
public:
std::string title;
void print() const
{
std::cout << title << ":\n";
for (std::size_t i = 0, s = size(); i < s; ++i)
std::cout << " " << (i + 1) << ". " << at(i).title << '\n';
}
};
// 참고: Menu::title은 기반 클래스와 독립된 역할이므로 문제가 되지 않습니다.
enum class Color { WHITE, RED, BLUE, GREEN };
void apply_terminal_color(Color) { /* OS별 처리 */ }
// 이건 나쁜 예입니다!
// ColorMenu는 모든 옵션이 고유한 색을 가진 Menu입니다.
class ColorMenu : public Menu
{
public:
std::vector<Color> colors;
void print() const
{
std::cout << title << ":\n";
for (std::size_t i = 0, s = size(); i < s; ++i)
{
std::cout << " " << (i + 1) << ". ";
apply_terminal_color(colors[i]);
std::cout << at(i).title << '\n';
apply_terminal_color(Color::WHITE);
}
}
};
// ColorMenu는 Menu를 public으로 상속해서는 만족시킬 수 없는
// 불변식들이 필요합니다. 예를 들어:
// - ColorMenu::colors와 Menu는 같은 수의 원소를 가져야 합니다.
// - 의미가 성립하려면 erase()를 호출하면 colors에서도 원소를
// 제거해야 옵션이 색을 유지합니다.
// 기본적으로 std::vector 메서드에 대한 비const 호출은 ColorMenu의
// 불변식을 깨뜨리며, 사용자가 colors를 올바르게 관리해 고쳐야 합니다.
int main()
{
ColorMenu color_menu;
// 이 클래스의 큰 문제는 ColorMenu::Color와 Menu를
// 동기화해야 한다는 점입니다.
color_menu.push_back(MenuOption{"Some choice"});
// color_menu.print(); // 오류! print()의 colors[i]가 범위를 벗어남
color_menu.colors.push_back(Color::RED);
color_menu.print(); // OK: colors와 Menu가 같은 수의 원소
}
protected 상속 (Protected inheritance)
클래스가 protected 접근 지정자로 기반을 상속하면, 기반 클래스의 모든 public·protected 멤버는 파생 클래스의 protected 멤버로 접근 가능해져요 (기반의 private 멤버는 friend가 아닌 한 접근 불가).
protected 상속은 "제어된 다형성(controlled polymorphism)" 에 쓰일 수 있어요. Derived의 멤버 안, 그리고 모든 더 파생된 클래스의 멤버 안에서는 파생 클래스 객체가 기반 클래스다(IS-A)라고 볼 수 있어요. 즉 Base에 대한 참조·포인터가 기대되는 곳에 Derived에 대한 참조·포인터를 쓸 수 있어요.
private 상속 (Private inheritance)
클래스가 private 접근 지정자로 기반을 상속하면, 기반 클래스의 모든 public·protected 멤버는 파생 클래스의 private 멤버로 접근 가능해져요 (기반의 private 멤버는 friend가 아닌 한 접근 불가).
private 상속은 정책 기반 설계(policy-based design) 에 흔히 쓰여요. 정책(policy)이 보통 빈 클래스라서, 그것들을 기반으로 쓰면 정적 다형성(static polymorphism) 을 가능하게 하면서 빈 기반 최적화도 활용할 수 있기 때문이에요.
private 상속은 합성(composition) 관계를 구현하는 데도 쓸 수 있어요 (기반 클래스 부분 객체가 파생 클래스 객체의 구현 세부사항이 되는 방식). 멤버로 두는 편이 더 나은 캡슐화를 제공해서 보통 권장돼요. 다만 파생 클래스가 기반의 protected 멤버(생성자 포함)에 접근해야 하거나, 기반의 가상 멤버를 재정의해야 하거나, 기반이 다른 기반 부분 객체보다 먼저 생성되고 나중에 파괴되어야 하거나, 가상 기반을 공유해야 하거나, 가상 기반의 생성을 제어해야 하는 경우에는 private 상속이 필요해요. 멤버로 합성을 구현하는 방식은 매개변수 팩에서의 다중 상속이나, 템플릿 메타프로그래밍으로 기반 클래스의 정체를 컴파일 타임에 정할 때는 적용되지 않아요.
protected 상속과 비슷하게, private 상속도 제어된 다형성에 쓸 수 있어요. 다만 파생 클래스의 멤버 안에서만(더 파생된 클래스 안에서는 아님) 파생 엔티티가 기반 클래스다(IS-A)로 취급돼요.
template<typename Transport>
class service : private Transport // Transport 정책에서 private 상속
{
public:
void transmit()
{
this->send(...); // 어떤 transport가 주어졌든 그것으로 send
}
};
// TCP 전송 정책
class tcp
{
public:
void send(...);
};
// UDP 전송 정책
class udp
{
public:
void send(...);
};
service<tcp> service(host, port);
service.transmit(...); // TCP로 send
멤버 이름 검색 (Member name lookup)
클래스 멤버에 대한 미한정·한정 이름 검색 규칙은 이름 검색(name lookup) 문서에 자세히 나와 있어요.
키워드 (Keywords)
관련 키워드는 virtual이에요.
결함 보고 (Defect reports)
다음의 동작을 바꾸는 결함 보고가 이전에 공표된 C++ 표준에 소급 적용됐어요.
| DR | 적용 대상 | 공표된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1710 | C++98 | class-or-decltype 문법 때문에 템플릿 구분자(template disambiguator)가 필요한 종속 클래스에서 파생할 수 없었음 | template 허용 |
더 알아보기 (Learn more)
- 가상 함수 (Virtual functions) — public 상속에서 동적 다형성을 만드는 메커니즘. (cppreference)
- 추상 클래스 (Abstract classes) — 순수 가상 함수로 인스턴스화할 수 없는 클래스. (cppreference)
- 이름 검색 (Name lookup) — 클래스 멤버 이름이 어떻게 찾아지는지의 세부 규칙. (cppreference)