본문 바로가기

프로그래밍

[스크랩]C++11: unique_ptr

[출처] C++11: unique_ptr|작성자 비블레리|http://oidoman.blog.me/90160979966


http://www.drdobbs.com/cpp/c11-uniqueptr/240002708

 

 

C++11: unique_ptr 

June 26, 2012

Mark Nelson

 

 

C++11에는 대단한 특징들이 많이 있지만, unique_ptr는 코드 위생의 영역에서 두곽을 나타낸다. 간단하게 말하자면, 이것은 동적으로 객체를 생성하기 위한 마법 탄환이다. 이것은 모든 문제를 해결하지는 않지만, 이것이 하는 일은 매우 매우 좋다: 간단한 소유 의미를 가지고 동적으로 생성된 객체를 관리하는 것.

 

 

기초 

 

클래스 템플릿 unique_ptr<T>는 타입 T의 객체에 대한 포인터를 관리한다. 당신은 unique_ptr 생성자 안에서 객체를 생성하기 위해 보통 new를 호출함으로써 이 타입의 객체를 생성할 것이다.

 

std::unique_ptr<foo> p( new foo(42) );

 

생성자 호출 후에, 당신은 그 객체를 마치 생 포인터인양 사용할 수 있다. *과 -> 연산자는 당신이 기대하는 것과 동일하게 동작하고 매우 효율적인데 보통 생 포인터를 접근하는 것과 매우 비슷한 어셈블리 코드를 만들어 낸다.

 

unique_ptr를 사용함으로 얻을 수 있는 첫 번째 이익은 포인터가 범위를 벗어날 때 포함하고 있는 객체를 자동으로 소멸시켜 준다는 것이다. 당신은 객체를 올바르게 해제시키기 위해 모든 가능한 출구를 조사할 필요가 없는 것이다. 그것은 자동으로 이루어진다. 더 중요한 것은 당신의 함수가 예외로 끝나더라도 소멸된다는 것이다.

 

 

컨테이너 

 

지금까지는 좋다. 그러나 가희 혁명적이지는 않다. 내가 지금까지 설명하는 것을 하는 클래스를 작성하는 것은 사소한 일이고, 당신은 원래의 C++ 표준에서도 이것을 할 수 있었다. 사실 불운의 (지금은 폐기된) auto_ptr가 바로 그것이었고, RIAA 포인터 래퍼로서 첫번째 시도였다. 

 

불운하게도, 언어는 auto_ptr가 올바르게 동작하는 데 까지는 발전하지 못했다. 결과적으로 당신은 그것을 약간 기초적인 것으로써 사용할 수 없다. 예를 들어, 당신은 auto_ptr 객체를 대부분의 컨테이너 안에 저장할 수 없다. 매우 큰 문제이다.

 

C++11은 우측값 참조와 이동의 의미를 추가함으로서 이 문제들을 해결했다. 결과적으로, unique_ptr 객체는 컨테이너 안에 저장될 수 있고, 컨테이너가 크기 조절이 되거나 이동될 때 올바르게 동작한다. 그리고 컨테이너가 소멸될 때 같이 소멸된다. 당신이 원하는 바와 같이.

 

 

유일성과 이동 의미론

 

그럼 unique란 단어의 정확한 뜻은 무엇일까? 대부분은 다음과 같다. 당신이 unique_ptr 객체를 생성할 때, 당신은 이 포인터의 하나의 복사본만을 가지려 한다고 선언하는 것이다. 누가 그것을 소유하고 있는지는 의문의 여지가 없는데, 당신은 그 포인터의 복사본들을 우연히 만들 수 없기 때문이다.

 

고전적인 생 포인터를 가진 다음과 같은 코드는 잠재되어 있는 버그를 가진다.

 

foo *p = new foo("useful object");
make_use( p );

 

여기서 나는 객체를 할당했고, 그 객체에 대한 포인터를 가지고 있다. make_use를 호출할 때, 이 포인터에 어떤 일이 벌어질까? make_use는 나중을 위해 포인터에 대한 복사본을 만들까? 포인터에 대한 소유권을 취해서 사용이 끝난 후에 소멸시킬까? 잠시동안 포인터를 빌려오고 그 다음에 호출자에게 포인터를 리턴할까?

 

우리는 확신을 가지고 이런 질문들에 대답할 수 없다. 왜냐하면 C++은 포인터 사용에 관한 계약을 쉽게 만들어 주지 않기 때문이다. 당신은 결국 코드 조사, 메모리, 혹은 문서에 의존할 수 밖에 없다. 이런 모든 것들은 주기적으로 지켜지지 않는다.

 

unique_ptr에서는 이런 문제점들을 가지지 않는다. 만약 당신이 다른 루틴에 포인터를 넘기기를 원한다면, 당신은 그 포인터의 중복된 복사본을 만들지 않을 것이다. 컴파일러가 그것을 거부한다.

 

 

누가 포인터를 소유하는가 

 

예를 보자. 포인터를 하나 만들어서 컨테이너에 저장하기를 원한다. unique_ptr의 새로운 사용자로서, 나는 다음과 같은 코드를 작성했다.

 

std::unique_ptr<foo> q( new foo(42) );
v.push_back( q );

 

이것은 합당한 것처럼 여겨진다. 그러나 이것은 나를 회색 지대로 이끌고 있다. 누가 포인터를 소유하고 있는가? 컨테이너가 어느 시점에 포인터를 소멸시키는가? 혹은 그것은 아직도 나의 일인가?

 

unique_ptr의 사용 규칙은 이런 종류의 코드를 금지하고 있어서, 컴파일 에러가 발생한다.

 

unique.cpp(26) : see reference to class template instantiation
 'std::vector<_Ty>' being compiled

 

어쨌든, 여기서의 문제는 우리는 오직 포인터의 유일한 복사본만을 가질 수 있다는 것이다. 즉 유일한 소유권 규칙이 적용되는 것이다. 만약 내가 그 객체를 다른 코드에 주기를 원한다면, 나는 이동 의미론을 적용해야만 한다.

 

v.push_back( std::move(q) );

 

객체를 컨테이너에게 이동시킨 후에는, 나의 원 unique_ptr인 q는 그 포인터의 소유권을 포기한 것이고 이제는 컨테이너에 있는 것이다. 비록 객체 q가 여전히 존재하지만, 이것을 역참조하는 시도는  널 포인터 예외를 발생시킬 것이다. 사실 이동 동작 후에는, q가 가지고 있는 내부 포인터는 null로 설정된다.

 

이동 의미론은 당신이 우측값 참조를 생성하는 모든 곳에서 자동적으로 사용된다. 예를 들어, 함수로부터 unique_ptr를 리턴하는 것은 어떠한 특별한 코드도 필요로 하지 않는다.

 

return q;

 

또한 함수 호출자에게 새롭게 생성되는 객체를 넘기지도 않는다.

 

process( std::unique_ptr<foo>( new foo(41) ) );

 

 

 

이전 코드 

 

우리 모두는 다뤄져야 할 이전 코드들을 가지고 있고, unique_ptr를 사용할 때 조차도 어떤 경우에는 함수에 생 포인터를 넘겨줘야만 할 때도 있다. 이렇게 하는 2가지 방식이 있다.

 

do_something( q.get() ); //retain ownership
do_something_else( q.release() ); //give up ownership

 

get()을 호출하는 것은 해당하는 메소드에 포인터를 넘겨준다. 당신은 가능하면 이 방식을 피해야만 하는데, 당신이 야생에 생 포인터를 내보내는 순간 unique_ptr로 얻어지는 많은 이점을 잃게 되는 것이다.

 

release()로 포인터를 추출하는 것은 좀 더 현실적인 방법이다. 이것은 당신이 이렇게 말하는 것과 같다. "나는 이 포인터에 대한 소유권을 가지고 있었다. 이제는 네 것이다."

 

 

비용 

 

unique_ptr의 찬성론자들은 이 래퍼 사용의 비용은 아주 작다고 말한다. 그리고 이것은 사실인 듯 보인다. 아래에 나는 클래스 foo의 멤버를 증가시키는 한 쌍의 루틴을 보여주고 있다. 하나는 생 포인터를 넘겨주고, 다른 하나는 unique_ptr를 넘겨준다.

 

릴리즈 모드로 컴파일된 디스어셈블된 코드를 보자.

 

int inc_bazp( foo *p )
{
01331700  push        ebp 
01331701  mov         ebp,esp 
    return p->baz++;
01331703  mov         edx,dword ptr [p] 
01331706  mov         eax,dword ptr [edx+18h] 
01331709  lea         ecx,[eax+1] 
0133170C  mov         dword ptr [edx+18h],ecx 
}
0133170F  pop         ebp 
01331710  ret
 
int inc_baz( std::unique_ptr<foo> &p )
{
00AC16E0  push        ebp 
00AC16E1  mov         ebp,esp 
    return p->baz++;
00AC16E3  mov         eax,dword ptr [p] 
00AC16E6  mov         edx,dword ptr [eax] 
00AC16E8  mov         eax,dword ptr [edx+18h] 
00AC16EB  lea         ecx,[eax+1] 
00AC16EE  mov         dword ptr [edx+18h],ecx 
}
00AC16F1  pop         ebp 
00AC16F2  ret 

 

unique_ptr 버전의 코드가 생 포인터 버전과 비교했을 때 하나의 추가적인 포인터 역참조 코드를 가지고 있다.

 

 

마지막 한 말씀 

 

나는 unique_ptr를 납득시키고 있다. 그래서 당신은 내 코드에서 많은 unique_ptr를 볼 것이다. 그러나, 당신은 좀더 조심스러움을 느낄 수도 있다. 괜찮다. C++11에서는, 당신은 너무 많은 추가 작업 없이 사정을 살필 수가 있다. 단지 포인터를 사용하는 루틴은 모든 포인터를 담기 위해 auto 타입을 사용하라. 이것은 만약 당신이 생 포인터에서 unique_ptr로 변경한다고 해도 소비자 코드에는 변화가 없다는 것을 의미한다. 그리고 이런 포인터를 넘기는 루틴들은 함수 템플릿으로 정의될 수 있는데, 이것은 넘겨지는 데이터 타입이 무엇이든 쉽게 수용될 수 있게 만든다.

 

생 포인터 의미론을 필요로 하는 이전 코드가 없는 완전한 세계에서는, unique_ptr는 자유 저장소로 부터 할당된 데이터가 새지 않는다는 것을 보장할 수 있다. 이것은 C++11 이전 까지는 쉽게 이루어질 수 없었다. 이것을 사용하라!