C, C++

[C, C++] More Effective C++ Item28 : Smart Pointer

Binceline 2014. 3. 30. 10:57

스마트 포인터는 보통의 포인터처럼 생겼고 보통의 포인터처럼 동작하고 보통의 포인터처럼 느껴지지만 사실 엄청난 기능을 제공하도록 설계된 객체이다. 리소스 관리 등에 사용된다.


스마트 포인터를 쓰는 이유는 다음과 같다.

생성과 소멸, 복사와 대입, 역참조 작업을 조절할 수 있기 때문이다.


스마트 포인터는 C++ 기본 제공 포인터처럼 타입이 명확해야 하기 때문에 템플릿 기반으로 만들어 진다.

스마트 포인터를 생성하는 방법은 가리킬 객체를 스마트 포인터의  내부에 있는 더미 포인터로 가리키도록 하는 것이다.


1. 생성, 대입, 소멸

스마트 포인터의 복사 생성자, 대입 연산자, 소멸자를 구현하는 방법은 소유권이라는 것 때문에 복잡하다. 

스마트 포인터는 소멸될 때, 가리키던 객체를 삭제해야 한다. 하지만 이는 그 객체가 동적으로 할당된 경우에만 해당한다. 포인터가 동적 할당된 건지 아닌지는 이전 장에서(Item 27 힙인지 아닌지) 알 수 있다. 


다음 코드를 보자.


template<class T>

class auto_ptr

{

public:

auto_ptr(T* ptr = 0) : pointee(ptr) {}

~auto_ptr() {delete pointee;}


private:

T* pointee;

};


다음과 같이 사용한다 가정하면


auto_ptr<A> p1(new A);

auto_ptr<A> p2 = p1;

auto_ptr<A> p3;

p3 = p2;


내부의 더미 포인터를 단순복사를 한다면 결국 2개의 같은 auto_ptr이 같은 객체를 가리키게 된다. 이렇게 되면 서로 소멸될 때 같은 객체를 해제하려 하므로 문제가 생길 수 있다. new로 복사될 객체의 사본을 만드는 방법도 있지만 엄청난 성능 저하가 생긴다. 그리고 만약 상속 관계에 있는 객체를 등록한다면 문제가 생길 수 있다. 가상 생성자로 해결할 수 있지만 적합하지 않다.

STL의 auto_ptr이 쓰는 방법은 auto_ptr이 복사 혹은 대입될 때 소유권을 바꾸는 방식(더미 포인터가 가리키는 객체로)이다.

그렇기 때문에 auto_ptr을 함수의 매개변수로 넘기면 절대 안된다(의도한 게 아니라면). 왜냐하면 소유권이 옮겨가는데 함수가 끝나면 함수 안의 auto_ptr이 소멸될 때 넘긴 객체가 해제될 것이기 때문이다.  그럼 함수가 끝나면 원래 객체는 메모리 해제되어 존재하지 않으므로 동작을 예측할 수 없게 된다. 그러니깐 함수 인자로 참조자를 사용한다. 그럼 복사 생성자가 호출되지 않는다.

그리고 다음을 보자


template<class T>

class auto_ptr {

public:

...

auto_ptr(auto_ptr<T>& rhs);  // 복사 생성자

auto_ptr<T>& operator=(auto_ptr<T>& rhs); // 대입 연산자

...

};


template<class T>

auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs) {

// pointee의 소유권을 rhs에서 *this로 옮긴다.

pointee = rhs.pointee;

// rhs는 이제 그 객체를 갖지 않는다.

rhs.pointee = 0;

}


template<class T>

auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) {

// 자기 자신에게 대입되는 경우에는 아무 일도 하지 않는다.

if (this == &rhs)

return *this;


// 현재 자신이 가지고 있는 객체는 삭제한다.

delete pointee;


// pointee의 소유권을 rhs에서 *this로 옮긴다.

pointee = rhs.pointee;

rhs.pointee = 0;


return *this;

}


이렇게 소유권을 받을 때 자신이 가지고 있는 객체는 삭제해야 한다. 왜냐하면 이 스마트 포인터가 완전히 바뀌는 것이기 때문에, 소유권을 옮긴다는 건 소멸되고 다시 생성되는 것과 같다. 스마트 포인터가 소멸될 때 객체를 해제해야 하기 때문에 갖고 있던 걸 해제해야 한다. 그렇지 않으면 메모리 누수가 있을 것이다.


스마트 포인터의 소멸자는 보통 이런 식으로 구현되어 있다.


template<class T>

SmartPtr<T>::~SmartPtr()

{

if (*this가 *pointee를 소유하면)

{

delete pointee;

}

}

이렇게 구현하면 된다. 소유권 점검을 정확하게 하기 위해서는 참조 카운팅 기능을 추가해야 한다.


3. 역참조 연산자 구현


template<class T>

SmartPtr<T>::operator* () const

{

// 스마트 포인터 처리를 수행

return *pointee;

}


이 함수에서 가장 중요한 것은

1. 어떻게든 pointee를 유효하게 하는 것이다. (null이 아니게) 그리고 유효하다고 판정되면 스마트 포인터가 가리키는 객체의 참조자를 반환한다. 참조자 대신에 객체를 반환하게 되면 큰 문제가 생긴다. 만약 객체가 상속 관계에 있는 클래스 형이라면, 객체를 반환한다면 그런 동적 정보를 알 수가 없어서 반환 타입을 제대로 맞추지 못할 수가 있다. 

이 말은 곧 가상 함수가 지원되지 않는다는 말과 같다. 값 반환보다 참조자 반환이 훨씬 효율적이다. 임시 객체를 만들지 않고 정확성도 있기 때문이다.

혹시 스마트 포인터가 가리키는 객체가 NULL 일 때는 null을 리턴하든 예외를 일으키든 맘대로 해라


사실 여기까지만 해도 스마트 포인터를 활용할 수 있다.


4. 스마트 포인터가 NULL인지 점검하기.

가능하지 않은 것 중 하나가 이것인데, 


SmartPtr<TreeNode> ptn;

...

if (ptn == 0) ...

if (ptn) ...

if (!ptn) ...


이렇게 되어 있을 때, 모두 에러가 난다. isNull 같은 함수를 만들어서 쓰게 된다면 일본 포인터와 거의 똑같이 동작하도록 의도한 것을 조금 포기해야 하기 때문에 별로 좋지 않다.

암시적 타입 변환 연산자를 넣어 주면 될까?

template<class T>
class SmartPtr {
public:
...
operator void*(); // 스마트 포인터가 널이면 0을 반환하고 아니면 0이 아닌 값을 반환한다.
...
};


SmartPtr<TreeNode> ptn;

...

if (ptn == 0) ...

if (ptn) ...

if (!ptn) ...

이제는 다 된다.


하지만 단점이 있다. 되면 안되는 것이 된다. 다음을 보자


SmartPtr<Apple> pa;

SmartPtr<Orange> po;

...

if (pa == po) ...  // 이게 된다;


완전히 서로 다른 타입의 스마트 포인터의 비교가 가능해진다. 암시적 변환 함수가 위험한 이유는 이런 동작들 때문이다.

이걸 해결하는 방법은 암시적 변환을 빼버리고 operator! 연산자를 오버로딩해서 이 연산자가 호출된 스마트 포인터가 NULL일 때만 true를 반환하는 것이다. 


template<class T>

class SmartPtr {

 public:

  ...

  // 스마트 포인터가 널일 때에만 true를 반환한다.

  bool operator!() const;

  ...

};


이제 이런 게 가능해 진다. 

SmartPtr<TreeNode> ptn;

...

if (!ptn) {

  ...

}

else {

  ...

}


하지만 암시적 변환을 지원하지 않으므로 이건 불가능해진다.

if (ptn == 0) ...

if (ptn) ... 

 

전혀 다른 타입의 스마트 포인터를 비교할 때는 이 경우만 조심하면 된다.

SmartPtr<Apple> pa;

SmartPtr<Orange> po;

...

if (!pa == !po) ... // 안타깝지만, 컴파일됩니다.

이런 짓은 하지 말자. 스마트 포인터끼리의 비교를 해야 할 상황은 전혀 없을 것이다.


스마트 포인터를 완벽하게 일반 포인터와 똑같이 동작하도록 하기는 쉽지 않다. 불가능에 가깝다.

그리고 암시적 타입 변환을 지원하는 것은 별로 좋지 않다. 꼭 그렇게 해야 하는 이유가 없으면, 스마트 포인터를 더미 포인터로 바꾸는 암시적 타입변환 연산자는 제공하지 말자.

스마트 포인터 클래스가 상속 기반의 타입변환에 대해서도 더미 포인터처럼 동작하도록 구현할 수 있을까? 절대 안 된다. 스마트 포인터는 ‘스마트’하지만 진짜 더미 포인터가 될 수는 없다.


반응형