Effective Modern C++ 스터디
18장, 19장
이데아 게임즈 손진화
시작하기 전에..
• 생 포인터(raw pointer)의 단점
1. 하나의 객체를 가리키는지 배열을
가리키는지 구분하기 어렵다
2. 포인터 사용 후에 가리키는 객체를 삭제해야
되는지 알 수 없다 (소유)
3. 파괴 방법을 알 수 없다
(delete 사용가능 여부)
생 포인터의 단점
4. delete 와 delete [] 중 뭘 써야 하는지 알기
어렵다
5. 파괴는 한 번만 하도록 신경 써야 한다
6. 포인터가 가리키는 객체가 여전히
살아있는지 알 방법이 없다
스마트 포인터
• 앞의 단점들로 인한 버그를 줄이기 위해 나온
포인터
• 생 포인터를 감싸는 형태로 구현
• 생 포인터의 기능을 거의 대부분 지원한다
스마트 포인터의 종류
• auto_ptr
C++ 11 에서 삭제
unique_ptr 로 대체 됨
• unique_ptr
한 명의 소유자만 허용
스마트 포인터의 종류
• shared_ptr
참조 횟수를 계산
• weak_ptr
shared_ptr 가리키는 대상을 가리킬 수 있지만
참조 횟수에 영향을 주지 않음
18장. 소유권 독점 자원의
관리에는 std::unique_ptr 을
사용하라
특징
• 기본적으로 생 포인터와 크기가 같다
• unique_ptr 객체가 사라지면 가리키는
인스턴스도 해제된다
void f() {
unique_ptr<int> a(new int(3));
cout << *a.get() << endl;
} // a에 할당된 메모리가 해제됨!
독점적 소유권
• unique_ptr 은 자신이 가리키고 있는
객체에 대한 소유권을 가지고 있으며 다른
unique_ptr 이 동시에 참조할 수 없다
• 소유권 이동은 가능
예제
class Investment
{
public:
virtual ~Investment();
};
class Stock : public Investment
{
public:
~Stock();
};
class Bond : public Investment
{ // ... 생략 ... };
class RealEstate : public Investment
{ // ... 생략 ... };
예제
template<typename... Ts>
std::unique_ptr<Investment> makeInvestment
(eInvestmentType type, Ts&&... params)
{
std::unique_ptr<Investment> pInv(nullptr);
if (type == eInvestmentType.STOCK)
{
pInv.reset(new Stock(std::forward<Ts>(params)...);
}
// ... 생략 ...
return pInv;
}
예제
template<typename... Ts>
std::unique_ptr<Investment> makeInvestment
(eInvestmentType type, Ts&&... params)
{
std::unique_ptr<Investment> pInv(nullptr);
if (type == eInvestmentType.STOCK)
{
pInv.reset(new Stock(std::forward<Ts>(params)...);
}
// ... 생략 ...
return pInv;
}
예제 – 커스텀 삭제자
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(eInvestmentType type,
Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
// ... 생략 ...
}
예제 – 커스텀 삭제자
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(eInvestmentType type,
Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
// ... 생략 ...
}
예제 (C++ 14)
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(eInvestmentType type, Ts&&... params)
{
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
// ... 생략 ...
}
커스텀 삭제자
• 반환 형식의 크기는 함수 포인터의 크기 만큼
증가한다
• 삭제자가 함수 객체일 때는 함수 객체에
저장된 상태의 크기만큼 증가한다
• 갈무리 없는 람다 표현식 권장
배열
• 개별 객체와 배열 객체 포인터 따로 지원
• std::unique_ptr<T>
색인 연산 [] 지원 안됨
• std::unique_ptr<T[]>
->, * 지원 안됨
shared_ptr로 변환
• shared_ptr 로의 변환이 쉽고 효율적임
std::shared_ptr<Investment> sp =
makeInvestment(…);
결론
• unique_ptr은 독점 소유권 의미론을 가진 자
원의 관리를 위한, 작고 빠른 이동전용 똑똑한
포인터이다.
• 기본적으로 자원 파괴는 delete를 통해 일어
나나, 커스텀 삭제자를 지정할 수 도 있다. 상
태 있는 삭제자나 함수 포인터를 사용하면
unique_ptr 객체의 크기가 커진다
결론
• unique_ptr를 shared_ptr로 손쉽게 변환할
수 있다
19장. 소유권 공유 자원의
관리에는 std::shared_ptr를
사용하라
Idea
• 객체 메모리를 일일이 관리하긴 귀찮지만 안
쓸 때 바로 해제하고 싶어!
C++
가비지
콜렉터를
지원하는
언어
shared_ptr
How?
참조횟수
• 해당 자원을 가리키는 shared_ptr 의 개수
• 참조횟수가 0이 되면 자원을 파괴한다
성능
• shared_ptr의 크기는 생 포인터의 2배이다
자원을 가리키는 포인터 +
참조 횟수를 저장하는 포인터
• 참조 횟수를 증감하는 연산은 원자적
연산이다
- 멀티 스레드환경에서도 안전함을
보장해야 하기 때문
- 이동 생성의 경우 횟수가 변하지 않는다
성능
• 참조 횟수를 담는 메모리도 동적으로
할당된다
- 객체는 동적 할당 될 때 참조 횟수를 따로
저장하지 않기 때문
- 내장 형식도 shared_ptr로 선언가능
- std::make_shared를 선언하면 비용을 줄
일 수 있다(커스텀 삭제자 지원 안됨)
커스텀 삭제자
• unique_ptr
선언
unique_ptr<Widget,
decltype(loggingDel)> upw
(new Widget, loggingDel);
타입
std::_ptr<Widget,
lambda []void (Widget *pw)->void>
• 삭제자에 따라 ptr 크기가 변한다
커스텀 삭제자
• shared_ptr
선언
std::shared_ptr<Widget> spw
(new Widget, loggingDel);
타입
std::shared_ptr<Widget>
• 삭제자를 지정해도 크기가 변하지 않는다
커스텀 삭제자
auto costomDeleter1 = [](Widget *pw) {delete pw; };
auto costomDeleter2 = [](Widget *pw) {delete pw; };
unique_ptr<Widget, decltype(costomDeleter1)> upwDeleter1
(new Widget, costomDeleter1);
unique_ptr<Widget, decltype(costomDeleter2)> upwDeleter2
(new Widget, costomDeleter2);
vector<unique_ptr<Widget, decltype(costomDeleter1)>> vupw;
vupw.push_back(upwDeleter1);
vupw.push_back(upwDeleter2); // error!
커스텀 삭제자
auto costomDeleter1 = [](Widget *pw) {delete pw; };
auto costomDeleter2 = [](Widget *pw) {delete pw; };
std::shared_ptr<Widget> spwDeleter1(new Widget, costomDeleter1);
std::shared_ptr<Widget> spwDeleter2(new Widget, costomDeleter2);
vector<shared_ptr<Widget>> vspw;
vspw.push_back(spwDeleter1);
vspw.push_back(spwDeleter2); // ok
제어블록
• shared_ptr 이 관리하는 객체 1개당 제어블
록 1개가 생성된다
제어블록 생성 규칙
• std::make_shared는 항상 제어블록을
생성한다
shared_ptr을 가리키는 객체를 새로
생성하기 때문에 그 객체에 대한 제어블록이
이미 존재할 가능성이 없다
• shared_ptr이나 weak_ptr로부터
shared_ptr을 생성하면 기존 포인터에서
가지고 있는 제어블록을 참고한다
제어블록 생성 규칙
• 고유 소유권 포인터(unique_ptr, auto_ptr)
로부터 shared_ptr 객체를 생성하면
제어블록이 생성된다
- 고유 소유권은 제어블록을 사용하지 않기
때문에 해당 객체에 대한 제어블록이 없다고
보장한다
- 고유 소유권 포인터는 shared_ptr로 이동
하면 해당 객체에 대한 권한을 상실한다
제어블록 생성 규칙
• 생 포인터로 shared_ptr을 생성하면 제어블
록이 생성된다
auto praw = new int(11);
shared_ptr<int> spwFromRaw1(praw);
shared_ptr<int> spwFromRaw2(praw);
// 미정의 행동!
shared_ptr<int> spwGood(new int(11));
// 미정의 행동 방지
this 포인터
class Widget;
vector<shared_ptr<Widget>> processWidgets;
class Widget
{
public:
void process()
{
processWidgets.emplace_back(this);
}
};
// 이미 해당객체를 가리키는 다른 shared_ptr이 있다면 문제가 됨
this 포인터
class Widget;
vector<shared_ptr<Widget>> processWidgets;
class Widget : public enable_shared_from_this<Widget>
{
public:
void process()
{
processWidgets.emplace_back(shared_from_this());
}
};
// Curiously Recurring Template Pattern
// 문제 해결?
this 포인터
class Widget;
vector<shared_ptr<Widget>> processWidgets;
class Widget : public enable_shared_from_this<Widget>
{
public:
template<typename ... Ts>
static shared_ptr<Widget> Create(void)
{
return shared_ptr<Widget>(new Widget());
}
void process()
{
processWidgets.emplace_back(shared_from_this());
}
private:
Widget() {};
};
비싼 비용?
• make_shared로 shared_ptr을 생성하면
제어 블록할당 비용은 무료다
• 제어블록에 있는 가상 함수는 많이 호출되지
않는다
• 원자 연산은 기계어 명령에 대응되기 때문에
비용이 그렇게 크지 않다
• 그래도 부담스럽다면 unique_ptr로 선언한
뒤 업그레이드 하면 된다
그 외
• 단일 객체 관리를 염두에 두고 설계되었기
때문에 operator[]를 제공하지 않는다
결론
• shared_ptr는 임의의 공유 자원의 수명을
편리하게 관리할 수 있는 수단을 제공한다
• 대체로 shared_ptr객체의 크기는
unique_ptr의 두 배이며, 제어 블록에
관련된 추가 부담을 유발하며,
원자적 참조 횟수 조작을 요구한다.
결론
• 자원은 기본적으로 delete를 통해 파괴되나
커스텀 삭제자도 지원한다. 삭제자의 형식은
shared_ptr의 형식에 아무런 영향도 미치지
않는다
• 생 포인터의 형식의 변수로부터
shared_ptr을 생성하는 일은 피해야 한다

Effective modern cpp item18, 19

  • 1.
    Effective Modern C++스터디 18장, 19장 이데아 게임즈 손진화
  • 2.
    시작하기 전에.. • 생포인터(raw pointer)의 단점 1. 하나의 객체를 가리키는지 배열을 가리키는지 구분하기 어렵다 2. 포인터 사용 후에 가리키는 객체를 삭제해야 되는지 알 수 없다 (소유) 3. 파괴 방법을 알 수 없다 (delete 사용가능 여부)
  • 3.
    생 포인터의 단점 4.delete 와 delete [] 중 뭘 써야 하는지 알기 어렵다 5. 파괴는 한 번만 하도록 신경 써야 한다 6. 포인터가 가리키는 객체가 여전히 살아있는지 알 방법이 없다
  • 4.
    스마트 포인터 • 앞의단점들로 인한 버그를 줄이기 위해 나온 포인터 • 생 포인터를 감싸는 형태로 구현 • 생 포인터의 기능을 거의 대부분 지원한다
  • 5.
    스마트 포인터의 종류 •auto_ptr C++ 11 에서 삭제 unique_ptr 로 대체 됨 • unique_ptr 한 명의 소유자만 허용
  • 6.
    스마트 포인터의 종류 •shared_ptr 참조 횟수를 계산 • weak_ptr shared_ptr 가리키는 대상을 가리킬 수 있지만 참조 횟수에 영향을 주지 않음
  • 7.
    18장. 소유권 독점자원의 관리에는 std::unique_ptr 을 사용하라
  • 8.
    특징 • 기본적으로 생포인터와 크기가 같다 • unique_ptr 객체가 사라지면 가리키는 인스턴스도 해제된다 void f() { unique_ptr<int> a(new int(3)); cout << *a.get() << endl; } // a에 할당된 메모리가 해제됨!
  • 9.
    독점적 소유권 • unique_ptr은 자신이 가리키고 있는 객체에 대한 소유권을 가지고 있으며 다른 unique_ptr 이 동시에 참조할 수 없다 • 소유권 이동은 가능
  • 10.
    예제 class Investment { public: virtual ~Investment(); }; classStock : public Investment { public: ~Stock(); }; class Bond : public Investment { // ... 생략 ... }; class RealEstate : public Investment { // ... 생략 ... };
  • 11.
    예제 template<typename... Ts> std::unique_ptr<Investment> makeInvestment (eInvestmentTypetype, Ts&&... params) { std::unique_ptr<Investment> pInv(nullptr); if (type == eInvestmentType.STOCK) { pInv.reset(new Stock(std::forward<Ts>(params)...); } // ... 생략 ... return pInv; }
  • 12.
    예제 template<typename... Ts> std::unique_ptr<Investment> makeInvestment (eInvestmentTypetype, Ts&&... params) { std::unique_ptr<Investment> pInv(nullptr); if (type == eInvestmentType.STOCK) { pInv.reset(new Stock(std::forward<Ts>(params)...); } // ... 생략 ... return pInv; }
  • 13.
    예제 – 커스텀삭제자 auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; }; template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(eInvestmentType type, Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // ... 생략 ... }
  • 14.
    예제 – 커스텀삭제자 auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; }; template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(eInvestmentType type, Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // ... 생략 ... }
  • 15.
    예제 (C++ 14) template<typename...Ts> std::unique_ptr<Investment, decltype(delInvmt)> makeInvestment(eInvestmentType type, Ts&&... params) { auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; }; std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // ... 생략 ... }
  • 16.
    커스텀 삭제자 • 반환형식의 크기는 함수 포인터의 크기 만큼 증가한다 • 삭제자가 함수 객체일 때는 함수 객체에 저장된 상태의 크기만큼 증가한다 • 갈무리 없는 람다 표현식 권장
  • 17.
    배열 • 개별 객체와배열 객체 포인터 따로 지원 • std::unique_ptr<T> 색인 연산 [] 지원 안됨 • std::unique_ptr<T[]> ->, * 지원 안됨
  • 18.
    shared_ptr로 변환 • shared_ptr로의 변환이 쉽고 효율적임 std::shared_ptr<Investment> sp = makeInvestment(…);
  • 19.
    결론 • unique_ptr은 독점소유권 의미론을 가진 자 원의 관리를 위한, 작고 빠른 이동전용 똑똑한 포인터이다. • 기본적으로 자원 파괴는 delete를 통해 일어 나나, 커스텀 삭제자를 지정할 수 도 있다. 상 태 있는 삭제자나 함수 포인터를 사용하면 unique_ptr 객체의 크기가 커진다
  • 20.
    결론 • unique_ptr를 shared_ptr로손쉽게 변환할 수 있다
  • 21.
    19장. 소유권 공유자원의 관리에는 std::shared_ptr를 사용하라
  • 22.
    Idea • 객체 메모리를일일이 관리하긴 귀찮지만 안 쓸 때 바로 해제하고 싶어! C++ 가비지 콜렉터를 지원하는 언어 shared_ptr
  • 23.
    How? 참조횟수 • 해당 자원을가리키는 shared_ptr 의 개수 • 참조횟수가 0이 되면 자원을 파괴한다
  • 24.
    성능 • shared_ptr의 크기는생 포인터의 2배이다 자원을 가리키는 포인터 + 참조 횟수를 저장하는 포인터 • 참조 횟수를 증감하는 연산은 원자적 연산이다 - 멀티 스레드환경에서도 안전함을 보장해야 하기 때문 - 이동 생성의 경우 횟수가 변하지 않는다
  • 25.
    성능 • 참조 횟수를담는 메모리도 동적으로 할당된다 - 객체는 동적 할당 될 때 참조 횟수를 따로 저장하지 않기 때문 - 내장 형식도 shared_ptr로 선언가능 - std::make_shared를 선언하면 비용을 줄 일 수 있다(커스텀 삭제자 지원 안됨)
  • 26.
    커스텀 삭제자 • unique_ptr 선언 unique_ptr<Widget, decltype(loggingDel)>upw (new Widget, loggingDel); 타입 std::_ptr<Widget, lambda []void (Widget *pw)->void> • 삭제자에 따라 ptr 크기가 변한다
  • 27.
    커스텀 삭제자 • shared_ptr 선언 std::shared_ptr<Widget>spw (new Widget, loggingDel); 타입 std::shared_ptr<Widget> • 삭제자를 지정해도 크기가 변하지 않는다
  • 28.
    커스텀 삭제자 auto costomDeleter1= [](Widget *pw) {delete pw; }; auto costomDeleter2 = [](Widget *pw) {delete pw; }; unique_ptr<Widget, decltype(costomDeleter1)> upwDeleter1 (new Widget, costomDeleter1); unique_ptr<Widget, decltype(costomDeleter2)> upwDeleter2 (new Widget, costomDeleter2); vector<unique_ptr<Widget, decltype(costomDeleter1)>> vupw; vupw.push_back(upwDeleter1); vupw.push_back(upwDeleter2); // error!
  • 29.
    커스텀 삭제자 auto costomDeleter1= [](Widget *pw) {delete pw; }; auto costomDeleter2 = [](Widget *pw) {delete pw; }; std::shared_ptr<Widget> spwDeleter1(new Widget, costomDeleter1); std::shared_ptr<Widget> spwDeleter2(new Widget, costomDeleter2); vector<shared_ptr<Widget>> vspw; vspw.push_back(spwDeleter1); vspw.push_back(spwDeleter2); // ok
  • 30.
    제어블록 • shared_ptr 이관리하는 객체 1개당 제어블 록 1개가 생성된다
  • 31.
    제어블록 생성 규칙 •std::make_shared는 항상 제어블록을 생성한다 shared_ptr을 가리키는 객체를 새로 생성하기 때문에 그 객체에 대한 제어블록이 이미 존재할 가능성이 없다 • shared_ptr이나 weak_ptr로부터 shared_ptr을 생성하면 기존 포인터에서 가지고 있는 제어블록을 참고한다
  • 32.
    제어블록 생성 규칙 •고유 소유권 포인터(unique_ptr, auto_ptr) 로부터 shared_ptr 객체를 생성하면 제어블록이 생성된다 - 고유 소유권은 제어블록을 사용하지 않기 때문에 해당 객체에 대한 제어블록이 없다고 보장한다 - 고유 소유권 포인터는 shared_ptr로 이동 하면 해당 객체에 대한 권한을 상실한다
  • 33.
    제어블록 생성 규칙 •생 포인터로 shared_ptr을 생성하면 제어블 록이 생성된다 auto praw = new int(11); shared_ptr<int> spwFromRaw1(praw); shared_ptr<int> spwFromRaw2(praw); // 미정의 행동! shared_ptr<int> spwGood(new int(11)); // 미정의 행동 방지
  • 34.
    this 포인터 class Widget; vector<shared_ptr<Widget>>processWidgets; class Widget { public: void process() { processWidgets.emplace_back(this); } }; // 이미 해당객체를 가리키는 다른 shared_ptr이 있다면 문제가 됨
  • 35.
    this 포인터 class Widget; vector<shared_ptr<Widget>>processWidgets; class Widget : public enable_shared_from_this<Widget> { public: void process() { processWidgets.emplace_back(shared_from_this()); } }; // Curiously Recurring Template Pattern // 문제 해결?
  • 36.
    this 포인터 class Widget; vector<shared_ptr<Widget>>processWidgets; class Widget : public enable_shared_from_this<Widget> { public: template<typename ... Ts> static shared_ptr<Widget> Create(void) { return shared_ptr<Widget>(new Widget()); } void process() { processWidgets.emplace_back(shared_from_this()); } private: Widget() {}; };
  • 37.
    비싼 비용? • make_shared로shared_ptr을 생성하면 제어 블록할당 비용은 무료다 • 제어블록에 있는 가상 함수는 많이 호출되지 않는다 • 원자 연산은 기계어 명령에 대응되기 때문에 비용이 그렇게 크지 않다 • 그래도 부담스럽다면 unique_ptr로 선언한 뒤 업그레이드 하면 된다
  • 38.
    그 외 • 단일객체 관리를 염두에 두고 설계되었기 때문에 operator[]를 제공하지 않는다
  • 39.
    결론 • shared_ptr는 임의의공유 자원의 수명을 편리하게 관리할 수 있는 수단을 제공한다 • 대체로 shared_ptr객체의 크기는 unique_ptr의 두 배이며, 제어 블록에 관련된 추가 부담을 유발하며, 원자적 참조 횟수 조작을 요구한다.
  • 40.
    결론 • 자원은 기본적으로delete를 통해 파괴되나 커스텀 삭제자도 지원한다. 삭제자의 형식은 shared_ptr의 형식에 아무런 영향도 미치지 않는다 • 생 포인터의 형식의 변수로부터 shared_ptr을 생성하는 일은 피해야 한다