[ Effective C++ ] 정리 모음집
" C++ 프로그래머의 필독서, 스콧 마이어스의 Effective C++ 를 읽고 내용 요약 / 정리 "
" 개발 도중에는 사용자에게 미칠 파급 효과를 최소로 만드는 것이 좋다! "
- 컴파일 의존성을 최소화하는 작업의 배경이 되는 가장 기본적인 아이디어는 '정의' 대신에 '선언'에 의존하게 만들자는 것이다. 이 아이디어에 기반한 두 가지 접근 방법은 핸들 클래스와 인터페이스 클래스이다.
- 라이브러리 헤더는 그 자체로 모든 것을 갖추어야 하며 선언부만 갖고 있는 형태여야 한다, 이 규칙은 템플릿이 쓰이거나 쓰이지 않거나 동일하게 적용하자!
[ 예시 코드 ]
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;
};
위 코드는 컴파일이 불가능 하다
- string, Date, Address와 같은 Person의 세부사항들이 어떻게 정의 됐는지 모르기 때문
세부사항들의 정보를 가져오기 위해선 #include
지시자를 사용해야 한다
#include <string>
, #include "Date.h"
, #include "Address.h"
- 하지만 #include는 Person 정의 파일과 헤더 파일들 사이의 컴파일 의존성을 증가시켜, 헤더 파일 중 하나라도 바뀌거나 헤더 파일들과 엮여있는 파일들이 바뀌기만 해도 컴파일을 다시 해야한다.
namespace std
{
class string;
}
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;
...
};
클래스 이름이 아닌 다른 것을 전방선언 할 경우
- string은 클래스가 아닌 typedef로 정의한 타입동의어 이다
basic_string <char>
- string을 전방선언 하기 위해선 템플릿을 추가로 끌고 들어와야해서 더 복잡해진다.
컴파일러는 컴파일 도중 객체들의 크기를 전부 알아야 한다
Person p(params);
- 컴파일러는 위 정의문을 만나면 객체 p에 대한 공간을 할당해야 하는데 Person의 크기를 알기 위해선 Person이 어떻게 정의되어 있는지를 알아야 한다
#include <string>
#include <memory>
class PersonImpl;
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 관용구(pointer to implementation)라는 이름으로 불린다.
- 포인터 변수의 이름은 pImpl이라고 붙이는 것이 일반적이다.
'정의부에 대한 의존성'을 '선언부에 대한 의존성'으로 바꾸는 것
- 컴파일 의존성을 최소화 하는 핵심 원리
객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다
- 참조자, 포인터를 정의할 때는 선언부만 필요하지만 객체를 정의할 때는 해당 타입의 정의가 필요하다
할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만들자
class Date;
Date today();
void clearAppointments(Date d);
- 어떤 클래스를 사용하는 함수를 선언할 때는 해당 클래스의 정의가 필요하지 않다, 심지어 해당 클래스를 값으로 전달 하거나 반환해도 필요 없다
#include "datefwd.h"
Date today();
void clearAppointments(Date d);
- 사용자 입장에서는 선언부의 헤더를 #include 해야 한다.
- 두 헤더 파일은 짝으로 관리 되어야 한다.
C++ 에서 지원하는 iostream 관련 함수 및 클래스들의 선언부 만으로 구성된 헤더
- sstream, streambuf, fstream, iostream 등
선언부 전용 헤더를 제공하는 것은 템플릿에도 적용된다는 대표적인 예시
📢 C++에서는 템플릿 선언과 정의를 분리할 수 있도록 하는 export 키워드도 제공되지만 현장에서 잘 쓰이지 않는다...
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birtday, addr))
{}
string Person::name() const
{
return pImpl->name();
}
pimpl 관용구를 사용하는 클래스를 핸들 클래스 라고 한다
핸들 클래스에서 어떤 함수를 호출하게 되어 있다면, 구현 클래스 쪽으로 함수 호출을 전달해 구현 클래스가 작업을 수행하게 만든다
class Person
{
public:
virtual ~Person();
virtual string name() const = 0;
virtual string birthDate() const = 0;
virtual string address() const = 0;
...
static shared_ptr<Person> create(const string& name, Date& birthday, Address& addr);
...
};
[사용자 쪽 코드]
string name;
Date dateOfBirth;
Address address;
...
shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
cout << pp->name() << " was born on" << pp->birthDate() << " and now lives at " << pp->address();
...
어떤 기능을 나타내는 인터페이스를 추상 기본 클래스로 마련, 이 클래스로부터 파생 클래스를 만들 수 있게 함
- 파생이 목적이기에 데이터 멤버, 생성자가 없으며, 하나의 가상 소멸자와 인터페이스를 구성하는 순수 가상 함수만 들어있다
이 클래스를 쓰기 위해선 포인터, 참조자로 프로그래밍 해야 함
- 추상 클래스는 인스턴스로 만들 수 없으니
인터페이스 클래스를 사용하기 위해서는 객체 생성수단이 최소한 하나는 있어야 한다
shared_ptr<Person> Person::create(const string& name, const Date& birthday, const Address& addr)
{
return shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
- 파생 클래스의 생성자 역할을 대신하는 팩토리 함수(혹은 가상 생성자)를 호출
- 실행 시간 비용 발생
- 객체 한 개당 필요한 저장 공간이 추가로 늘음
- 인라인 함수의 도움을 받기 힘듬
- 인라인이 되게 만드려면 함수 본문을 대개 헤더 파일에 두어야 하지만 핸들 클래스와 인터페이스 클래스는 구현부를 사용자의 눈으로부터 차단하는 데 중점을 둔 설계
한번 접근할 때마다 요구되는 간접화 연산이 한 단계 더 증가
- 핸들 클래스의 함수 호출 시 구현부의 데이터에 접근하기 위해 포인터를 거쳐야 한다
객체 하나씩을 저장하는데 필요한 메모리 크기에 구현부 포인터의 크기가 더해짐
구현부 포인터가 동적 할당된 구현부 객체를 가리키도록 그 구현부 포인터의 초기화가 발생해야 한다
- 동적 메모리 할당에 따르는 연산 오버헤드, bad-alloc 예외와도 마찰 가능성 생김