일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- operator new
- lua
- 암시적 변환
- 상속
- UE4
- Smart Pointer
- Effective c++
- 메타테이블
- Vector
- 예외
- resource management class
- exception
- c++
- more effective c++
- 반복자
- 티스토리챌린지
- 영화
- 참조자
- 게임
- 루아
- 언리얼
- virtual function
- implicit conversion
- 다형성
- effective stl
- reference
- 비교 함수 객체
- 영화 리뷰
- 스마트 포인터
- 오블완
Archives
- Today
- Total
스토리텔링 개발자
[Effective C++] 31. 컴파일 의존성 줄이기 본문
728x90
항목 31 : 파일 사이의 컴파일 의존성을 최대로 줄이자
C++ 컴파일 의존성 문제
- C++은 인터페이스와 구현을 깔끔하게 분리하지 못한다.
- 구현 세부사항이 헤더에 포함되기 때문이다.
- 구현 세부사항이란, 멤버 변수, 암시적 인라인 함수 등.
- #include 문을 통한 헤더 파일들 사이의 컴파일 의존성(compilation dependency)이 발생할 수 있다.
- 구현부의 외부로 노출되지 않은 코드를 수정했음에도 건들지 않은 다른 곳까지 몽땅 컴파일 / 링크되는 상황이 발생할 수 있다.
#include <string>
#include "date.h"
#include "address.h"
// #include 문을 통한 컴파일 의존성 발생
class Person
{
public:
Person(const string& name, const Date& birthday, const Address& addr);
string name() const;
string birthDate() const;
string address() const;
...
private:
string theName; // 구현 세부사항
Date theBirthDate; // 구현 세부사항
Address theAddress; // 구현 세부사항
};
컴파일 의존성을 해결해보자
- 전방선언을 통해서 해결이 가능하지 않을까?
namespace std
{
class string; // 전방 선언(틀렸지만)
}
class Date; // 전방 선언
class Address; // 전방 선언
class Person
{
...
}
- 두 가지의 문제점
- string 은 클래스가 아니라 typedef로 정의한 타입 동의어이다.
- basic_string<char>를 typedef 한 것일 뿐이다.
- string 자체는 클래스가 아니므로 당연히 전방 선언이 동작하지 않는다.
- 보통은 표준 라이브러리 일부가 변경될 일은 없으니 그저 #include로 사용하면 될 것이다.
- 만약의 만약, string을 커스텀 했다면?
- 표준 라이브러리 구성요소 중 원치 않는 #include가 생기게 하는 것들을 사용하지 않게끔 직접 손을 봐야 할 것이다.
- 컴파일러가 컴파일 중 객체들의 크기를 전부 알아야 하므로 전방 선언으로는 불충분할 수 있다.
-
class Date; class Address; // 전방 선언을 했지만.. class Person { public: ... private: // 전방 선언으로는 각 객체의 크기를 컴파일러가 알 수 없다. // 그렇기에 해당 방법으로는 컴파일 에러가 발생한다. Date theBirthDate; Address theAddress; };
- 컴파일러가 객체 하나의 크기가 얼마인지 알려면 정의된 정보를 보는 수밖에 없다.(#include)
- 스몰토크 / 자바 등의 언어에서는 포인터 뒤에 실제 객체 구현부를 숨기는 식의 구현이므로 이런 문제가 발생하지 않는다.
- 이는 pImpl 관용구를 통해서 해결할 수 있을 것이다.
-
class PersonImpl; // pImpl을 위한 전방 선언 class Date; class Address; // 클래스 인터페이스들이 해당 클래스들을 사용하므로 전방 선언. class Person { public: Person(const string& name, const Date& birthday, const Address& addr); string name() const; string birthDate() const; string address() const; ... private: shared_ptr<PersonImpl> pImpl; // pImpl 관용구 };
-
- string 은 클래스가 아니라 typedef로 정의한 타입 동의어이다.
pImpl 관용구
- 주어진 클래스를 두개로 쪼개어 한쪽은 인터페이스만, 한쪽은 그 인터페이스의 구현만 맡게 한다.
- 구현 클래스를 마음대로 고쳐도 인터페이스 변경이 없으면, 사용자 쪽에서 컴파일을 다시 할 필요가 없다.
컴파일 의존성을 최소화기 위한 핵심 원리
정의부에 대한 의존성을 선언부에 대한 의존성으로 바꾼다.
- 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다.
- 할 수 있으면 클래스 정의 말고 클래스 선언에 최대한 의존하게 만든다.
- 다음 경우 클래스 정의가 필요 없다.
- 객체를 사용해 함수를 선언할 때
- 객체를 값으로 전달하거나 반환할 때
-
class Date; // 전방 선언 void clearAppointments(Date d); // 클래스를 사용한 함수 선언 Date today(); // 클래스 객체 반환
- 이것이 가능한 이유는, 모두가 해당 함수를 호출하진 않기 때문이다.
- 부담을 실제 함수 호출이 일어나는 사용자의 소스파일 쪽으로 전가한다.
- 즉, 이를 통해 실제 쓰지 않을 타입 정의에 대해 사용자가 의존성을 끌어오는 것이 예방된다.
- 다음 경우 클래스 정의가 필요 없다.
- 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다.
- 선언부를 위한 헤더 파일과 정의부를 위한 헤더 파일을 쪼개서 관리한다.
- 클래스 전방 선언 대신 선언부를 위한 헤더 파일을 include 한다.
핸들 클래스
- pImpl 관용구를 사용하는 클래스는 핸들 클래스라고 부른다.
컴파일 의존성을 최소화하는 방법
- pImpl 관용구의 핸들 클래스를 사용한다.
#include "Person.h" #include "PersonImpl.h" // PersonImpl 함수를 사용하므로 include 해줘야 한다. Person::Person(const string& name) : pImpl(new PersonImpl(name)) // pImpl 초기화 { } string Person::name() const { return pImpl->name(); }
- 인터페이스 클래스(interace class)로 만든다. (항목 34 참조)
class Person { public: virtual ~Person(); virtual string name() const = 0; ... }
- 인스턴스화하지 못하기 때문에 포인터 / 참조자로 프로그래밍 할 수밖에 없다.
- 인터페이스 클래스의 인터페이스가 수정되지 않는 한 다시 컴파일할 필요가 없다.
- 이를 사용하기 위해서는 객체 생성 수단이 최소 하나 있어야 한다.
- 팩트리 함수(가상 생성자)
- 인터페이스 클래스를 구현하는 방법
- 인터페이스 클래스를 만들고, 그를 상속받아 가상함수를 구현하는 방법.(대중적)
- 다중 상속을 사용하는 방법.(항목 40 참조)
컴파일 의존성을 챙길 때의 단점
- 실행 시간 비용이 들어간다.
- 객체 한 개당 필요한 저장 공간이 추가로 늘어난다.
- 핸들 클래스의 약점
- 멤버 함수 호출 시 구현부 객체 데이터까지 포인터를 타고 들어가야 한다.
- 간접화 연산이 하나 증가한다.
- 객체 하나를 저장하는데 필요한 메모리 크기에 더해 구현부 포인터의 크기가 추가로 필요하다.
- 구현부 포인터의 초기화를 해야한다.
- 동적 메모리 할당과 해제에 따르는 연산 오버헤드가 동반된다.
- bad_alloc(메모리 고갈) 예외 발생의 가능성까지 대비해야 한다.
- 멤버 함수 호출 시 구현부 객체 데이터까지 포인터를 타고 들어가야 한다.
- 인터페이스 클래스의 약점
- 핸들 클래스와 인터페이스 클래스의 공통적인 약점
- 인라인 함수를 활용하기 힘들어진다.
728x90
'개발 > Effective C++' 카테고리의 다른 글
[Effective C++] 33. 상속된 이름 가리기 문제 (0) | 2024.07.02 |
---|---|
[Effective C++] 32. public 상속은 "is-a" (0) | 2024.06.28 |
[Effective C++] 30. 인라인 함수 (0) | 2024.06.26 |
[Effective C++] 29. 예외 안전성 (0) | 2024.06.25 |
[Effective C++] 28. 클래스 내부 private 객체에 대한 핸들 리턴 문제 (0) | 2024.06.24 |
Comments