raw pointer의 단점
1. 포인터의 선언에서 하나의 객체를 가리키는지 배열을 가리키는지 나타내지 않는다.
2. 포인터가 가리키는 객체에 대한 사용이 끝났을 때, 포인터가 가리키는 것을 파괴해야 하는지 여부, 즉 포인터가 가리키는 것을 포인터가 소유하고 있는지 여부에 대해 아무것도 나타내지 않는다.
3. 포인터가 가리키는 것을 파괴해야 한다고 결정했다면, 파괴할 방법을 알 방법이 없다.
4. delete가 객체를 파괴할 올바른 방법이라는 것을 알 때, delete를 사용해야 할지 delete[]를 사용해야 할지 알 수 없다. 잘못된 방법을 사용하면 정의되지 않은 결과가 발생한다.
5. 코드의 모든 경로를 따라 정확히 한 번만 파괴를 수행하도록 보장하기 어렵다. 경로를 잃으면 메모리 누수가 발생하며, 두 번 이상의 파괴가 수행되면 정의하지 않은 결과가 발생한다.
6. Dangling pointer(허상 포인터): 이미 메모리에서 해제된 객체를 가리키고 있는 포인터.
Smart pointer는 위의 문제점을 해결하는 한 가지 방법이다.
스마트 포인터는 raw pointer를 감싸며, 많은 함정들을 피한다.
C++11에는 4가지 스마트 포인터가 있다: std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr.
동적으로 할당된 객체의 life time의 관리를 돕기위해 설계되었으며, 따라서 객체가 적절한 시간에 적절한 방법으로 파괴되도록 함으로써 메모리 누수문제를 방지한다.
std::auto_ptr은 C++98에서 더 이상 사용되지 않는다.
나중에 C++11의 std::unique_ptr이 된 것을 표준화하려는 시도였다.
작업을 올바르게 수행하기 위해서 move semantics이 필요했지만, C++98에는 이러한 개념이 없었다.
해결 방법으로 std::auto_ptr은 이동을 위해 복사 작업을 선택했다.
이것은 std::auto_ptr을 복사하면 null로 설정되는 놀라운 코드와, std::auto_ptr을 컨테이너에 저장할 수 없는 사용제한을 발생시켰다.
std::unique_ptr은 std::auto_ptr이 하는 모든 일과 그 이상을 수행한다.
Item 18: Use std::unique_ptr for exclusive-ownership resource management
std::unique_ptr
std::unique_ptr은 exclusive-ownership(독점적 소유권)의미를 구현한다.
null이 아닌 std::unique_ptr은 그것이 아리키는 것을 소유한다.
std::unique_ptr을 이동하는 것은 source pointer에서 destination pointer로 소유권을 이전하는 것이다.
std::unique_ptr을 복사하는 것은 허용되지 않는다. 복사하게 되면 같은 자원을 가리키는 두개의 std::unique_ptr이 생기며, 각 std::unique_ptr은 자신이 자원을 소유한다고 생각한다.
따라서 std::unique_ptr은 move-only type이다.
기본으로, 리소스 해제는 std::unique_ptr안의 raw pointer를 delete함으로써 달성된다.
std::unique_ptr의 사용: factory function return type for objects in a hierarchy
class Investment{ ... };
class Stock: public Investment{ ... };
class Bond: public Investment{ ... };
class RealEstate: public Investment{ ... };
다음과 같은 계층에 대한 factory function은 객체를 heap에 할당하고 객체에 대한 포인터를 리턴한다.
caller는 객체가 더 이상 필요하지 않을 때, 객체를 파괴할 책임이 부여된다.
caller가 factory에 의해 반환받은 자원에 대한 책임을 얻기 때문에 std::unique_ptr과 완벽하게 대응된다.
std::unique_ptr은 포인터가 파괴될 때, 자동으로 그것이 가리키는 객체도 삭제한다.
Destruction: default delete, custom deleters
기본으로 delete를 통해 파괴되지만, 객체를 생성할 때 std::unique_ptr이 custom deleters를 사용하도록 할 수 있다.
임의의 함수가 객체가 소멸될 때 호출된다.
// custom deleter(a lambda expression)
auto delInvmt = [](Investment* pInvestment){makeLogEngtry(pInvestment); delete pInvestment; };
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)>pInv(nullptr, delInvmt);
if( /* a Stock object should be created */ )
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if( /* a Bond object should be created */ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if( /* a RealEstate object should be created */ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
delInvmt는 makeInvestment로부터 반환되는 custom deleter 객체다.
모든 사용자 정의 삭제 함수는 파괴할 객체에 대한 raw pointer를 수학한 다음 해당 개체를 파괴하는데 필요한 작업을 수행한다.
lambda expression을 사용하는 것이 편리하고 효율적이다.
Custom deleter를 사용하기 위해, std::unique_ptr의 두 번째 argument로 custom deleter를 전달해야 한다.
Raw pointer에서 smart pointer로의 암시적 형변환은 문제가 될 수 있기 때문에, C++11은 이를 금지한다.
따라서 reset을 사용해 new를 통해 생성된 객체의 소유권을 가지도록 한다.
C++14: using function return type deduction
function return type deduction을 사용하면 makeInvestment를 더 간단하고 캡슐화된 방법으로 구현할 수 있다.
그러나 함수 포인터를 사용하면 std::unique_ptr 객체의 사이즈가 커지기 때문에 lambda expression을 사용하는 것이 좋다.
std::unique_ptr<T>, std::unique_ptr<T[]>
std::unique_ptr의 형태는 위와 같이 두가지로 나뉜다.
따라서 하나의 객체를 가리키는지, 배열인지 모호함이 없다.
single-object 형태에 대해서는 indexing operator가 없으며, array 형태에 대해서는 dereferencing operator가 없다.
std::unique_ptr to std::shared_ptr
std::unique_ptr에서 std::shared_ptr로의 변환은 쉽다.
std::shared_ptr<Investment> sp = makeInvestment(arguments);
Things to Remember
std::unique_ptr은 작고, 빠르고, 이동하는 유일한 스마트 포인터이며, 독점적 소유권을 가지며 자원을 관리한다.
기본적으로 resource destruction은 delete를 통해 수행되며, custom deleter를 지정되어야 한다.
Stateful delters와 function pointers는 std::unique_ptr 객체의 사이즈를 증가시킨다.
std::unique_ptr에서 std::shared_ptr로의 변환은 쉽다.
Item 19: Use std::shared_ptr for shared_ownership resource management
std::shared_ptr
std::shared_ptr을 통해 접근되는 객체는 shared ownership(공유 소유권)을 통해 해당 포인터에 의해 수명이 관리된다.
특정 std::shared_ptr이 객체를 소유하지 않는다.
특정한 객체를 가리키는 마지막 std::shared_ptr이 객체를 더이상 가리키지 않으면, std::shared_ptr은 그것이 가리키던 객체를 파괴한다.
std::shared_ptr은 resoure's reference count를 통해 그것을 가리키는 마지막 포인터인지 알 수 있다.
reference count 값은 얼마나 많은 std::shared_ptr이 해당 객체를 가리키는지 추적한다.
reference count의 존재는 다음과 같은 시사점을 가진다.
1. std::shared_ptr의 크기는 raw pointer 사이즈의 두배다.
내부적으로 자원에 대한 raw pointer뿐만 아니라 자원의 reference ount에 대한 raw pointer도 포함하기 때문이다.
2. reference count에 대한 메모리는 동적으로 할당된다.
개념적으로, reference count는 가리키는 객체와 연관되지만, 가리키는 객체는 이것에 대해 아무것도 모른다.
따라서, 객체에는 reference count를 저장할 공간이 없다.
std::shared_ptr을 std::make_shared를 통해 만들면 동적 할당을 피할 수 있지만, std::make_shared를 사용할 수 없는 경우 reference count는 동적 할당된 데이터로 저장된다.
3. reference count의 증감은 atomic이어야 한다.
다른 스레드에 동시 판독기와 작성기가 있을 수 있다.
Moving std::shared_ptr is faster than copying them
move-constructing은 reference count 조작이 필요하지 않기 때문에 복사하는 것보다 빠르다.
Custom deleters
std::shared_ptr도 delete를 기본 자원 해제 매커니즘으로 사용하지만, custom deleter도 지원한다.
std::unique_ptr은 deleter의 타입이 스마트 포인터 타입의 일부분이지만, std::shared_ptr은 그렇지 않다.
// custom deleter
auto loggingDel = [](Widget* pw){ makeLogEntry(pw); delete pw; };
// deleter type is part of ptr type
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// deleter type is not part of ptr type
std::shared_ptr<Widget> spw(new Widget, loggingDel);
shared_ptr은 더 flexible하다.
서로 다른 custom deleter를 가진 std::shared_ptr끼리 할당될 수 있으며, 같은 container에 위치할 수 있다.
그러나 std::unique_ptr에서는 custom deleter의 타입이 스마트 포인터의 타입에 영향을 주기 때문에 이것이 불가능하다.
std::shared_ptr의 custom deleter는 std::shared_ptr 객체의 사이즈를 변화시키지 않는다.
deleter와 상관없이 std::shared_ptr 객체는 두 포인터의 크기와 같다.
Control block
std::shared_ptr은 reference count, custom deleter는 control block이라고 불리는 자료 구조의 일부분이다.
control block은 std::shared_ptr 객체가 처음 만들어질 때 설정된다.
control block 생성에 대한 규칙은 다음과 같다.
1. std::make_shared은 항상 control block을 생성한다.
2. unique-ownership pointer로부터 std::shared_ptr이 생성될 때 control block이 생성된다.
3. std::shared_ptr 생성자가 raw pointer와 호출될 때, control block이 생성된다.
이러한 규칙을 따르면 여러개의 control block이 생성될 수 있다.
따라서 std::shared_ptr을 사용하기 위한 두가지 lesson이 있다.
1. raw pointer를 std::shared_ptr의 생성자에 전달하는 것을 피해라. 대안으로 std::make_shared를 사용한다.
2. raw pointer를 std::shared_ptr의 생성자에 전달해야 한다면, new의 결과를 직접 전달해라.
std::enable_shared_from_this
이것은 std::shared_ptr에 의해 관리되는 클래스가 this 포인터에서 std::shared_ptr을 안전하게 생성할 수 있도록 하려는 경우 사용하는 상속받은 기본 클래스에 대한 템플릿이다.
std::enable_shared_from_this는 기반 클래스 템플릿이며, type parameter는 항상 derived된 클래스의 이름이어야 한다.
이러한 설계 패턴을 Curiously Recurring Template Pattern, CRTP(묘하게 되풀이되는 템플릿 패턴)이라고 한다.
class Widget: public std::enable_shared_from_this<Widget>{
public:
...
void process();
...
};
std::enable_shared_from_this는 현재 객체를 가리키는 std::shared_ptr를 생성하지만 control block을 복제하지 않는 멤버 함수 하나를 정의한다.
그 멤버 함수는 shared_from_this이다.
this포인터와 같은 객체를 가리키는 std::shared_ptr이 필요할 때 이 멤버 함수를 사용한다.
void Widget::process()
{
//as before, process the Widget
...
//add std::shared_ptr to current object to precessedWidgets
processWidgets.emplace_back(shared_from_this());
}
Things to Remember
std::shared_ptr은 임의 자원의 공유 수명 관리를 위해 garbage collection에 가까운 편의성을 제공한다.
std::unique_ptr과 비교했을 때, std::shared_ptr 객체는 전형적으로 두배 더 크고, 제어 블록에 대한 오버헤드가 발생하며, atomic reference count 조작이 필요하다.
기본적인 자원 해제는 delete를 통해 수행되며, custom deleters도 지원된다.
deleter의 타입은 std::shared_ptr의 타입에 영향을 주지 않는다.
raw pointer 타입의 변수에서 std::shared_ptr을 생성하지 않아야 한다.
Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle
std::weak_ptrs은 전형적으로 std::shared_ptr로부터 생성된다.
std::shared_ptr이 가리키는 것과 동일한 위치를 가리키지만, 가리키는 객체의 reference count에 영향을 주지 않는다.
// after spw is constructed the pointed-to widget's ref count is 1
auto spw = std::make_shared<Widget>();
// wpw points to same Widget as spw. RC remains 1
std::weak_ptr<Widget> wpw(spw);
// RC goes to 0, and the Widget is destroyed
// wpw now dangles
spw = nullptr;
대상을 잃은(dangle) std::weak_ptr을 만료되었다(expired)고 한다.
// if wpw doesn't point to an object
if(wpw.expired())...
std::weak_ptr은 dereferencing operations이 없기 때문에, 가리키는 객체에 접근하는 것은 불가능하다.
역참조가 가능하더라도, expired 호출과 역참조를 분리하면 race condition(경쟁 조건)이 발생할 수 있다.
또 다른 스레드가 std::shared_ptr이 마지막으로 가리켰던 객체를 재할당하거나 파괴하면, 역참조는 정의되지 않은 행동을 야기할 수 있다.
필요한 것은 std::weak_ptr이 만료되었는지 atomic operation을 통해 확인하는 것이다.
이것은 std::weak_ptr을 std::shared_ptr로부터 생성함으로써 달성되며 두가지 형태로 나타난다.
1. lock member function을 사용한다.
std::weak_ptr::lock이 std::shared_ptr을 리턴, std::weak_ptr이 만료되었다면 std::shared_ptr은 null이 된다.
// if wpw's expired, spw1 is null
std::shared_ptr<Widget> spw1 = wpw.lock();
// same as above, but uses auto
auto spw2 = wpw.lock();
2. std::shared_ptr 생성자가 std::weak_ptr을 argument로 가진다.
이 경우에 std::weak_ptr이 만료되면, 예외가 발생한다.
// if wpw's expired, throw std::bad_weak_ptr
std::shared_ptr<Widget> spw3(wpw);
std::weak_ptr을 사용할 수 있는 용도는 다음과 같다.
1. Caching
고유한 id를 기반으로하는 읽기 전용의 객체에 대한 스마트 포인터를 생성하는 factory function을 고려하자.
factory function의 리턴 타입은 std::unique_ptr이다.
최적화를 위해 결과를 캐시에 저장한다고 하자.
모든 결과를 캐시에 저장하면 성능 문제가 발생하기 때문에, 더 이상 쓰지 않는 객체는 삭제하는 것이 타당하다.
이 경우 std::unique_ptr로 반환하는 것은 좋지 않다.
캐시의 포인터들은 자신이 객체를 잃었음을 감지할 수 있어야 한다.
따라서 캐시의 포인터들은 std::weak_ptr이어야 하며, 이는 factory function의 리턴 타입이 std::shared_ptr이어야 함을 의미한다.
2. Observer design pattern
이 패턴의 주요 컴포넌트는 subjects(상태가 변할 수 있는 객체, 관찰 대상)과 observer(상태 변화를 통지받는 객체, 관찰자)이다.
각 subject는 그것의 observers에 대한 포인터를 가지는 data member를 포함한다.
만약에 observer가 파괴되면, subjects는 접근을 시도하지 않아야 한다.
따라서 각각의 subject가 그것의 observer에 대한 std::weak_ptr의 container를 가지는 것이 타당하며, subject는 포인터가 가리키는 객체가 없어졌는지 확인할 수 있다.
아래는 std::weak_ptr의 활용 예이다.
A와 C는 B의 소유권을 공유하며, B는 A를 가리키는 포인터를 가진다.
이 경우 어떤 포인터를 사용해야 하는가?
1) A raw pointer
이 경우 A가 파괴되어도 B는 이를 감지하지 못한다.
따라서 B는 dangling pointer를 역참조할 것이며 이는 정의되지 않은 행동을 야기한다.
2) A std::shared_ptr
std::shared_ptr cycle이 발생하며 이는 A와 B둘다 파괴되는 것을 막는다.
프로그램의 다른 자료 구조에서 A와 B에 접근할 수 없어도 각각의 reference count(참조 카운트)는 1이다.
이 경우 사실상 A와 B는 누수가 일어났다고 볼 수 있으며, 프로그램이 이에 접근할 수 없으므로 자원을 재확보할 수도 없다.
3) A std::weak_ptr
이것은 위에서 언급한 문제들을 피한다.
A가 파괴되면 B는 dangling여부를 파악할 수 있으며, A와 B가 서로를 가리키더라도 순환 문제가 발생하지 않는다.
Things to Remember
std::shared_ptr처럼 작동하며 가리키는 객체를 잃을 수 있는 포인터가 필요한 경우 std::weak_ptr를 사용한다.
std::weak_ptr의 잠재적인 사용 예로는 caching, observer list, prevention of std::shared_ptr cycles이 있다.
Item 21: Prefer std::make_unique and std::make_shared to direct use of new
Make functions: std::make_unique, std::make_shared, std::allocate_shared
std::make_unique와 std::make_shared는 임의의 argument 집합을 사용하여 객체를 동적으로 할당하고 해당 객체에 대한 스마트 포인터를 반환하는 함수다.
std::allocate_shared는 std::make_shared처럼 동작하지만, argument의 첫번째가 동적 메모리 할당을 위해 사용되는 allocator object라는 점이 다르다.
make function을 사용할 때의 장점은 다음과 같다.
1) 소스 코드 중복 제거
// with make func
auto spw1(std::make_unique<Widget>());
// without make func
std::unique_ptr<Widget> upw2(new widget);
new를 사용한 버전은 생성할 타입을 반복하는데 이는 코드 중복을 피해야 한다는 소프트웨어 공학의 핵심 원칙에 위배된다.
소스 코드 중복은 컴파일 시간을 증가시키며, object code가 부풀려지고, 일반적으로 코드 기반을 작업하기 더 어렵게 만들고, 종종 일관적이지 않은 코드로 진화하며 이것은 오류를 야기한다.
2) 예외 안전성
void processWidget(std::shared_ptr<Widget> spw, int priority);
함수 processWidget이 호출되기 전에, argument에 대한 함수가 먼저 실행되어야 하며, 따라서 processWidget 실행 전에 다음과 같은 것들이 발생한다.
new Widget 수행 → std::shared_ptr 생성자 실행 → computePriority 실행
그러나 만약 computePriority 실행이 std::shared_ptr 생성자 실행 전에 수행된다면, 그리고 computePriority에서 예외가 발생하는 경우 문제가 발생한다.
동적으로 할당된 Widget이 누수되는 문제가 발생한다.
std::make_shared를 사용하면 이 문제를 피할 수 있다.
processWidget(std::make_shared<Widget>(), computePriority());
std::make_shared를 사용하면 동적으로 할당된 Widget에 대한 raw pointer는 std::shared_ptr에 안전하게 저장된다.
만약 computePriority가 예외를 야기하더라도, std::shared_ptr 소멸자는 자신이 소유한 Widget이 소멸되었음을 확인한다.
std::make_shared는 컴파일러가 작고 빠른 코드를 생성하도록 한다.
new를 사용하면, Widget에 대한 메모리 할당과 control block에 대한 메모리 할당이 필요하다.
그러나 std::make_shared를 사용하면 한 개의 할당만으로도 충분하다.
std::make_shared가 Widget객체와 control block을 모두 보유하기 위해 단일 메모리 청크를 할당하기 때문이다.
코드가 오직 한개의 메모리 할당 호출만 포함하기 때문에, 이 최적화는 프로그램의 static size를 줄인다.
또한 메모리가 한번만 할당되기 대문에 실행 코드의 속도가 증가한다.
게다가 std::make_shared는 control block의 일부 부기 정보의 필요성을 방지하며, 잠재적으로 프로그램의 전체 메모리 발자국을 감소시킨다.
이 장점은 std::allocated_shared에도 적용된다.
Make function이 부적절한 상황
1) custom deleters를 지정해야 하는 경우
어떠한 make function도 custom deleter의 지정을 허용하지 않는다.
그러나 std::unique_ptr과 std::shared_ptr은 custom deleter의 지정을 수행하는 생성자를 가진다.
2) Braced initializer를 사용해야 하는 경우
braced initializer를 사용해서 객체를 생성해야 하는 경우, new를 직접 사용해야 한다.
Std::shared_ptr에 대해 make function이 부적절한 상황
1) 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우
몇몇 클래스들은 자신의 operator new와 operator delete를 정의한다.
이 함수들의 존재는 global allocation과 deallocation이 부적절함을 시사한다.
이런 클래스 고유 메모리 관리 루틴은 단지 클래스의 객체와 정확히 같은 크기의 메모리만 할당, 해제하는 경우가 많다.
이 루틴들은 std::shared_ptr의 custom allocation과 deallocation과는 잘 맞지 않는다.
std::allocate_shared가 요구하는 메모리 조각의 크기는 동적으로 할당되는 객체의 크기에 제어 블록의 크기를 더한 것이기 때문이다.
2) 객체의 타입이 괘 크고, std::weak_ptr들이 std::shared_ptr보다 더 오래 살아남는 경우
std::shared_ptr make function을 통해 할당된 메모리는 마지막 std::shared_ptr과 std::weak_ptr이 파괴되기 전까지 할당해제할 수 없다.
만약 객체의 타입이 꽤 크고, 마지막 std::shared_ptr과 마지막 std::weak_ptr 소멸 사이의 시간 차이가 상당한 경우, 객체가 파괴되는 시점과 점유한 메모리가 해제되는 시점 사이에 지연이 발생할 수 있다.
new를 직접 사용하면, std::shared_ptr이 파괴될 때 바로 객체의 메모리를 해제할 수 있다.
Things to Remember
new의 직접적인 사용과 비교하여, make functions은 소스 코드 중복을 제거하고, 예외 안전성을 향상시키며, std::make_shared와 std::allocated_shared의 경우 더 작고 빠른 코드가 생성된다.
make function의 사용이 부적절한 상황은 custom deleters를 지정할 필요가 있는 경우, braced initializers를 사용해야 하는 경우가 있다.
std::shared_ptr에 대해 make function이 부적절한 경우는 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우, 큰 객체를 다룰 때 std::weak_ptr들이 std::shared_ptr보다 오래 살아남는 경우이다.
Item 22: When using the Pimpl Idiom, define special member functions in the implementation file
Pimpl Idiom
Pimpl Idiom은 class clients와 class implementation사이의 컴파일 의존성을 줄임으로써 빌드 시간을 줄인다.
Pimpl Idiom in C++98
Part 1) data member를 incomplete type을 가리키는 포인터로 선언
C++98의 Pimpl Idiom은 data members를 선언만 하고 정의하지 않은 struct에 대한raw pointer로 대체한다.
이러면 다른 타입을 클래스 내에서 언급하지 않기 때문에, 더 이상 #include할 필요가 없고 컴파일 속도를 높일 수 있다.
또한 다른 헤더파일이 변경되더라도 클래스에 영향을 주지 않는다.
이러한 선언만 하고 정의하지 않는 타입을 incomplete type이라고 한다.
class Widget{
public:
Widget();
~Widget();
private:
struct Impl; // declare implementation struct
Impl *pImpl; // and pointer to it
};
part 2) 기존의 클래스에서 사용하던 데이터 멤버들을 가지는 객체를 동적으로 할당, 해제
할당, 해제 코드는 구현 파일에 적는다(Widget의 경우, widget.cpp에).
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
};
Widget::Widget() // allocate data members for this Widget object
:pImpl(new Impl)
{}
Widget::~Widget() // destroy data members for this object
{ delete pImpl; }
하지만 이것은 C++98의 코드다.
raw pointer와 raw new와 delete를 사용한다.
Pimpl Idiom: using std::unique_ptr
std::unique_ptr을 사용하면 소멸자를 작성할 필요가 없다.
pImpl의 파괴를 std::unique_ptr의 소멸자가 수행한다.
class Widget{
public:
Widget();
private:
struct Impl;
std::unique_ptr<Impl> pImpl; // use smart pointer
};
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
};
Widget::Widget()
:pImpl(std::make_unique<Impl>())
{}
이 코드는 잘 컴파일 되지만, 클라리언트 쪽에서 컴파일되지 않는다.
#include "widget.h"
Widget w; // error!
이 코드의 문제는 w가 파괴되는 경우 발생한다.
w가 파괴되는 경우, destructor가 호출되며, 클래스 정의에서 std::unique_ptr을 사용하기 때문에 소멸자를 선언할 필요가 없으므로 어떠한 코드도 작성하지 않았으며, 따라서 컴파일러가 대신 소멸자를 작성한다.
컴파일러는 default deleter를 사용하여 std::unique_ptr을 삭제한다.
그러나 대부분의 컴파일러에서 delete를 적용하기 전에, raw pointer가 incomplete type을 가졌는지 C++11의 static_assert를 사용하여 점검한다.
위 코드의 Widget w의 소멸자 코드를 컴파일러가 생성할 때, static_assert가 실패한 것으로 판정되며, 에러 메시지가 발생한다.
이 문제를 해결하기 위해, std::unique_ptr<Widget::Impl>을 파괴하는 코드가 만들어지는 지점에서 Widget:Impl이 완전한 타입이 되도록 한다.
컴파일러는 Widget:Impl 타입의 정의를 봤을 때, 이 타입을 완전한 타입으로 간주한다.
그리고 Widget:Impl은 widget.cpp안에서 정의된다.
따라서, Widget:Impl의 정의 이후에 컴파일러가 그 소스 파일에 있는 widget의 소멸자의 본문을 보게 하면 클라이언트 코드가 성공적으로 컴파일된다.
class Widget{ // as before, in "widget.h"
public:
Widget();
~Widget(); // declaration only
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl{
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
};
Widget::Widget()
:pImpl(std::make_unique<Impl>())
{}
Widget::~Widget()
{}
Things to Remember
Pimpl Idiom은 class clients와 class implementation사이의 컴파일 의존성을 줄임으로써 빌드 시간을 줄일 수 있다.
std::unique_ptr타입의 pImpl 포인터를 사용할 때, 특수 멤버 함수를 클래스 헤더에 선언하고 implementation file에 구현해야 한다. 기본 함수 구현이 허용되는 경우에도 이 작업을 수행한다.
이는 std::unique_ptr에 적용되며, std::shared_ptr에는 적용되지 않는다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] Chapter 3: Moving to Modern C++(1) (0) | 2022.07.20 |
---|---|
[Effective Modern C++] Chapter 2: Auto (0) | 2022.07.19 |
[Effective Modern C++] Chapter 1: Deducing Types (1) | 2022.07.18 |