std::multiset<std::string> names;

void logAndAdd(const std::string& name) {
    ...
    names.emplace(name);
}

std::string petName("Darla");
logAndAdd(petName); // 왼값
logAndAdd(std::string("Persephone")); // 오른값
logAndAdd("Patty Dog"); // 문자열 리터럴

인수가 왼값인 경우 emplace의 복사는 피할 수 없다.

 

인수가 오른값인 경우 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어진다. 하지만 인수가 오른값이기 때문에 원칙적으로는 복사 대신 이동을 적용할 여지가 있다.

 

인수가 리터럴 문자열인 경우 두번째 경우와 마찬가지로 매개변수 name은 오른값에 묶이지만 name 자체는 왼값이기 때문에 복사가 이루어지며 이동을 적용할 여지가 있다. 약간의 차이가 있다면 두번째 경우는 명시적인 임시 string 객체를 생성하고 매개변수가 거기에 묶였지만, 리터럴 문자열은 암묵적으로 생성된 임시 string 객체에 name이 묶인다.

만약 문자열 리터럴을 직접 emplace에 전달했다면 multiset 안에서 직접 string 객체를 생성했을 것이다. 애초에 복사나 이동 비용을 치를 필요가 없던 것이다.

 

std::multiset<std::string> names;

template<typename T>
void logAndAdd(T&& name) {
    ...
    names.emplace(std::forward<T>(name));
}

std::string petName("Darla");
logAndAdd(petName); // 왼값 복사
logAndAdd(std::string("Persephone")); // 오른값 이동
logAndAdd("Patty Dog"); // 임시객체 복사 대신 multiset 안에 string 생성

보편 전달을 적용함으로써 최적화를 이뤄낼 수 있다.

 

이번에는 인수가 문자열이 아니라 정수타입 변수를 통해 인덱스를 전달하고 함수 내에서 해당 인덱스를 통해 이름을 가져오는 함수를 오버로딩 한다고 해보자.

 

std::string nameFromIdx(int idx); // idx에 해당하는 이름 반환

void logAndAdd(int idx) {
    ...
    names.emplace(nameFromIdx(idx));
}

logAndAdd(22);

잘 해소된것처럼 보이지만 short 타입을 전달하면 에러가 발생한다.

short타입을 캐스팅 등을 통해 int타입으로 변환하지 않는 이상 보편 참조를 받는 버전의 함수가 호출된다.

이 때, T&&는 short&로 추론되며 name에 묶이고 emplace 함수를 호출할 때, name을 string 생성자에 전달하지만 short타입을 받는 생성자는 존재하지 않기 때문에 에러가 발생하는 것이다.

 

보편 참조를 받는 함수 템플릿은 생각보다 많은 타입들을 빨아들이기 때문에 오버로딩과 결합하는 것은 웬만해서는 나쁜 선택이 된다.

 

이런 문제를 쉽게 피하는 방법 중 하나는 완벽 전달(Perfect Forwarding) 생성자를 작성하는 것이지만 더 심각한 문제를 야기할수도 있다.

 

class Person {
public:
    template<typename T>
    explicit Person(T&& n) : name(std::forward<T>(n)) {} // 완벽 전달 생성자
    explicit Person(int idx) : name(nameFromIdx(idx)) {}
    
    Person(const Person& rhs); // 복사 생성자
    Person(Person&& rhs); // 이동 생성자
private:
    std::string name;
};

Person p("Nancy");
auto cloneOfP(p); // 복사 생성자 호출?

완벽 전달 생성자, 복사 생성자, 이동 생성자 모두 정의되어있는 클래스이다.

cloneOfP는 복사 생성자를 호출함으로써 객체를 생성하려고 했지만 실제로 호출되는 것은 완벽 전달 생성자이다.

 

class Person {
public:
    explicit Person(Person& n) : name(std::forward<Person&>(n)) {}
    ...
};

cloneOfP의 생성자는 Person& 타입의 매개변수를 받는 형태로 인스턴스화 된다. p가 비const 객체이기 때문에 복사 생성자보다는 완벽 전달 생성자의 매개변수 타입에 더 부합하기 때문에 완벽 전달 생성자가 호출된다.

만약 p가 const 객체였다면 복사 생성자의 매개변수 타입에 부합하므로 복사 생성자가 호출될 것이다.

 

class Person {
public:
    explicit Person(const Person& n);
    Person(const Person& rhs);
    ...
};

그런데 p가 const객체로 전달되는 경우 생성자 템플릿의 매개변수가 복사 생성자와 똑같아진다. 이런 경우 템플릿 함수보다 비템플릿 함수(일반적인 함수)의 호출을 우선시한다는 규칙에 의해 자동으로 작성된 복사 생성자가 호출된다.

 

class SpecialPerson : public Person {
public:
    SpecialPerson(const SpecialPerson& rhs) : Person(rhs) {}
    SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) {}
    ...
};

상속이 관여하는 클래스에서는 완벽 전달 생성자와 컴파일러가 작성한 복사 및 이동 연산들 사이의 상호작용이 미치는 여파가 더욱 크다.

파생 클래스의 복사, 이동 생성자가 기반 클래스의 복사, 이동 생성자를 호출하는 것 처럼 보이지만 실제로는 둘 다 완벽 전달 생성자를 호출하고 있다.

 

보편 참조에 대한 함수 오버로딩에 대한 해결책은 다음 항목에서 다루도록 한다.

 

◾ 보편 참조에 대한 오버로딩은 거의 항상 보편 참조 오버로딩 버전이 예상보다 자주 호출되는 상황으로 이어진다.

◾ 완벽 전달 생성자들은 특히나 문제가 많다. 그런 생성자는 대체로 비const 왼값에 대한 복사 생성자보다 더 나은 부합이며, 기반 클래스 복사 및 이동 생성자들에 대한 파생 클래스의 호출들을 가로챌 수 있기 때문이다.

오른값 참조를 다른 함수에 넘겨줄때는 std::move를 사용하고 보편 참조를 다른 함수에 넘겨줄때는 std::forward를 사용해야 한다.

 

오른값 참조에 std::forward를 사용하는 것은 동작 자체는 문제가 없지만 실수의 여지가 있다. 보편 참조에 std::move를 사용하는것은 아예 좋지 않은 결과가 초래될 수 있다.

 

class Widget {
public:
    template<typename T>
    void setName(T&& newName) { name = std::move(newName); } // 보편 참조를 move로 이동
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

std::string getWidgetName(); // 팩토리 함수
Widget w;
auto n = getWidgetName();
w.setName(n); // 호출이 끝나면 n의 값은 알 수 없어진다

const string&을 받는 함수를 오버로딩 하면 되는거 아닌가? 싶겠지만 리터럴 문자열을 인수로 넘겨줬을때 기존에는 발생하지 않았던 비용이 발생하는 것을 생각하면 그다지 올바르지 않다.

그보다는 코드의 확장이 나쁘다는것이 더 큰 문제이다. 지금은 매개변수가 고작 하나라서 함수 오버로딩이 두개면 됐지만 여러개이고 심지어 왼값, 오른값 모두 존재한다면 오버로딩의 수가 기하급수적으로 늘어난다.

이런 문제를 피하기 위해서 보편 참조에 대해서는 대체로 std::forward를 사용해야 한다.

 

'항상'이 아니라 '대체로' 라고 했다. 그 이유는 오른값 참조나 보편 참조에 묶인 객체는 함수 내에서 여러번 사용될수도 있기 때문에 전부 다 사용하기 전에 이동되는 일은 피해야 하기 때문이다. 그래서 std::move나 std::forward를 적용하는 것은 해당 참조의 마지막 사용이어야 한다.

 

또한 std::move의 경우 경우에 따라 std::move_if_noexcept를 사용하는 게 바람직한 경우도 존재한다. (항목 14 참조)

만약 함수가 결과를 값으로 돌려주고 해당 값이 오른값 참조나 보편 참조에 묶인 객체라면 std::move 또는 std::forward를 적용시켜서 반환하는 것이 바람직하다.

 

그런데 함수 내에서 선언된 지역 변수를 반환하는 경우에도 std::move를 적용하면 복사 연산 대신 이동 연산이 이루어 질거라는 확대 해석을 해서는 안된다.

 

Widget makeWidget() {
    Widget w;
    ...
    return std::move(w);
}

이런 경우는 굳이 std::move를 사용하지 않고 return w만 하더라도 반환값 최적화(RVO)에 의해 복사가 제거되어 복사가 이뤄지지 않는다.

다만 RVO가 적용되려면 지역 객체의 타입이 함수의 반환 타입과 같아야 하고 그 지역 객체가 함수의 반환값이어 한다.

위의 경우는 둘 다 해당되기 때문에 RVO가 적용된다.

 

굳이 std::move를 적용해서 반환하면 타입이 달라져서 RVO의 조건이 충족되지 않기 때문에 오히려 최적화 여지를 제한하게 되는 결과를 낳게된다.

 

RVO가 적용되더라도 컴파일러가 복사 연산을 반드시 제거하지는 않는다. 하지만 이런 경우라면 반환되는 객체는 반드시 오른값으로 취급되어야 한다고 정해져 있으므로 굳이 우리가 명시적으로 std::move를 사용할 필요는 없다.

어차피 RVO가 적용되는경우 복사 연산이 제거되거나 그렇지 않더라도 암묵적으로 반환값에 std::move 연산을 수행해주기 때문이다.

 

◾ 오른값 참조나 보편 참조가 마지막으로 쓰이는 지점에서, 오른값 참조에는 std::move를, 보편 참조에는 std::forward를 적용하라.

◾ 결과를 값 전달 방식으로 돌려주는 함수가 오른값 참조나 보편 참조를 돌려줄때에도 각각 std::move나 std::forward를 이용하라.

◾ 반환값 최적화(RVO)의 대상이 될 수 있는 지역 객체에는 절대로 std::move나 std::forward를 적용하지 말아야 한다.

T&&가 오른값 참조라고 가정하는것은 당연한 것 같지만 안타깝게도 그렇지는 않다.

 

void f(Widget&& param); // 오른값 참조
Widget&& var1 = Widget(); // 오른값 참조

auto&& var2 = var1; // 오른값 참조 아님

template<typename T>
void f(std::vector<T>& param); // 오른값 참조

template<typename T>
void f(T&& param); // 오른값 참조 아님

T&&에는 두 가지 의미가 있다. 하나는 오른값 참조이고 다른 하나는 오른값 참조 또는 왼값 참조 중 하나라는 것이다.

오른값 참조인 것처럼 보여도 상황에 따라서는 왼값 참조로 동작한다.

이런 이중적인 참조는 const, volatile 객체 또는 둘 다 해당되는 객체에 묶일수도 있다. 거의 모든것에 묶을 수 있는 유연한 참조이고 이를 보편 참조(universal reference)라고 부른다. 거의 항상 std::forward를 사용하기 때문에 전달 참조(forwarding reference)라고 부르기도 한다.

 

template<typename T>
void f(T&& param);

auto&& var2 = var1;

보편 참조는 보통 타입 추론이 일어나는 템플릿 매개변수나 auto 선언에서 나타난다.

 

template<typename T>
void f(T&& param);

Widget w;
f(w); // 왼값 전달. param의 타입은 Widget&
f(std::move(w)); // 오른값 전달. param의 타입은 Widget&&

하나의 참조가 보편 참조이려면 반드시 타입 추론이 관여해야 한다(필요조건). 그렇지만 타입 추론이 관여했다고 해서 반드시 보편 참조가 되는것은 아니고(충분조건X) 참조 선언의 형태도 정확해야 한다.

 

template<typename T>
void f(std::vector<T>&& param); // 오른값 참조. 보편 참조가 아님

template<typename T>
void f(const T&& param); // 오른값 참조. 보편 참조가 아님

param의 타입이 단순히 T&&가 아니라 std::vector<T>&&라면 보편 참조가 될 수 없고 오른값 참조로만 동작한다.

또는 const 한정사 하나만 붙여도 보편 참조가 되지 못한다.

 

 

그런데 또 T&&라고 다 보편참조는 아니다.

 

template<class T, class Allocator = allocator<T>>
class vector { // C++ 표준 벡터
public:
    void push_back(T&& x);
    ...
};

push_back의 매개변수는 보편 참조로 보이지만 push_back은 반드시 구체적으로 인스턴스화된 vector의 일부여야 하기 때문에 타입 추론이 전혀 일어나지 않는다.

 

std::vector<Widget> v; // Widget 타입으로 인스턴스화

class vector<Widget, allocator<Widget>> {
public:
    void push_back(Widget&& x); // 오른값 참조
    ...
};

Widget 타입으로 인스턴스화 되면서 push_back의 매개변수 T&&가 Widget&& 타입으로 결정되고 여기서 타입 추론은 전혀 관여하지 않는다.

 

template<class T, class Allocator = allocator<T>>
class vector {
public:
    template<class... Args>
    void emplace_back(Args&&... args);
    ...
};

이 경우는 T와 독립적이기 때문에 타입 추론이 적용되어서 Args는 보편 참조가 된다.

T가 Args로 바뀌었을 뿐 형태는 똑같다. 그래서 보편 참조의 형태가 T&&라고 할 뿐이다. 정확히는 타입&&이 보편 참조의 형태이다.

 

auto역시 보편 참조가 될 수 있다고 했는데 흔히 사용되지는 않고 C++14 이후의 람다식에서는 자주 사용되는 편이다.

 

auto timeFuncInvocation = [](auto&& func, auto&&... params) { // C++14
    std::forward<decltype(func)>(func)(
        std::forward<decltype(params)>(params)...
    );
};

C++14부터 람다식의 매개변수 타입에 auto를 사용할 수 있게 되면서 보편 참조의 사용이 가능해졌다.

 

◾ 함수 템플릿 매개변수의 타입의 형태가 T&&이고 T가 추론된다면, 또는 객체를 auto&&로 선언한다면, 그 매개변수나 객체는 보편 참조이다.

◾ 선언된 타입의 형태가 정확히 타입&&가 아니거나 타입 추론이 일어나지 않으면 타입&&는 오른값 참조를 뜻한다.

◾ 오른값으로 초기화되는 보편 참조는 오른값 참조에 해당된다. 왼값으로 초기화되는 보편 참조는 왼값 참조에 해당한다.

std::move는 모든 것을 이동하지는 않고 std::forward는 모든 것을 전달하지는 않는다.

둘 다 그저 캐스팅을 수행하는 함수 템플릿일 뿐이다.

 

template<typename T>
typename remove_reference<T>::type&& move(T&& param) {
    using ReturnType = typename remove_reference<T>::type&&;
    
    return static_cast<ReturnType>(param);
}

std::move는 T&&가 왼값 참조인 경우를 고려해서 참조자를 제거한뒤 오른값 참조를 붙여서 항상 오른값이 반환되도록 구현되어있다.

 

전달받은 인수를 그저 오른값으로 캐스팅 하는것뿐이라 move보다 rvalue_cast같은 이름이 더 낫다는 제안도 있었으나 결국은 move라는 이름을 가지게 되었다.

물론 오른값은 이동의 후보가 맞지만 아닌 경우도 존재한다.

 

class Annotation {
public:
    explicit Annotation(const std::string text) : value(std::move(text)) {}
private:
    std::string value;
};

그저 text의 내용을 읽기만 할 것이기 때문에 가급적 const를 붙이면 좋다는 점에 따라 const를 붙이고 move를 통해 복사 대신 오른값을 얻으려고 한다.

컴파일도 잘되고 링크도 잘되는데 의도한 동작이 전혀 이뤄지지 않는다.

move때문에 오른값으로 캐스팅 되는것은 확실하지만 text는 const std::string으로 선언되어있기 때문에 캐스팅 이후에는 오른값 const std::string가 될 것이다.

 

class string {
public:
    string(const string& rhs); // 복사 생성자
    string(string&& rhs); // 이동 생성자
};

그런데 이동 생성자는 const가 아닌 타입에 대한 오른값 참조를 받고, const에 대한 왼값 참조를 const 오른값에 묶는 것이 허용되기 때문에 복사 생성자가 호출된다.

 

생각해보면 const 객체의 이동은 지원해선 안된다. 이동이 된다는것은 기존 객체의 내용을 수정하겠다는 것인데 const 객체의 수정이 이루어지면 const 정확성이 유지되지 않는다. 그래서 언어 차원에서 방지하는 것은 당연한 것이다.

 

이를 통해 알 수 있는것은 이동을 지원할 객체는 const로 선언되어서는 안되고 std::move는 실제로 아무것도 이동시키지 않을 뿐만 아니라 캐스팅된 객체가 이동 자격을 갖추게 된다는 보장도 제공하지 않는다.

그저 확실한 것은 오른값을 확실하게 돌려준다는 것 뿐이다.

 

 

std::forward역시 비슷한 개념이 적용된다. 다만 move는 무조건 오른값으로 캐스팅하지만 forward는 특정 조건이 만족될 때에만 캐스팅한다는 차이가 있다.

 

void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param) {
    auto now = std::chrono::system_clock::now();
    
    makeLogEntry("Calling 'process'", now);
    process(param); // 왼값? 오른값?
}

Widget w;
logAndProcess(w); // 왼값으로 호출
logAndProcess(std::move(w)); // 오른값으로 호출

어떤 함수로 전달된 매개변수의 타입이 오른값 참조라고 하더라도 그 매개변수 자체는 왼값이다.

그래서 위와 같은 경우 호출은 왼값과 오른값으로 구분되었지만 param 자체는 왼값이기 때문에 둘 다 왼값 참조 타입을 매개변수로 받는 process가 호출된다.

이럴 때 logAndProcess 함수에서 process 함수에 param을 인수로 넘겨줄 때 forward를 통해 조건부 캐스팅하여 넘겨주면 본래 가지고 있던 타입으로 전달된다.

 

    ...
    process(std::forward<T>(param));
    ...

 

그럼 forward는 어떻게 인수가 오른값인지를 알 수 있을까?

간단하게 설명하면 해당 정보가 logAndProcess의 템플릿 매개변수 T에 부호화 되어 저장되어 있다.

std::forward로 전달될 때 해당 정보를 복원하여 캐스팅을 수행한다.

 

 

결과적으로 move, forward 둘 다 캐스팅을 수행하는 함수라는 점이다.

다만 move는 항상 캐스팅하지만 forward는 조건부 캐스팅이라는 차이가 있다.

 

move대신 forward만 사용해도 문제가 없지만 여러가지 이유로 권장하지 않는다.

 

◾ std::move는 오른값으로의 무조건 캐스팅을 수행한다. std::move 자체는 아무것도 이동하지 않는다.

◾ std::forward는 주어진 인수가 오른값에 묶인 경우에만 그것을 오른값으로 캐스팅한다.

◾ std::move와 std::forward 둘 다, 실행시점에서는 아무 일도 하지 않는다.

빌드 시간을 줄이기 위해 클래스에 pimpl 관용구를 적용시키는 경우 전방 선언한 클래스나 구조체의 포인터를 사용하게 된다.

그리고 이를 동적할당하여 사용 후 소멸자에서 해제를 시키는 것이 보통일 것이다.

 

// Widget.h
class Widget {
public:
    Widget();
    ~Widget();
private:
    struct Impl; // 전방 선언
    Impl* pImpl;
};

// Widget.cpp
struct Widget::Impl {
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget() : pImpl(new Impl) {}
Widget::~Widget() { delete pImpl; }

하지만 이제는 원시 포인터와 new, delete를 직접 사용하는 대신 스마트 포인터를 사용하는 것이 좋다는 것을 안다.

 

// Widget.h
class Widget {
public:
    ... // 소멸자에서 delete를 해줄 필요가 없다
private:
    struct Impl; // 전방 선언
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
...
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}

스마트 포인터이기 때문에 소멸자에서 delete를 해줄 필요가 없다.

위의 코드들 자체는 컴파일이 잘 되지만 클라이언트에서 Widget 객체를 선언하여 사용하려고 할 때에는 컴파일 에러가 발생하게 된다.

 

#include "Widget.h"

Widget w;

w가 파괴되는 시점에 소멸자가 호출되는데, 스마트 포인터로 변경하면서 소멸자를 작성하지 않았기 때문에 컴파일러가 소멸자를 자동으로 작성하게 된다. 이 때 pImpl의 소멸자인 unique_ptr의 기본 삭제자를 호출한다.

unique_ptr의 기본 삭제자는 소유중인 원시 포인터에 delete를 적용하기 전에 혹시라도 원시 포인터가 불완전한 타입을 가리키고 있지는 않은지를 static_assert를 이용해서 점검하게 된다. 그런데 여기서 불완전한 타입을 가리키고 있다고 판단하게 되어 에러가 발생하는 것이다.

 

컴파일러가 자동으로 작성하는 특수 멤버 함수는 암묵적으로 inline이고 Widget.h 안에는 Impl에 대한 정의가 되어있지 않아서 자동으로 작성된 소멸자가 호출되는 순간에는 불완전한 타입이 되는 것이다.

 

이 문제는 unique_ptr<Widget::Impl>을 파괴하는 코드가 만들어지는 지점에서 완전한 타입이 되면 바로 해결된다.

Widget::Impl의 정의는 cpp에 되어있기 때문에 소멸자의 호출이 헤더가 아닌 cpp에서 이뤄지면 될 것이다.

 

그럼 이제 어떻게 하면 되는가.

소멸자의 선언과 정의를 분리시켜서 명시적으로 작성해주면 된다.

 

// Widget.h
class Widget {
public:
    Widget();
    ~Widget();
    ...
private:
    ...
};

// Widget.cpp
Widget::~Widget() {} // Widget::~Widget() = default;

불완전한 타입에 대한 문제는 해결되었다.

 

Pimpl 관용구를 사용하는 클래스는 이동 연산들을 지원하기에 아주 좋은 클래스이다.

심지어 별도로 작성하지 않아도 컴파일러가 자동으로 작성해주는 이동 연산들이 요구에 딱 맞는 이동을 수행한다.

하지만 불완전한 타입에 대한 문제를 해결하느라 소멸자를 명시적으로 만들어주는 바람에 이동 연산은 자동으로 생성되지 않는다. 하지만 이것 역시 명시적으로 작성해주면 된다.

 

// Widget.h
class Widget {
public:
    Widget(Widget&& rhs); // 여기에 default를 적으면 안됨
    Widget& operator=(Widget&& rhs);
    ...
};

// Widget.cpp
Widget::Widget(Widget&& rhs) = default;
Widget& operator=(Widget&& rhs) = default;

이동연산에 대한 문제도 해결하였다.

 

하지만 만약 Gadget이 string, vector와 같이 복사가 가능한 타입이라면 복사 연산을 지원해주는것이 합당하다.

그런데 복사 연산은 이동 연산과 달리 직접 구현해주어야 한다. 왜냐면 unique_ptr같은 이동 전용 타입이 있는 클래스에서는 컴파일러가 복사 연산들을 작성해주지 않을뿐더러 설령 작성된다 하더라도 얕은 복사를 수행하기 때문이다.

 

// Widget.h
class Widget {
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs);
    ...
};

// Widget.cpp
Widget::Widget(const Widget& rhs) : pImpl(nulltpr) {
    if (rhs.pImpl) pImpl = std::make_unqiue<Impl>(*rhs.pImpl);
}

Widget& Widget::operator=(const Widget& rhs) {
    if (!rhs.pImpl) pImpl.reset();
    else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl);
    else *pImpl = *rhs.pImpl;
    
    return *this;
}

복사 연산을 직접 구현했을뿐 기존처럼 선언과 정의를 분리시켰다.

 

 

재미있게도 unique_ptr와 shared_ptr 둘 중 어느것을 사용하느냐에 따라 여지껏 언급한 문제해결 방법이 유효할수도, 필요없을수도 있다는 것이다.

shared_ptr의 삭제자는 타입의 일부가 아니라 제어 블록에서 같은 shared_ptr과 공유되기 때문에 런타임에 크기가 더 커지고 속도도 약간 느려지지만 삭제자가 호출될 때 원시 포인터의 타입이 완전한 타입이어야 한다는 제약 조건이 사라진다.

 

Pimpl 관용구를 사용함에 있어서 unique_ptr이나 shared_ptr사이에서 어떤 절충 관계가 존재하는 것은 아니다.

Widget과 Widget::Impl 사이의 관계는 독점적 소유 관계이기 때문에 unique_ptr이 더 잘 맞아떨어질 뿐이다.

상황에 따라 shared_ptr를 선택하는 경우에는 불완전한 타입에 대한 문제를 신경쓰지 않아도 된다는 점만 알면 된다.

 

◾ Pimpl 관용구는 클래스 구현과 클래스 클라이언트 사이의 컴파일 의존성을 줄임으로써 빌드 시간을 감소한다.

◾ std::unique_ptr 타입의 pImpl 포인터를 사용할 때에는 특수 멤버 함수들은 클래스 헤더에 선언하고 구현 파일에서 구현해야 한다. 컴파일러가 기본으로 작성하는 함수 구현들이 사용하기에 적합한 경우에도 그렇게 해야 한다.

◾ 위의 조언은 std::unique_ptr에 적용될 뿐, std::shared_ptr에는 적용되지 않는다.

+ Recent posts