Home [C++] emc++ 03: Smart Pointer
Post
Cancel

[C++] emc++ 03: Smart Pointer

Smart Pointer

  • raw pointer의 단점

    • 객체와 배열 둘다 가리킬 수 있음
    • 사용자가 delete해야 할 책임이 있는지 확인 어려움
    • 어떻게 해제 해야하는지에 대한 정보를 얻기 어려움 (delete or 다른 매커니즘)
    • delete ? delete [] ?
    • 파괴가 정확히 한 번 일어남을 보장하기 어려움
      • 파괴가 일어나지 않으면 메모리 누수
      • 파괴를 여러번 수행 = 미정의 행동
    • 포인터가 객체를 가리키고 있는 상황에서 객체 파괴 시 포인터는 대상을 잃음
  • smart pointer

    • 동적으로 할당된 객체의 수명 관리에 도움이 되도록 보장, 자원 누수가 생기지 않도록 설계됨
  • unique_ptr

    • auto_ptr 보다 더 효율적
    • 객체 복사의 의미론을 왜곡 x
  • 4가지 스마트 포인터의 공통적인 기능은 기본 생성 뿐

항목18: 소유권 독접 자원의 관리에는 std::unique_ptr 을 사용하라

  • raw pointer와 같은 크기라고 가정할 수 있음

  • 독점적 소유권 의미론

    • nullptr 이 아닌 unique_ptr 은 항상 자신이 가리키는 객체를 소유함
    • std::unique_ptr을 이동하면, 소유권이 대상 포인터로 옮겨짐
    • 복사 금지
    • 이동 전용 타입
    • 소멸 시 자원 파괴
  • 흔한 용도

    • 상속 계층구조 안의 객체를 생성하는 팩터리 함수의 리턴 타입으로 쓰이는 것
      • 팩터리 함수: 흔히 힙에 객체 생성, 그 객체를 가리키는 포인터 리턴 (객체 삭제는 호출자 몫)
    • Pimpl 관용구의 구현 메커니즘 (항목22)
  • 커스텀 삭제자 (custom deleter)

    • 해당 삭제자는 해당 자원의 파괴 시점에서 호출되는 임의의 함수
    • 로그 기록 가능
    • 함수 포인터를 삭제자로 지정할 경우 unique_ptr의 크기가 1워드에서 2워드로 증가
    • 삭제자가 함수 객체일 경우 그 객체 크기만큼 포인터의 크기가 증가
    • 상태 없는 함수 객체 (람다)의 경우 크기 변화가 없으
  • 스마트 포인터 팁

    • 다중 인자 템플릿에서 파라미터를 받아 생성할 때, Ts … params 을 완벽하게 전달하기 위해 forward를 사용
    • std::unique_ptr<T[]>: 배열용 포인터
    • std::shared로 쉽게 변환 가능, 효율적
      • 팩터리 함수의 리턴타입으로 적합한 이유
      • 좀 더 유연한 동기(sibling)으로 변환할 수 있는 여지

기억해 둘 사항

  • std::unique_ptr은 독점 소유권 의미론을 가진 자원의 관리를 위한, 작고 빠른 이동 전용 스마트 포인터

  • 기본적으로 자원 파괴는 delete를 통해 일어나, 커스텀 삭제자를 지정할 수 있음
    • 커스텀 삭제자에 따라 unique_ptr의 크기가 변함
  • std::shared_ptr 로 쉽게 변환 가능

항목19: 소유권 공유 자원의 관리에는 std::shared_ptr 사용하라


  • std::shared_ptr

    • 공유된 소유권 의미론
    • 객체가 더 이상 필요하지 않게 된 시점에서 객체가 파괴됨
  • 자원의 참조 횟수

    • std::shared_ptr의 생성자는 참조횟수를 증가, 소멸자는 감소
    • 복사 대입 연산자는 증가와 감소를 모두 수행

성능

  • std::shared_ptr의 크기는 raw_pointer 의 두배
    • 생포인터 + 참조 횟수
  • 참조횟수를 담는 메모리 = 동적할당
    • shared_ptr가 가리키는 객체 자체는 참조 횟수를 알지 못함
  • std::make_shared를 이용하면 동적할당의 비용 피하기 가능(항목 21)
  • 참조 횟수의 증가와 감소가 반드시 원자적 연산이어야함

    • 원자적 연산은 비원자적 연산보다 느림
  • 참조 횟수가 증가하지 않는 경우
    • 이동 생성
      • 원본 std::shared_ptr은 nullptr이됨, 새 std::shared_ptr의 수명이 시작될 때 기존의 ptr은 더이상 자원을 가리키지 않는 상태
      • std::shared_ptr를 이동하는 것이 복사하는 것보다 빠름 (참조 횟수 증가 여부)

커스텀 삭제자

  • 삭제자의 타입이 포인터 타입의 일부가 아니라 설계가 더 유연
    • 같은 객체에 서로 다른 삭제자
  • 삭제자에 따라 크기가 변하지 않음
    • 삭제자와 무관하게 항상 포인터 두 개 분량
    • 임의의 크기의 삭제자를 추가적인 메모리 없이 지칭..
    • 추가적인 메모리 사용 가능
    • std::shared_ptr의 객체의 일부가 아님
    • 추가 메모리는 힙에서 할당,

제어블록

  • 참조횟수는 제어블록이라고 부르는 더 큰 자료구조의 일부(사실상 표준 구현방법)
  • shared_ptr가 관리하는 객체당 하나의 제어 블록이 존재
  • shared_ptr 생성 시 커스텀 삭제자를 지정하면
    • 참조 횟수 + 커스텀 삭제자의 복사본 = 제어 블록에 담김
    • 커스텀 할당자도 마찬가지
  • 약한 횟수도 포함 (항목21)
  • 최초의 std::shared_ptr가 생성될 때 설정

  • 제어블록2

    • std::make_shared는 항상 제어블록을 생성
      • 공유 포인터가 가리킬 객체를 새로 생성하므로
      • 그 객체에 대한 제어블록이 이미 존재할 가능성 0
    • std::unique_ptr으로부터 shared_ptr을 생성하면, 제어블록이 생성
      • std::unique_ptr은 제어블록을 사용하지 않으므로
    • raw_pointer로 std::shared_ptr 생성자 호출하면 제어블록 생성
    • std::shared_ptr, std::weak_ptr을 생성자 인수로 지정하면 새로운 제어 블록을 만들지 않음
  • 제어블록 크기
    • 커스텀 삭제자 + 커스텀 할당자 => 크기가 더 커질 수 있음
    • 구현이 복잡 (상속 활용, 파괴하기 위한 가상함수 존재)

주의점

  • raw pointer로 shared_ptr를 생성하지 마라
    • 하나의 raw pointer로 여러 개의 std::shared_ptr를 생성하면, 여러 개의 제어블록이 생성
    • 해당 객체가 여러 번 파괴 가능
    • 대안: std::make_shared (그러나 커스텀 삭제자 지정 불가)
  • raw pointer를 사용할 수 밖에 없다면 new 의 결과 직접 전달

  • this pointer를 전달하면, 새 제어블록이 만들어짐 -> 미정의 행동
    • std::enable_shared_from_this 라는 템플릿 사용하여 방지 가능 (CRTP)
      • this 대신 shared_from_this() 사용
      • 이는 이미 shared_ptr이 존재한다는 가정 (없으면 함수의 행동은 정의되지 x)
      • 생성자를 private로 선언, 팩터리 함수를 제공해야함
1
2
3
4
std::vector<std::shared_ptr<Widget>> pw;
void Widget::process() {
  pw.emplace_back(this); // 잘못된 형식
}

std::make_shared

  • std::make_shared로 생성
    • 제어블록의 크기 = 워드 3개
    • std::shared_ptr의 역참조 비용은 raw_pointer 비용보다 크지 않음 (단순히 명령어 하나차이)

불가능한 일

  • std::shared_ptr을 std::unique_ptr로 불가
  • std::shared_ptr<T[]> 없음 즉, 배열관리 불가

기억해 둘 사항들

  • std::unique_ptr 크기의 두배
  • 제어 블록에 관련된 추가 비용이 발생
  • 원자적 참조 횟수 조작을 요구
  • 커스텀 삭제자 지원, 타입과 무관
  • raw pointer로부터 std::shared_ptr를 생성하는 일은 피해야함

항목20: std::shared_ptr처럼 작동하되, 대상을 잃을 수도 있는 포인터가 필요하면 std::weak_ptr을 사용하라


  • 소유권 공유에 참여 x (참조 횟수에 영향 x)
  • 역참조 불가, 널체크 불가
  • shared_ptr를 인자로 생성됨
  • expired()로 객체 유효 체크 가능

std::weak_ptr의 사용

  • 만료 여부를 체크하고, 가리키는 객체에 대한 접근을 돌려주는 연산을 하나의 원자적 연산으로 수행하는 것
    • std::weak_ptr에서 std::shared_ptr을 생성하는 것
      • 이미 만료됨: 널 리턴
1
2
3
std::shared_ptr<Widget> spw1 = wpw.lock();
auto spw2 = wpw.lock(); // spw2는 만료될 수 있음 그러면 null
std::shared_ptr<Widget> spw3{wpw}; // wpw가 만료이면 bad_weak_ptr 발생

캐시 적용 팩터리 함수

  • 팩터리 함수의 비용이 크다고 할 때 (조회를 위해 파일이나 데이터베이스 입출력 수행 등)

    • ID들을 되풀이해서 쓰이는 경우가 많을 때
    • 팩터리 함수와 같은일을 하되, 호출 결과들을캐싱하는 함수를 작성하는 것
      • 요청된 Widget을 캐시에 담아둔다면, 성능상 문제
      • 더이상 쓰지 않는 Widget을 캐시에서 제거해야함
  • 두 조건

    • 호출자가 캐싱된 객체를 가리키는 스마트 포인터를 받아야함
    • 그객체들의 수명을 호출자가 결정할 수 있어야함
      • 팩터리 함수가 돌려준 객체를 클라이언트가 다 사용하고 나면 그 객체는 파괴
      • 캐시 항목은 대상을 잃게됨
  • weak_ptr

    • 캐시에 저장할 포인터는 자신이 대상을 잃었음을 감지할 수 있어야함
    • 팩터리 함수의 반환이 shared_ptr 이어야함

관찰자 패턴

  • Observer

    • 관찰자 대상(subject; 상태가 변할 수 있는 객체)
    • 관찰자(observer; 상태가 변할 때 알림을 받는 객체)
  • 구현

    • 각 관찰자 대상 객체에는 관찰자들을 가리키는 포인터들을 담은 멤버가 있음
    • 관찰 대상은 관찰자들의 수명을 제어하는 데에는 관심이 없지만, 파괴된 관찰자에 접근해서는 안됨
    • 관찰 대상에 합당한 설계
      • 관찰자들을 가리키는 std::weak_ptr들의 컨테이너를 멤버로 두는 것
      • 만료 여부를 보고 관찰자가 유효한지 점검한 후에 관찰자에 접근 가능

더블 링크드 리스트 or 소유권 공유

graph LR;
    A-->B;
    C-->B;
    B-.->A;
  • 위와 같은 상황일 때 B에서 A를 가리키는 포인터는 어떤 포인터?

    1. Raw Pointer
    • A가 파괴되면 B는 dangling pointer를 가지게 됨 (대상 잃은 포인터 역참조)
    1. shared_ptr
    • 순환고리가 생김, A와 B가 파괴 되지 않음
    • 메모리 누수 발생
    1. weak_ptr
    • 위의 두 문제 해결
  • 트리와 같이 엄격한 계층 구조

    • 자식 노드노드들을 오직 그 부모만 소유
    • 부모노드가 파괴 -> 자식들도 파괴
    • 부모에서 자식은 std::unique_ptr로 구현하는게 최선
    • 역링크는 raw pointer로 충분 (수명을 부모가 관리하기 때문에 역참조 위험 x)

효율성

  • std::shared_ptr와 본질적으로 효율성은 동일

  • 객체의 소유권 공유에 참여 x
  • 피지칭 객체의 참조 횟수에 영향 x
  • 제어 블록에는 ‘두 번째’ 참조 횟수가 있음
    • std::weak_ptr이 조작하는 참조횟수

기억해 둘 사항

  • std::shared_ptr처럼 동작하되 대상을 잃을 수 있는 포인터가 필요하면 weak 사용

  • weak 의 잠재적인 용도: 캐싱, 옵저버 패턴에서 옵저버 목록, std::shared_ptr 순환고리 방지

항목21: new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라


std::make_unique 와 std::make_shared

  • std::make_shared는 C++11
  • std::make_unique는 C++14
    • 구현: 매개변수들을 생성할 객체의 생성자로 완벽 전달 (배열 지원 x)
1
2
3
4
5
6
// C++11에서 직접 구현
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
  • 두 make는 임의의 개수와 타입의 인수들을 받아서 그것들을 생성자로 완벽 전달해서 객체를 동적으로 생성하고, 그 객체를 가리키는 스마트포인터를 돌려줌

사용할 이유

1. new 생성과의 본절적인 차이점

1
2
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget());
  • new 방식
    • 생성할 객체의 타입이 되풀이해서 나옴
      • 소프트웨어 공학의 핵심 교의: “코드 중복을 피하라”와 충돌
      • 컴파일 시간이 늘어나고, 목적 코드의 덩치가 커지고, 코드 기반으로 다루기 어려워짐, 일관성 없는 코드로 진화, 비일관성은 버그로 이어짐, 타자량이 많아짐

2. 예외 안정성

1
2
3
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
  • 자원 누수 발생 가능성

    • 컴파일러가 소스코드를 목적코드로 번역하는 방식과 관련
      • 실행 시점에서 함수가 호출될 때, 함수의 코드가 실행되기 전에 함수의 인자들이 먼저 평가됨
    • 위 코드에서 processWidget의 경우 다음과 같은 순서로 실행됨
      1. new Widget이 평가, Widget 객체 힙에 생성
      2. new가 산출한 포인터를 관리하는 std::shared_ptr<Widget>의 생성자가 실행
      3. computePriority가 실행
    • 컴파일러가 위 순서대로 실행하는 코드를 생성해야하는 것은 아님
      1. new Widget이 평가, Widget 객체 힙에 생성
      2. computePriority가 실행
      3. std::shared_ptr<Widget>의 생성자가 실행
    • 이렇게 computePriority가 먼저 실행될 때, 이 함수가 예외를 던지면, 1에의해 메모리 누수 발생
  • 이러한 문제는 make_shared, make_unique 함수를 사용하면 해결 가능

3. 향상된 효율성

  • 컴파일러가 좀 더 간결한 자료구조를 사용하는 더 작고 빠른 코드를 산츨할 수 있게 됨
1
std::shared_ptr<Widget> spw(new Widget());
  • new 사용시
    • 한 번의 메모리 할당 실행 x
    • 두 번의 할당이 일어남
      • 제어 블록을 위한 메모리 할당
1
auto spw = std::make_shared<Widget>();
  • make_shared 사용시

    • 한 번의 메모리 할당
      • 제어 블록과 Widget 객체 모두를 담을 수 있는 크기의 메모리 조각을 한 번에 할당
      • 프로그램의 정적 크기가 줄어듦
      • 실행 코드 속도도 빨리짐
      • 제어 블록에 일정 정도의 내부 관리용 정보를 포함할 필요가 없어, 전체적인 메모리 사용량이 줄어듦
  • std::allocate_shared 또한 make_shared 장점이 그대로 적용됨

make 함수 사용할 수 없는 상황

  • 커스텀 삭제자 지정 불가

  • 구현들의 구문적 세부사항에서 비롯된 한계
  • std::initializer_list를 받는 생성자와 받지않는 생성자를 모두 가진 형식의 객체를 생성할 때
    • 중괄호나 괄호에 따라 버전이 달라짐
    • make는 forward를 통해 객체의 생성자에 완벽하게 전달
    • 이 때, 구현이 중괄호? 괄호? 에 따라 커다란 차이가 생길 수 있음
  • make함수들은 괄호를 사용함
  • 따라서 중괄호 초기치로 생성하려면 new를 사용해야함
    • 아니면, initializer_list 객체를 생성하여 파라미터로 넘겨주어야함

중괄호 초기치는 forward 불가

std::shared_ptr만의 문제

  1. 클래스에 자신만의 new와 delete를 정의하는 경우 std::make_shared는 부적합
  • 이 상황은 전역 메모리 할당 루틴과 해제 루틴이 그 타입의 객체에 적합하지 않음을 의미
  • 클래스 고유 메모리 관리 루틴: 클래스의 객체와 정확히 같은 크기의 메모리 조각들만 할당, 해제하는 경우가 많음
  • 이런 경우 커스텀 할당(std::allocate_shared)과 커스텀 해제에는 잘안맞음
  • std::allocate_shared가 요구하는 메모리 조각의 크기는 동적으로 할당되는 객체의 크기가 아니라, 그 크기에 제어블록의 크기를 더한 것이기 때문.
  1. make_shared의 속도상 장점은 제어블록이 객체와 동일한 메모리 조각에 놓이기 때문
  • 하지만, 객체가 차지하고 있던 메모리는, 제어블록이 파괴되기 전까지 해제 불가
  • make함수가 할당한 메모리 조각은 shared, weak 둘다 파괴된 후 해제됨

    • weak_ptr이 참조횟수를 점검해야함
    • weak_ptr이 존재하는한 제어블록도 존재해야함
  • 객체의 크기가 크고, weak 파괴와 shared의 파괴 간격이 길다면, 메모리 해제되는 시점 사이에 시간 지연이 생김

  • new를 사용하는 경우 shared_ptr이 파괴되면 즉시 그 객체의 메모리가 해제

  • 최선의 방법
    • new의 결과를 하나의 문장에서 즉시 넘겨주는것
      • 커스텀 삭제자와 같이 넘기고 예외가 발생하면, 커스텀 삭제자로 메모리 누수 방지
      • 비효율적임: 함수 호출에서 왼값을 넘겨준다는것, 복사가 발생한다는것, std::move로 오른값변환을 해야함
1
processWidget(std::move(spw), computePriority());

기억해 둘 사항들

  • new 직접 사용에 비해, make를 사용하면

    • 코드 중복의 여지가 없음
    • 예외 안전성 향상
    • allocate_shared와 마찬가지로 더 작고 빠른 코드가 산출됨
  • make 함수 부적절한 경우

    • 커스텀 삭제자 지정해야하는 경우
    • 중괄호 초기치를 전달해야하는 경우
  • make_shared 함수 부적절한 경우

    • 커스텀 메모리 관리 기능을 가진 클래스를 다루는 경우
    • 메모리가 넉넉하지 않은 시스템에서 큰 객체를 자주 다루고, weak_ptr의 수명이 긴 경우

항목22: Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현 파일에서 정의하라


  • Pimpl 관용구
    • 클래스의 멤버들을 포인터로 대체
    • 클래스에서 쓰이는 자료 멤버들을 그 구현 클래스로 옮기고
    • 포인터를 통해서 그 자료 멤버들에 간접적으로 접근하는 기법
1
2
3
4
5
class Widget {
  std::string name;
  std::vector<double> data;
  Gadget g1, g2, g3;
}
  • 위 코드를 C++98에서는 다음과 같이 구현
1
2
3
4
5
6
class Widget {
  // 생성자, 소멸자
  // ...
  struct Impl;
  Impl* pImpl;
};
  • 그 다음, 원래의 클래스에서 사용하던 자료 멤버들을 담는 객체를 동적으로 할당, 해제하는 코드를 추가하는 것

    • 이러한 할당 및 해제 코드는 클래스를 구현하는 소스 코드 파일에 둠
  • 이렇게하면, 의존성들이 헤더에서 cpp 파일로 옮길 수 있음

    • 단, 반드시 포인터를 동적으로 할당 및 해제해야함

스마트 포인터를 사용한 PIMPL

1
2
3
4
class Widget {
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};
1
2
3
4
5
6
  struct Widget::Impl {
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
  };
  Widget::Widget() : pImpl(std::make_unique<Impl>) {}
  • 소멸자가 없어도되지만 컴파일 오류 발생

  • 컴파일러는 소멸자를 자동으로 생성, pImpl의 소멸자를 호출하는 코드를 삽입

    • unique_ptr의 기본 삭제자는 불완전한 형식을 가리키지 않는지를 C++11의 static_assert를 이용하여 점검
    • 컴파일러는 형식의 정의를 보게 되면 그 형식을 완전한 형식으로 간주
    • Widget::Impl의 정의는 cpp 파일에 있음
      • 따라서 Impl 정의 이후에 컴파일러가 그 소스 파일에만 있는 Widget의 소멸자의 본문 을 보게한다면, 문제없이 컴파일러됨
1
2
3
4
5
6
7
class Widget {
  // 생성자
  // ...
  ~Widget();
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};
1
2
3
4
5
6
7
8
  struct Widget::Impl {
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
  };

  Widget::Widget() : pImpl(std::make_unique<Impl>) {}
  Widget::~Widget() = default;

이동 연산

  • Pimpl 관용구를 사용하는 클래스는 이동 연산들을 지원하기에 자연스러운 후보
  • 컴파일러가 자동으로작성하는 이동 연산들이 그런 클래스의 요구에 맞는 std::unique_ptr에 대한 이동을 수행하기 때문
1
2
3
4
5
6
7
8
9
10
11
class Widget {
  // 생성자
  // ...
  ~Widget();

  Widget(Widget&& rhs) = default; // 오류
  Widget& operator=(Widget&& rhs) = default; // 오류

  struct Impl;
  std::unique_ptr<Impl> pImpl;
};
  • 하지만 소멸자를 선언하면, 이동 연산들을 직접 작성해야함
    • 헤더파일안에 pImpl이 불완전하기에, 자동으로 생성하는것이 불가능 (컴파일러가 자동으로 작성한 이동 연산자는 pImpl을 재배정하기 전에 pImpl이 가리키는 객체를 파괴해야함)
    • 이동 연산들의 정의를 구현파일로 옮기면됨
1
2
3
4
5
6
7
8
9
10
  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;

깊은 복사 연산

  • std::unique_ptr같은 이동 전용 타입이 있는 클래스에 대해서는, 컴파일러가 복사 연산들을 작성해주지 않는다.

  • 작성한다고 해도, 작성된함수들은 std::unique_ptr 자체만 복사하는 얕은 복사를 수행하기 때문

1
2
3
4
5
6
7
8
9
10
class Widget {
  // 생성자, 이동, 소멸...
  // ...

  Widget(Widget& rhs);
  Widget& operator=(Widget& rhs);

  struct Impl;
  std::unique_ptr<Impl> pImpl;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  struct Widget::Impl {
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
  };
  // ... 소멸 등
  Widget::Widget(Widget& rhs): pImpl(nullptr) {
    if(rhs.pImpl) {
      pImpl = std::make_unique<Impl>(*rhs.pImpl);
    }
  }
  Widget::Widget& operator=(Widget& rhs) {
    if(!rhs.pImpl) {
      pImpl.reset()
    }
    else if(!pImpl) {
      pImpl = std::make_unique<Impl>(*rhs.pImpl);
    }
    else {
      *pImpl = *rhs.pImpl;
    }
    return *this;
  }
  • 위 코드에서 널체크한 것, Impl의 복사연산을 활용한 것을 확인 가능

std::shared_ptr을 활용한 Pimpl

  • 이 경우, 소멸자를 선언할 피룡가 없으며, 컴파일러가 이동연산들을 작성함

  • 이러한 차이는, 커스텀 삭제자를 지원하는 방식의 차이에서 비롯된 것

    • unique_ptr의 삭제자 형식
      • 포인터 형식의 일부
      • 그렇기에, 컴파일러가 작성하는 멤버 함수가 쓰이는 시점에서 피지칭 형식들이 완전한 형식들이어야함
    • shared_ptr의 삭제자 형식
      • 포인터 형식의 일부가 아님
      • 실행시점 자료구조가 더 크고, 코드도 좀 느려짐
      • 그러나, 컴파일러가 작성하는 멤버 함수가 쓰이는 시점에서 피지칭 형식들이 완전한 형식이 아니어도 상관없음

기억해 둘 사항들

  • Pimpl 관용구는 클래스 구현과 클래스 클라이언트 사이의 컴파일 의존성을 줄임

    • 빌드시간 감소
  • unique_ptr의 경우

    • 소멸자 등 함수들을 헤더에 선언하고, 구현파일에서 구현해야함
This post is licensed under CC BY 4.0 by the author.

[ostep] 제한적 직접 실행 원리

[C++] emc++ 04: Move, Perfect forwarding