Computer Science/C & C++

[C++] Smart Pointer Template Class | 스마트 포인터 템플릿 클래스

lww7438 2020. 8. 5. 22:05

Smart Pointer Template Class

스마트 포인터 템플릿 클래스



- Smart Pointer는 포인터에 몇 가지 기능이 추가된 클래스 객체를 의미한다.
- 특히, 동적으로 메모리를 할당받은 포인터가 수명을 다했을 때, 자동으로 메모리를 반납해주는 기능을 지원한다.
  (이런 이유로, 스마트 포인터 객체는 Heap 공간 이외의 메모리를 가리키는 포인터를 받아들일 수 없다.)
- 스마트 포인터 클래스는 memory 헤더 파일에 정의되어 있다.
- 스마트 포인터 클래스 이름은 std 이름 공간에 포함되어 있다.

- 스마트 포인터는 아래와 같이 3개의 스마트 템플릿으로 구성된다:

  1. auto_ptr
  2. unique_ptr
  3. shared_ptr

- 위 스마트 포인터 모두 내부적으로 new를 통해 주소를 얻고, delete를 통해 반환하는 구조이다.
- 스마트 포인터 객체의 수명이 다하면, 자동으로 Destructor가 실행되어 delete 문을 통해 메모리를 반환하므로,
  사용자가 직접 메모리를 반납하게 할 필요가 없다. (즉, 일반적인 변수와 동일하게, 신경쓰지 않아도 된다.)
- 스마트 포인터 클래스는 템플릿으로 구현되어 있다.
- 사용자는 사용하고자 하는 데이터형을 템플릿 구문에 명시하여 Instantiation(구체화)한다.


History (스마트 포인터의 탄생 배경)


- 어떤 함수에서 동적으로 메모리를 할당받은 객체가 있다 가정하자.
  이 객체는 함수가 정상적으로 종료될 시에는 정상적으로 메모리를 반환하는데,
  예외가 발생하면 메모리가 반환되지 못한 채, 함수가 종료되어 메모리 누수가 발생하게 되는 구조이다.
- 만약 이 포인터 데이터형이 객체로 되어있었다면, 예외가 발생되어 함수가 종료되어도,
  해당 객체의 Destructor가 호출되어 정상적으로 메모리를 반납할 수 있게 될 것이다.
- 이것이 스마트 포인터의 탄생 배경이다.



스마트 포인터와 일반 포인터형 사이의 Type Casting
- 스마트 포인터 클래스에는 매개변수로 일반 포인터도 받아들이기 위한 explicit Constructor가 구현되어 있다.
- 따라서, 일반 포인터형 변수를 스마트 포인터에 대입하고자 할 때에는 생성자를 이용해야한다:

share_ptr<double> pd;
double* p_reg = new double;

pd = p_reg;                          // Not Allowed Implicit Casting
pd = share_ptr<double> (p_reg);      // Allowed Explicit Casting
share_ptr<double> pshared = p_reg;   // Not Allowed Implicit Casing
share_ptr<double> pshared(p_reg);    // Allowed Explicit Casting




일반 포인터 변수와 달리, 다수의 스마트 포인터 객체는 동시에 한 메모리를 가리킬 수 없다.
- 객체의 수명이 다했을 때, 메모리를 자동으로 반환하기 때문에,
  하나의 스마트 포인터 객체에 하나의 메모리 블럭이 단독으로 배정되어야 한다.

- 이러한 규칙을 구현하기 위한 전략은 아래와 같다:


1. 객체를 복사하도록 대입 연산자를 재정의한다.
- 이렇게 하면, 한 객체가 다른 객체의 복사본이 되어 두 포인터는 서로 다른 객체를 가리키게 된다.

2. 특정 객체를 하나의 스마트 포인터만이 소유할 수 있도록 소유권을 부여한다.
- auto_ptr, unique_ptr이 차용하고 있는 전략이다.
- 스마트 포인터가 해당 객체에 대한 소유권이 있는 경우에만 Destructor가 호출되어 delete 연산을 수행하도록 구현한다.
- 대입 연산이 이루어지면, L-Value에 소유권이 이전된다.

3. Reference Counting(참조 카운팅) 스마트 포인터를 이용한다.
- shared_ptr이 차용하고 있는 전략이다.
- 참조 카운팅 스마트 포인터란, 특정 객체를 참조하는 스마트 포인터의 개수를 파악하고 있는 스마트 포인터이다.
- 대입 연산이 이루어질 때마다, 참조 카운팅 값이 1씩 증가한다.
- 어떤 스마트 포인터의 수명이 다한 경우, 참조 카운팅이 1씩 감소한다.
- 최종적으로, 마지막 스마트 포인터의 수명이 다한 경우에만 Destructor를 호출하여 delete 연산을 수행하도록 한다.


Smart Pointer Syntax (스마트 포인터 사용법)

#include <memory>
#include <string>

std::auto_ptr<double> pd(new double);
// double형을 가리키는 auto_ptr형 변수 pd

std::auto_ptr<std::string> ps(new std::string);
// string형을 가리키는 auto_ptr형 변수 ps




auto_ptr보다 unique_ptr이 안전성 측면에서 더 우수하다.
- 소유권이 이전된 객체에 접근할 때,
  auto_ptr은 Program Crash를 발생시키고,
  unique_ptr은 Compile Error를 발생시키기 때문이다.
- 이런 이유로, auto_ptr보다는 unique_ptr의 사용이 권장된다.


Example. auto_ptr 사용 중, 소유권 이전으로 인해 발생할 수 있는 Program Crash

#include <iostream>
#include <string>
#include <memory>

int main() {
	using namespace std;
	auto_ptr<string> films[5] = {
		auto_ptr<string> (new string("Fowl Balls")),
		auto_ptr<string> (new string("Duck Walks")),
		auto_ptr<string> (new string("Chicken Runs")),
		auto_ptr<string> (new string("Turkey Errors")),
		auto_ptr<string> (new string("Goose Eggs"))
	};
    
	auto_ptr<string> pwin;	// 대신, unique_ptr을 사용한다면 컴파일 단계에서 오류가 발생한다.
	pwin = films[2]; // films[2] loses ownership, pwin gets ownership
    
	cout << "The nominees for best avian baseball film are\n";
	for (int i = 0; i < 5; i++)
	cout << *films[i] << endl;
	cout << "The winner is " << *pwin << "!\n";
	cin.get();
	return 0;
}

// Output on Console
The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Segmentation fault (core dumped)	// *films[2]의 출력 (소유권을 잃었으므로, 객체를 가리키고 있지 않다.)
									// shared_ptr을 사용하면 해결되는 오류이다.




소유권이 이전된 스마트 포인터가 계속해서 잔재한다면 유효하지 않은 값에 접근할 위험이 높아진다.
- 아래의 방법처럼, 유효하지 않은 스마트 포인터가 수명을 다하게끔 유도하는 방법도 있다:

unique_ptr<std::string> demo(const char* s) {
    unique_ptr<std::string> temp(new string(s));
    return temp;
}

...

unique_ptr<std::string> ps;
ps = demo("Uniquely special");    // demo() 함수는 임시 객체를 리턴한다.
				  // 임시 객체는 ps에 소유권을 넘긴 뒤, 수명을 다한다.
                                  // 즉, 유효하지 않은 값에 접근할 여지를 없앤다.




unique_ptr 객체는 다른 객체에 대입될 때, 원본 객체가 임시 R-Value인 경우에는 대입이 허용되나,
원본 객체가 이미 포인팅을 하고 있는 경우에는 대입이 허용되지 않는다.
- 하지만, move() 메서드를 이용하여 unique_ptr 객체를 대입하는 것이 가능하긴 하다.

using namespace std;
unique_ptr< string> pu1(new string "Hi ho!");
unique_ptr< string> pu2;

pu2 = pu1;    		 // not allowed (pu1이 무효한 값을 가진채로 남게되기 때문에 허용되지 않는다.)
pu2 = move(ps1); 	 // allowed (move() 함수를 통한 대입은 허용된다.)
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string "Yo!");    //allowed (대입되는 임시 무명 객체는 곧바로 소멸될 것이기 때문에 허용된다.)




unique_ptr은 array도 취급할 수 있는데 반해, auto_ptrshared_ptr은 array를 취급할 수 없다.

std::unique_ptr<double[]> pda(new double[5]);




하나의 객체에 대해 두 개 이상의 스마트 포인터를 사용해야 하는 경우, shared_ptr이 적합하다.
- 포인터의 STL 컨테이너를 포함하는 프로그램, 복사/대입 연산을 포함하는 많은 STL 알고리즘에는 shared_ptr이 적합하다.
- 컴파일러가 shared_ptr을 지원하지 않을 경우, Boost Library에서 shared_ptr을 추가시킬 수 있다.

 


Reference: Stephen Prata, C++ Primer Plus 6th Edition, Pearson, 2011