일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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
- reference
- virtual function
- 언리얼
- 예외
- effective modern c++
- resource management class
- std::async
- 반복자
- 보편 참조
- 스마트 포인터
- Smart Pointer
- operator new
- lua
- 게임
- 암시적 변환
- universal reference
- exception
- 상속
- 영화 리뷰
- 오블완
- effective stl
- implicit conversion
- 티스토리챌린지
- Effective c++
- c++
- 참조자
- iterator
- UE4
- more effective c++
- 영화
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 22. unique_ptr를 사용한 pImpl 관용구의 특수 멤버 함수 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 22. unique_ptr를 사용한 pImpl 관용구의 특수 멤버 함수
김디트 2025. 3. 7. 11:19728x90
항목 22. pImpl 관용구를 사용할 때에는 특수 멤버 함수들을 cpp 파일에서 정의하라
pImpl 관용구
- 구현부를 클래스 포인터로 대체하고, 그 포인터를 통해 자료 멤버들에 간접적으로 접근하는 기법
- 각종 타입들을 헤더에서 포함시켜야 하므로 컴파일이 느려진다.
- 또한 타입이 추가될 때마다 재컴파일이 필요하다.
- 이를 해결하기 위해서 pImpl 관용구를 사용한다.
pImpl 구현
// 기존 구현
class Widget
{
public:
Widget();
...
private:
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
- C++98에서는 아래와 같이 할 수 있었다.
// 헤더 파일
class Widget
{
public:
Widget();
~Widget();
...
private:
strcut Impl; // 선언만 하고 정의는 하지 않는다.(불완전 타입)
Impl *pImpl; // raw 포인터 사용
};
// cpp 파일
struct Widget::Impl // 구현부 구조체
{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() : pImpl(new Impl) {}
Widget::~Widget() { delete pImpl; } // 해제를 위해 반드시 소멸자가 필요하다.
- 모던 C++에서는 스마트 포인터를 사용할 수 있다.
// 헤더 파일
class Widget
{
public:
Widget();
...
private:
strcut Impl; // 선언만 하고 정의는 하지 않는다.(불완전 타입)
std::unique_ptr<Impl> pImpl; // unique_ptr 사용
};
// cpp 파일
struct Widget::Impl // 구현부 구조체
{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
// 소멸자는 필요 없다.
- 헌데 위 코드는 막상 Widget을 사용하려 하면 문제가 발생한다.
-
#include "widget.h" Widget w; // 컴파일 에러!!!
- 컴파일러는 불완전 형식에 sizeof나 delete를 사용한다는 불평을 내뱉는다.
- 이 문제는, w가 파괴될 때 컴파일러가 작성하는 코드에서 기인한다.
-
pImpl 파괴 시 코드 문제 상황
- w의 소멸자를 호출한다.
- std::unique_ptr을 이용하는 Widget 클래스에는 소멸자가 따로 선언되어 있지 않다.
- 소멸자에서 할 일이 없으므로.
- 컴파일러가 생성하는 특수 멤버 함수 규칙으로 인해 컴파일러가 대신 소멸자를 생성한다. (항목 17 참조)
- 컴파일러는 그 소멸자 안에 Widget의 자료 멤버 pImpl의 소멸자를 호출하는 코드를 삽입한다.
- pImpl은 std::unique_ptr<Widget::Impl> 이므로, unique_ptr의 기본 삭제자를 사용한다.
- 그 기본 삭제자는 std::unique_ptr 안의 raw 포인터에 delete를 적용한다.
- 근데 대부분의 표준 라이브러리 구현들에서 그 삭제자 함수는 delete를 적용하기 전에 적용할 대상이 불완전 타입인지를 static_assert를 이용해서 점검한다.
- 헌데 불완전 타입이므로 컴파일 에러가 발생한다.
- w가 파괴되는 지점 쪽을 지정하며 에러가 발생하는데, 특수 멤버 함수는 암묵적으로 inline이기 때문이다.
- 즉, 파괴 시점에 Widget::Impl이 완전한 타입이 되게 하면 문제는 해결된다.
해결 방법
// 헤더 파일
class Widget
{
public:
Widget();
~Widget(); // 선언만 한다.
...
private:
strcut Impl; // 선언만 하고 정의는 하지 않는다.(불완전 타입)
std::unique_ptr<Impl> pImpl; // unique_ptr 사용
};
// cpp 파일
struct Widget::Impl // 구현부 구조체
{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() {} // ~Widget의 정의
- 이렇게 Widget의 소멸자를 헤더에 선언, cpp에 정의하면 된다.
- 물론 컴파일러가 자동으로 만든 소멸자가 문제 없다는 걸 명시하고 싶다면 아래처럼 해도 된다.
Widget::~Widget() = default;
pImpl과 이동 연산
- 이동 연산자들은 소멸자가 선언되어 있으면 자동으로 작성되지 않는다.(항목 17 참조)
- 컴파일러가 작성하는 이동 연산들이 적합하다고 가정하면, 기본 구현을 지정하면 되지 않을까?
// 헤더 파일
class Widget
{
public:
Widget();
~Widget();
// 이 역시 이동 연산을 처리할 때 컴파일 에러가 발생하는 코드!
Widget(Widget&& rhs) = default;
Widget& operator=(Widget&& rhs) = default;
...
private:
strcut Impl;
std::unique_ptr<Impl> pImpl;
};
- 이동 할당 연산 시 pImpl을 재배정하기 전에 pImpl이 가리키는 객체를 파괴해야 하는데 그 시점에 불완전 타입이다!
- 이동 생성자는 예외 발생 시 pImpl을 파괴하기 위한 코드를 작성하는데 파괴하려면 완전 타입이어야 한다.
- 해결책은 동일하다.
// 헤더 파일
class Widget
{
public:
Widget();
~Widget();
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
...
private:
strcut Impl;
std::unique_ptr<Impl> pImpl;
};
// cpp 파일
struct Widget::Impl // 구현부 구조체
{
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
// 구현부에 정의
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
- 만약 커스텀 깊은 복사를 만들고 싶다면, 역시 같은 룰을 적용하여 구현부를 cpp에 구현해주면 된다.
shared_ptr을 사용한다면?
- 이 항목의 조언이 더 이상 적용되지 않는다.
- 소멸자를 선언할 필요가 없다.
- 그러므로 이동 연산들은 컴파일러가 알아서 만들어준다.
// 헤더 파일
class Widget
{
public:
Widget();
...
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};
// cpp 파일
// 아래는 모두 성공한다.
Widget w1;
auto w2(std::move(w1));
w1 = std::move(w2);
- unique_ptr
- 커스텀 삭제자가 타입에 반영된다.
- 컴파일러는 더 작은 런타임 자료구조와 더 빠른 런타임 코드를 만들어낼 수 있다.
- 하지만 특수 멤버 함수가 쓰이는 시점에서 대상 타입들이 완전한 타입이어야 한다는 제약이 있다.
- shared_ptr
- 커스텀 삭제자가 타입에 반영되지 않는다.
- unique_ptr보다 다소 느리고 무겁다.
- 하지만 대상 타입들이 완전한 타입이어야 한다는 제약이 사라진다.
- pImpl의 경우 독점적 관계이므로 unique_ptr을 쓰는 게 가장 적합한 것은 확실하지만
- 위 차이를 알아두는 건 의미가 있을 것이다.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 24. 보편 참조와 rValue (0) | 2025.03.11 |
---|---|
[Effective Modern C++] 23. std::move, std::forward (0) | 2025.03.10 |
[Effective Modern C++] 21. std::make_unique, std::make_shared (0) | 2025.03.06 |
[Effective Modern C++] 20. std::weak_ptr (0) | 2025.03.05 |
[Effective Modern C++] 19. std::shared_ptr (0) | 2025.03.04 |
Comments