Post List

2015년 4월 12일 일요일

item 18: 공유하지 않을 자원관리에는 std::unqiue_ptr을 사용하자. (MVA Version)

작가
Meyers, Scott
출판
O'ReillyMedia
발매
2014.12.12
평점








Effective Modern C++ - Scott Meyers
Item 18: Use std::unique_ptr for exclusive-ownership resource management.

item 18. 공유하지 않을 자원관리에는 std::unique_ptr을 사용하자.

스마트 포인터 중에 가장 대표적인 것으로 std::unique_ptr 이 있습니다.
가장 많이 사용되는 것에 대해서는 std::shared_ptr 이냐 std::unique_ptr이냐 논란이 있을 수 있지만,
그래도 기본적으로는 std::unique_ptr로 생성하는 경우가 많습니다.
(그 이유에 대해서는 뒤에 자세히 설명해 드리겠습니다.)


std::unique_ptr의 특징

- default로 선언할 경우 Size가 Raw Pointer 만큼 작습니다.
  (custom deleter 를 사용할 경우에는 해당 함수의 state만큼 크기가 커집니다.)

- Raw Pointer의 연산자 대부분을 다 지원합니다.

- 특정 개체에 대하여 배타적 소유권 (독점권) 을 가집니다.

- MOVE-only 타입입니다.

위와 같은 특징을 풀어서 설명하자며,

1. Raw Pointer 만큼 작고 빠르며, 거의 흡사한 방법으로 사용이 가능합니다.

2. default로는 가리키는 개체를 해제할 때 delete 연산을 사용합니다만,
   custom deleter를 사용할 수도 있습니다.

3. std::unique_ptr의 값이 null이 아니라면, 가리키고 있는 개체에 대하여 독점 소유권을 가집니다.
   std::unique_ptr이 해제될때, 가리키고 있는 개체도 같이 해제 됩니다.

4. std::unique_ptrMOVE하면 SRC 포인터에 값은 null이 되면서 DST 포인터로 소유권을 넘겨주게 됩니다.
  COPY 연산은 허용되지 않습니다.


* Factory method pattern using std::unique_ptr

디자인 패턴 중에 Factory method pattern에 대해서 들어본 적이 있으실 겁니다.
자세한 설명 및 예제는 아래 글을 참조해 주시기 바랍니다.
( http://devluna.blogspot.kr/2015/01/factory-method-pattern.html  )

Factory method pattern이 많이 사용하는 동작방식에 대해서 간단히 설명 드리자면,

1. 사용자가 개체 생성을 요구하면,
2. 개체 생성시 Heap에 동적으로 생성한 뒤,
3. 그것의 Pointer를 return 합니다.
4. 사용자는 해당 개체를 사용하면서,
5. 다 사용한 뒤 필요없어졌을 때 책임지고 그 개체를 해제시켜주어야 합니다.

위의 과정은 std::unique_ptr의 동작과 완벽하게 일치합니다.
거기에 더해서 std::unique_ptr은 자신이 해제될 때 가리키고 있는 개체도 자동으로 같이 해제해줍니다.

한가지 예제를 들어서 설명하겠습니다.
리니지 2라는 게임에 Elf라는 종족이 있습니다.
해당 종족의 Unit 중에 Temple Knight (기사), Elememtal Summoner (소환사), Elder (힐러) 가 있습니다.
이것을 다음과 같이 계층 구조로 표현하였습니다.



class ElvenUnit { ... };

class TempleKnight     : public ElvenUnit { ... };
class ElemetalSummoner : public ElvenUnit { ... };
class Elder            : public ElvenUnit { ... };

위의 경우에 대하여 Factory method는 다음과 같은 방법으로 만들 수 있습니다.

template<typename... Ts>
std::unique_ptr<ElvenUnitMakeElvenUnit(Ts&&... params)
{
    std::unique_ptr<ElvenUnit> pElf(nullptr); // make null unique_ptr
    if ( /*is TK*/)      { pElf.reset(new TempleKnight(std::forward<Ts>(params)...)); }
    else if ( /*is ES*/) { pElf.reset(new ElemetalSummoner(std::forward<Ts>(params)...)); }
    else if ( /*is Ed*/) { pElf.reset(new Elder(std::forward<Ts>(params)...)); }
    return pElf;

}
{
    auto pElf = MakeElvenUnit( arguments ); // std::unique_ptr<ElvenUnit>
    ...
} // destroy *pElf

이 함수에서 만들어진 개체는 범위를 벗어나면 자동으로 해제됩니다.

이제 이렇게 만들어진 Elf 유닛을 부대로 지정하여 사용하도록 해 보겠습니다.
std::unique_ptr 을 std::vector 로 MOVE 하여 사용한 뒤 나중에 해제되도록 하는 시나리오 입니다.

std::vector<std::unique_ptr<ElvenUnit>> ElvenArmy;

{
    auto pElf = MakeElvenUnit( arguments ); // std::unique_ptr<ElvenUnit>

    ElvenArmy.emplace_back(std::move(pElf)); // set pElf to null
}

하지만 factory method에 의해서 return 된 std::unique_ptr을 컨테이너로 MOVE 한 뒤,
나중에 해제 되도록 하는 시나리오를 생각해 봅시다.
std::unique_ptrMOVE 중에 예외나 다른 비정상적인 동작이 일어나게 된다면,
std::unique_ptr은 스스로 Destructor를 호출하여, 자신 및 관리하는 개체를 해제 시킵니다.


* custom deleter

default로는 자원해제를 delete 명령어를 이용해서 하지만, 생성시 custom deleter 설정이 가능합니다.
이 경우 자원 해제시 설정한 deleter 함수 (또는 람다)가 호출됩니다.
MakeElvenUnit()라는 함수에 의해서 생성된 std::unique_ptr이 해제시
해당 Unit이 적을 죽인 만큼 혈맹 경험치를 추가하는 함수를 호출하고자 한다면,
다음과 같이 custom deleter를 생성하면 됩니다.

auto DieElf = [](ElvenUnit* pElf) // custom deleter (using lambda expression)
{
    AddGuildExp(pElf);
    delete pElf;
};

template<typename... Ts>
std::unique_ptr<ElvenUnit, decltype(DieElf)> MakeElvenUnit(Ts&&... params)
{
    std::unique_ptr<ElvenUnit, decltype(DieElf)> pElf(nullptr, DieElf); // make null unique_ptr

    if ( /* Temple Knight 만들 조건이면 */)
    {
       pElf.reset(new TempleKnight(std::forward<Ts>(params)...));
    }
    else if ( /* Elemental Summoner 만들 조건이면 */)
    {
       pElf.reset(new ElemetalSummoner(std::forward<Ts>(params)...));
    }
    else if ( /* Elder 만들 조건이면 */)
    {
       pElf.reset(new Elder(std::forward<Ts>(params)...));
    }

    return pElf;
}

std::unique_ptr의 경우 custom deleter 를 선언시 타입으로 같이 지정해줘야 합니다.
즉, custom deleter 는 std::unique_ptr 타입의 일부분이 됩니다.

MakeElvenUnit()의 결과를 auto에 저장하는 경우라면 편안하게 그냥 사용하면 됩니다만,
그래도 다음 사항들을 이해하고 넘어가면 좋습니다.

DieElf()는 MakeElvenUnit()으로 부터 return되는 개체에 사용된  custom deleter 입니다.
  개체를 delete 하기 전에 해제하기 전에 AddGuildExp()를 호출해 준 뒤에 delete로 해제합니다.
  함수 선언보다는 람다식을 이용하는게 편리합니다.

- custom deleter가 사용하는 경우 그것의 타입을 std::unique_ptr의 2번째 인자로 넣어줘야합니다.
   이 경우 MakeElvenUnit()에서 return되는 타입은 std::unique_ptr<ElvenUnit, decltype(DieElf)>가 됩니다.

-  MakeElvenUnit()의 기본적인 동작은 null std::unique_ptr을 만들어서, 특정 개체를 가리키게 한뒤 return 합니다.
custom deleter DieElf()를 pElf와 연계시킬려면, 2번째 Constructor 인자로 넘겨줘야 합니다.

new를 이용한 Raw Pointer 개체를 std::unique_ptr에 대입하려하면 컴파일되지 않습니다.
  왜냐면, Raw Pointer에서 Smart Pointer로 암시적 변환과정이 일어나게 되는데,
  C++11의 스마트 포인터들은 그걸 금지시켰습니다.
  pElf에 new로 생성한 개체를 넣을려면 reset()을 사용해야 합니다.

new로 개체 생성시 인자 전달에 std::forward<Ts>()를 사용하여 perfect-forward하여 전달하였습니다.
  R-Value, L-Value의 정확한 정보를 전달하기 위해서 입니다.

- custom deleter의 인자 타입은 ElvenUnit* 입니다.
  Factory Method에서 실제로 생성되는 타입들은 Derived class인 TempleKnightElemetalSummonerElder 등이기 때문에,
  ElvenUnit의 Destructor는 virtual로 선언되어야 합니다.


C++14에서는 함수의 return타입에 대한 추론도 가능해졌으므로,
MakeElvenUnit()의 선언을 더 간단하게 할 수 있습니다.
custom deleter의 선언도 해당 함수 안으로 넣어서 보다 더 캡슐화 된 모양을 가지게 되었습니다.

template<typename... Ts>
auto MakeElvenUnit(Ts&&... params)
{
    auto DieElf = [](ElvenUnit* pElf) // custom deleter (using lambda expression)
    {
       AddGuildExp(pElf);
       delete pElf;
    };

    std::unique_ptr<ElvenUnit, decltype(DieElf)> pElf(nullptr, DieElf); // make null unique_ptr

    if ( /* Temple Knight 만들 조건이면 */)
    {
       pElf.reset(new TempleKnight(std::forward<Ts>(params)...));
    }
    else if ( /* Elemental Summoner 만들 조건이면 */)
    {
       pElf.reset(new ElemetalSummoner(std::forward<Ts>(params)...));
    }
    else if ( /* Elder 만들 조건이면 */)
    {
       pElf.reset(new Elder(std::forward<Ts>(params)...));
    }

    return pElf;
}


* size of std::unique_ptr using custom deleter

앞서서 default deleter (delete)로 만들어진 std::unique_ptr은 Raw Pointer와 같은 크기를 가진다고 말했습니다.
하지만, custom deleter를 사용하면 더이상 그렇지 않습니다.
deleter를 function pointer로 생성하면 std::unique_ptr의 크기는 1 word에서 2 word로 증가합니다.

그리고 deleter는 fuction object이기 때문에, 내부에 얼마나 많은 state를 가지고 있느냐에 따라 크기가 결정됩니다.
stateless function object (e.g. captureless lambda)의 경우는 크기 패널티가 없습니다.
고로 deleter로는 function보다는 captureless lambda가 더 바람직 합니다.

auto DieElf = [](ElvenUnit* pElf) // stateless lambda
{
    AddGuildExp(pElf);
    delete pElf;
};

template<typename... Ts>
std::unique_ptr<ElvenUnit, decltype(DieElf)> // return type has size of EvenUnit*
MakeElvenUnit(Ts&&... params);


void DieElf(ElvenUnit* pElf) // function
{
    AddGuildExp(pElf);
    delete pElf;
};

template<typename... Ts>
std::unique_ptr<ElvenUnit, void (*)(ElvenUnit*)> // return type has size of EvenUnit*
MakeElvenUnit(Ts&&... params);                   // + at least size of function pointer !

많은 state를 가진 function object를 사용하면 std::unique_ptr의 크기를 증가시킵니다.
만약 그로 인해서 std::unique_ptr의 크기가 용납할수 없을 정도로 커진다면,
그건 코드 디자인을 잘못 한것으로 판단하여, 다시 수정하는게 좋을 것입니다.

Factory method만이 std::unique_ptr를 사용하는 유일한 예제는 아닙니다.
Pimpl Idiom을 구현하기 위한 메커니즘으로도 많이 사용됩니다.
그 내용에 대해서는 item 22에서 자세히 다루겠습니다.


* 기타 std::unique_ptr에 대한 사항들

std::unique_ptr은 싱글 오브젝트 (std::unique_ptr<T>) 와 배열 (std::unique_ptr<T[]>) 의 2가지 다 가능합니다.
그래서 std::unique_ptr가 가리키는 것에 대한 애매한 점은 없다.
std::unique_ptr API는 그 2가지에 대해서 각각 다르게 생성해 줍니다.
싱글 오브젝트에는 인덱스 연산자 (operator [])가 없고, 배열에는 dereference 연산자가 없습니다. (operator * 와 operator ->)

그런데 사실상 std::unique_ptr에 배열을 할당하는 것은 별로 의미가 없습니다.
차라리 std::arraystd::vector, std::string 등을 사용하는것이 더 효과적입니다.

std::unique_ptr은 std::shared_ptr로 쉽게 캐스팅이 가능하다.
( 반대로 std::shared_ptr에서 std::unique_ptr로는 변환은 불가능 합니다. )
그래서 Factory 함수의 return타입으로 std::unique_ptr이 더 적절합니다.
사용자가 해당 개체를 독점적으로 사용할지, 공유해서 사용할지 모르기 때문입니다.

* Things to remember

std::unique_ptr은 작고, 빠르고, MOVE-only 스마트 포인터이며, 자원을 독점적으로 관리해줍니다.

default로는 개체 해제에 delete를 사용하지만, custom deleter를 사용 할 수도 있다.
  state가 있는 deleter의 경우, 그 크기만큼 std::unique_ptr 개체의 크기가 증가한다.

std::unique_ptr에서 std::shared_ptr로 변환은 간단하다.

댓글 없음:

댓글 쓰기