[Effective C++] 항목 31 ~ 32
항목 31 : 파일 사이의 컴파일 의존성을 최대로 줄이자
- 구현부의 외부로 노출되지 않은 코드를 수정했음에도 건들지 않은 다른 곳까지 몽땅 컴파일 / 링크되는 상황이 발생할 수 있다.
- #include문은 헤더 파일들 사이의 컴파일 의존성을 발생시킨다.
- 클래스 정의 시 구현 세부사항을 떼고 전방선언을 한다?
- ex)
namespace std{ class string; } .... class Person { .... std::string name() const; .... }
- 문제가 되는 부분
- string은 클래스가 아니라 typedef로 정의한 타입 동의어이다.(basic_string
를 typedef) - 표준 라이브러리 구성요소 중 원치 않는 #include가 생기게 하는 것들을 사용하지 않게끔 직접 손을 봐야 한다.
- 컴파일러가 컴파일 중 객체들의 크기를 전부 알아야 한다.
- 객체 하나의 크기가 얼마인지 정의된 정보를 보는 수밖에 없다.
- 스몰토크 / 자바는 그 객체의 포인터를 담을 공간만 할당한다.(포인터 뒤에 실제 객체 구현부를 숨긴다.)
- string은 클래스가 아니라 typedef로 정의한 타입 동의어이다.(basic_string
- pImpl 관용구 : 주어진 클래스를 두개로 쪼개어 한쪽은 인터페이스만, 한쪽은 그 인터페이스의 구현만 맡게 한다.
- 구현 클래스 포인터의 이름은 대개 pImpl이라 불린다.
- 구현 클래스를 마음대로 고칠 수 있지만 인터페이스로 쓰는 사용자 쪽은 컴파일을 다시 할 필요가 없다.
- 컴파일 의존성을 최소화하는 핵심 원리 : 정의부에 대한 의존성을 선언부에 대한 의존성으로 바꾼다.
- 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다.
- 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하게 만든다.
- 어떤 클래스를 사용하는 함수 선언 / 어떤 클래스 객체를 값으로 전달하거나 반환 -> 클래스 정의가 필요 없다.
- 모두가 그 함수를 호출하진 않으므로 부담을 실제 함수 호출이 일어나는 사용자의 소스파일 쪽으로 전가한다. 실제 쓰지 않을 타입 정의에 대해 사용자가 의존성을 끌어오는 것을 예방한다.
- 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다.
- 클래스 전방 선언 대신 헤더 파일(선언만 있는)을 include한다.
-
pImpl 관용구를 사용하는 클래스는 핸들 클래스라고 부른다.
- 컴파일 의존성을 최소화하는 방법
- pImpl 관용구의 핸들 클래스 사용
- ex)
Person::Person(const std::string& name) : pImpl(new PersonImpl(name) {} std::string Person::name() const { return pImpl->name(); }
- ex)
- 인터페이스 클래스로 만든다.
- ex)
class Person { public: virtual ~Person(); virtual std::string name() const = 0; .... }
- 인스턴스화하지 못하기 때문에 포인터 / 참조자로 프로그래밍 할 수밖에 없다.
- 인터페이스 클래스의 인터페이스가 수정되지 않는 한 다시 컴파일할 필요가 없다.
- 객체 생성 수단이 최소 하나 있어야 한다.
- 가상 생성자(팩토리 함수) : 주어진 인터페이스 클래스의 인터페이스 지원 객체를 동적 할당한 후 포인터를 반환한다.(물론 스마트포인터면 좋다.) -> 보통 인터페이스 클래스 내부 정적 멤버로 존재한다.
- ex)
- pImpl 관용구의 핸들 클래스 사용
-
실행 시간 비용과 객체 한 개당 필요한 저장 공간이 추가로 증가한다.
- 핸들 클래스의 약점
- 멤버 함수 호출 시 구현부 객체 데이터까지 포인터를 타야 한다. -> 간접화 연산이 하나 증가한다.
- 저장하는 데 필요한 메모리 크기에 구현부 포인터의 크기가 더해진다.
- 구현부 포인터가 동적 할당된 구현부 객체를 가리키게 어디선가 그 구현부 포인터의 초기화를 해야한다.
- 동적 메모리 할당에 따르는 연산 오버헤드 존재
- bad_alloc(메모리 고갈) 예외 발생 가능성 존재
- 인터페이스 클래스의 약점
- 호출되는 함수가 전부 가상함수 -> 함수 호출 시 가상 테이블 점프 비용을 소모한다.
- 파생 객체 모두 가상 테이블 포인터를 지녀야 한다.
- 만약 가상함수를 공급하는게 인터페이스 클래스 뿐일 때는 이 가상 테이블 포인터도 객체 하나를 저장하는 데 필요한 메모리 크기를 늘리는 요인이 된다.
- 공통적인 약점 : 인라인 함수의 도움을 제대로 이끌기 힘들다.
항목 32 : public 상속 모형은 반드시 “is-a(~는 ~의 일종이다)”를 따르도록 만들자
- 자식 클래스의 모든 객체는 부모 클래스 타입 객체이지만 부모 클래스의 객체는 자식 클래스의 타입의 객체가 아니다.
- 최고의 설계는 제작하려는 소프트웨어 시스템이 기대하는 바에 따라 달라진다.
- 유효하지 않은 코드를 컴파일 단계에서 막아주는 인터페이스가 좋은 인터페이스이다.
- ex)
class Penguin : public Bird { public: virtual void fly() { error("can't fly!"); } }
- 프로그램이 실행될 때만 발견할 수 있다.
-
public 상속은 기본 클래스 객체가 가진 ‘모든 것들이’ 파생 클래스 객체에도 그대로 적용된다고 단정하는 상속
- is-a 외에도 두 가지 관계가 더 존재한다. 1) has-a(~는 ~를 가진다) 2) is-implemented-in-terms-of(~는 ~를 써서 구현한다)