[Effective Modern C++] 22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라
Pimpl이란 pointer to implementation을 의미한다.
// 기존
class Widget
{
public:
Widget();
...
private:
std::string name;
std:;vector<double> data;
Gadget g1, g2, g3;
};
// Pimpl
class Widget
{
public:
Widget();
~Widget();
...
private:
struct Impl;
Impl *pImpl;
};
Pimpl 적용 코드를 보면 string, vector, Gadget을 선언하고 있지 않아 #include 할 필요가 없어져 컴파일 속도가 빨라지게 되었다. 이렇게 선언만 하고 정의를 하지 않는 형식을 불완전한 형식이라고 하며, 구현부에 작성하면 된다.
#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(new Impl)
{}
Widget::~Widget()
{ delete pImpl; }
여기서 pImpl을 보면 unique_ptr을 사용하기 적합한 상황임을 알 수 있지만, unique_ptr을 사용하게 되면 컴파일이 되지 않는다.
왜냐하면 컴파일러가 자동으로 pImpl의 소멸자를 호출하는데 Impl은 불완전한 형식이기 때문이다.
해결법은 Impl을 완전한 형식으로 만들어주면 되는데, 컴파일러는 타입의 정의를 보게 되면 그 타입을 완전한 타입으로 간주한다. 따라서 컴파일러가 소스파일의 Widget소멸자(unique_ptr 데이터 멤버를 파괴하는 코드를 작성하는 곳)를 보게 한다면 클라이언트 코드가 문제없이 컴파일된다.
struct Widget::Impl
{
std::string name;
std:;vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget()
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget() = default;
그리고 이 경우 소멸자를 선언하면 컴파일러는 이동 연산들을 작성하지 않아 이동 연산들을 지원하려면 직접 선언해야 하고, 소멸자와 마찬가지로 정의가 구현 파일에 있어야 한다.
복사 연산도 마찬가지이다. 왜냐하면 unique_ptr 같은 이동 전용 타입이 있는 클래스에 대해서는 컴파일러가 복사 연산들을 작성해주지 않으며, 작성한다고 해도 작성된 함수들은 unique_ptr 자체만 복사하는 얕은 복사를 수행하기 때문이다.
// "widget.h"
class Widget
{
public:
Widget(void);
~Widget(void);
// 선언만 하고 정의는 하지 않는다.
// 이동 연산
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
// 복사 연산
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs);
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// "widget.cpp"
#include <string>
#include "widget.h"
...
struct Widget::Impl { ... };
Widget::Widget(void)
: pImpl(std::make_unique<Impl>())
{}
// 소멸자
Widget::~Widget(void) = default;
// 이동 연산자
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
// 복사 생성자
Widget::Widget(const Widget& rhs)
: pImpl(nullptr)
{ if (rhs.pImpl) pImpl = std::make_unique<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은 삭제자의 타입이 스마트 포인터 타입의 일부가 아니라 실행 시점 자료구조가 더 커지고 실행 코드가 다소 더 느려지지만, 컴파일러가 작성한 특수 멤버 함수들이 쓰이는 시점에서 피지칭 타입들이 완전한 타입 이어야 한다는 요구조건이 사라지기 때문이다.
'Books > Effective Modern C++' 카테고리의 다른 글
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
[Effective Modern C++] 38. 스레드 핸들 소멸자들의 다양한 행동 방식을 주의하라
2022.10.23 -
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
[Effective Modern C++] 23. std::move와 std::forward를 숙지하라
2022.09.24 -
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
[Effective Modern C++] 21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라
2022.09.18 -
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
[Effective Modern C++] 20. std::shared_ptr처럼 작동하되 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr를 사용하라
2022.09.18