Effective modern cpp item18, 19
Transcript of Effective modern cpp item18, 19
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을 생성하는 일은 피해야 한다