모두의 코드
씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr>

작성일 : 2018-09-18 이 글은 57630 번 읽혔습니다.

이번 강좌에서는

  • C++ 의 RAII 패턴

  • unique_ptr

안녕하세요 여러분! 지난번 강좌에서 다루었던 move semanticsperfect forwarding 은 이해가 잘 되셨나요? C++ 에서 이와 같이 우측값들을 직접 다룰 수 있는 툴들을 많이 추가해준 덕분에 좀더 세심한 최적화와, 기존에 불가능 하였던 많은 일들이 가능하게 되었습니다.

이번 강좌에서는 C++ 11 에서 자원을 관리하는 방법에 대해서 다루도록 할 것입니다. 컴퓨터에서 자원 (resource) 라 하면 여러 가지를 꼽을 수 있지만 예를 들어보자면 여러분이 할당한 메모리도 자원이고, open 한 파일 역시 하나의 자원이라고 볼 수 있습니다.

중요한 점은, 자원의 양은 프로그램 마다 한정되어 있기 때문에 관리를 잘 해주어야 합니다. 이말은 즉슨, 사용이 끝난 자원은 반드시 반환을 해서 다른 작업 때 사용할 수 있도록 해야 합니다. 예를 들어서 메모리를 할당만 하고 해제를 하지 않는다면, 결국 메모리 부족으로 프로그램이 crash 될 수 도 있습니다.

자원(resource) 관리의 중요성

C++ 이후에 나온 많은 언어 (Java 등등) 들은 대부분은 가비지 컬렉터 (Garbage Collector - GC) 라 불리는 자원 청소기가 기본적으로 내장되어 있습니다. 이 가비지 컬렉터의 역할은 프로그램 상에서 더 이상 쓰이지 않는 자원을 자동으로 해제해 주는 역할을 합니다. 따라서 프로그래머들이 코드를 작성할 때, 자원을 해제하는 일에 대해 크게 신경 쓸 필요는 없습니다.

하지만 C++ 의 경우는 다릅니다. 여러분이 한 번 획득한 자원은, 직접 해제해주지 않는 이상 프로그램이 종료되기 전 까지 영원히 남아있게 됩니다. (프로그램이 종료되면 운영체제가 알아서 해제해줍니다.) 예를 들어서;

#include <iostream>

class A {
  int *data;

 public:
  A() {
    data = new int[100];
    std::cout << "자원을 획득함!" << std::endl;
  }

  ~A() {
    std::cout << "소멸자 호출!" << std::endl;
    delete[] data;
  }
};

void do_something() { A *pa = new A(); }

int main() {
  do_something();

  // 할당된 객체가 소멸되지 않음!
  // 즉, 400 바이트 (4 * 100) 만큼의 메모리 누수 발생
}

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!

와 같이 나옵니다.

즉 자원을 획득만 하고, 소멸자는 호출되지 않은 점을 확인할 수 있습니다. 그 이유는 까먹고

delete pa;

를 해주지 않았기 때문이지요 (아시다싶이 delete 는 메모리를 해제하기 직전 가리키는 객체의 소멸자를 호출해줍니다).

만약에 deletedo_something 함수 안에서 해주지 않는다면, 생성된 객체를 가리키던 pa 는 메모리에서 사라지게 됩니다. 따라서 Heap 어딘가에 클래스 A 의 객체가 남아있지만, 그 주소값을 가지고 있는 포인터는 메모리 상에 존재하지 않게 됩니다. 그 객체는 영원히 해제되지 못한 채 힙에서 자리만 차지하고 있게 됩니다. 위 경우 400 바이트의 메모리 누수가 발생하게 되겠네요.

언뜻 생각하기에 아 그럼 항상 잊지 말고 자원 해제를 꼭 해주면 되잖아! 라고 하실 분들이 계실 것입니다. 하지만 프로그램의 크기가 커지면, 자원을 해제하는 위치가 애매한 경우가 많아서 놓치기 십상입니다. 물론 그래도 그건 프로그래머 책임 아님? 이라고 반문 하실 수 있습니다.

그런데, 다음과 같은 상황을 생각해보세요

#include <iostream>

class A {
  int *data;

 public:
  A() {
    data = new int[100];
    std::cout << "자원을 획득함!" << std::endl;
  }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

void thrower() {
  // 예외를 발생시킴!
  throw 1;
}

void do_something() {
  A *pa = new A();
  thrower();

  // 발생된 예외로 인해 delete pa 가 호출되지 않는다!
  delete pa;
}

int main() {
  try {
    do_something();
  } catch (int i) {
    std::cout << "예외 발생!" << std::endl;
    // 예외 처리
  }
}

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
예외 발생!

와 같이 나옵니다.

보시다 싶이, thrower() 로 에서 발생된 예외로 인해, 밑에 있는 delete pa 가 실행 되지 않고 넘어가버렸습니다. 물론 예외는 정상적으로 처리되었지만, 이로 인해 메모리 누수는 피할 수 없게 됩니다.

그렇다면 이 상황을 어떻게 해결할까요?

Resource Acquisition Is Initialization - RAII

C++ 창시자인 비야네 스트로스트룹은 C++ 에서 자원을 관리하는 방법으로 다음과 같은 디자인 패턴을 제안하였습니다. 바로 흔히 RAII 라 불리는 자원의 획득은 초기화다 - *Resource Acquisition Is Initialization* 입니다. 이는 자원 관리를 스택에 할당한 객체를 통해 수행하는 것입니다.

지난 강좌에서 예외가 발생해서 함수를 빠져나가더라도, 그 함수의 스택에 정의되어 있는 모든 객체들은 빠짐없이 소멸자가 호출된다고 하였습니다 (이를 stack unwinding 이라 한다고 했었죠). 물론 예외가 발생하지 않는다면, 함수가 종료될 때 당연히 소멸자들이 호출되지요.

그렇다면 생각을 조금 바꿔서 만약에 이 소멸자들 안에 다 사용한 자원을 해제하는 루틴을 넣으면 어떨까요?

예를 들어서 위 포인터 pa 의 경우 객체가 아니기 때문에 소멸자가 호출되지 않습니다. 그렇다면 그 대신에, pa 를 일반적인 포인터가 아닌, 포인터 '객체' 로 만들어서 자신이 소멸 될 때 자신이 가리키고 있는 데이터도 같이 delete 하게 하면 됩니다. 즉, 자원 (이 경우 메모리) 관리를 스택의 객체 (포인터 객체) 를 통해 수행하게 되는 것입니다.

이렇게 똑똑하게 작동하는 포인터 객체를 스마트 포인터(smart pointer)라고 부릅니다. C++ 11 이전에, 이러한 문제를 해결하기 위해 auto_ptr 란게 잠시 등장했었지만, 너무나 많은 문제들이 있었기에 사용을 권장하지 않습니다. (아니, 아예 사용을 금지한다고 보는 것이 맞습니다.)

C++ 11 에서는 auto_ptr 를 보완한 두 가지 형태의 새로운 스마트 포인터를 제공하고 있습니다. 바로 unique_ptrshared_ptr 입니다.

객체의 유일한 소유권 - unique_ptr

C++ 에서 메모리를 잘못된 방식으로 관리하였을 때 크게 두 가지 종류의 문제점이 발생할 수 있습니다.

첫 번째로 앞서도 이야기한 메모리를 사용한 후에 해제하지 않은 경우입니다 (이를 메모리 누수(memory leak) 이라고 부릅니다) 간단한 프로그램의 경우 크게 문제될 일이 없지만, 서버 처럼 장시간 작동하는 프로그램의 경우 시간이 지남에 따라 점점 사용하는 메모리 양의 늘어나서 결과적으로 나중에 시스템 메모리가 부족해져서 서버가 죽어버리는 상황이 발생할 수 도 있습니다.

다행이도 위 문제는 위에서 이야기한 RAII 패턴을 사용하면 해결 할 수 있습니다. RAII 를 통해서 사용이 끝난 메모리는 항상 해제시켜 버리면 메모리 누수 문제를 사전에 막을 수 있습니다.

두 번째로 발생 가능한 문제는, 이미 해제된 메모리를 다시 참조하는 경우 입니다. 예를 들어서

Data* data = new Data();
Date* data2 = data;

// data 의 입장 : 사용 다 했으니 소멸시켜야지.
delete data;

// ...

// data2 의 입장 : 나도 사용 다 했으니 소멸시켜야지
delete data2;

위 경우 datadata2 가 동시에 한 객체를 가리키고 있는데, delete data 를 통해 그 객체를 소멸시켜주었습니다. 그런데, data2 가 이미 소멸된 객체를 다시 소멸시키려고 합니다. 보통 이럴 경우 메모리 오류가 나면서 프로그램이 죽게 됩니다. 이렇게 이미 소멸된 객체를 다시 소멸시켜서 발생하는 버그를 double free 버그라고 부릅니다.

위와 같은 문제가 발생한 이유는 만들어진 객체의 소유권이 명확하지 않아서 입니다. 만약에, 우리가 어떤 포인터에 객체의 유일한 소유권을 부여해서, 이 포인터 말고는 객체를 소멸시킬 수 없다! 라고 한다면, 위와 같이 같은 객체를 두 번 소멸시켜버리는 일은 없을 것입니다.

위 경우 datanew Data() 로 생성된 객체의 소유권을 보유한다면, delete data 만 가능하고, delete data2 는 불가능 하게 됩니다.

C++ 에서는 이렇게, 특정 객체에 유일한 소유권을 부여하는 포인터 객체를 unique_ptr 라고 합니다.

#include <iostream>
#include <memory>

class A {
  int *data;

 public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

void do_something() {
  std::unique_ptr<A> pa(new A());
  pa->some();
}

int main() { do_something(); }

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
일반 포인터와 동일하게 사용가능!
자원을 해제함!

와 같이 나옵니다.

std::unique_ptr<A> pa(new A());

먼저 unique_ptr 을 정의하는 부분 부터 살펴봅시다. unique_ptr 를 정의하기 위해서는 템플릿에 인자로, 포인터가 가리킬 클래스를 전달하면 됩니다. 위 경우 paA 클래스의 객체를 가리키는 포인터가 되겠지요. 위는 마치

A* pa = new A();

와 동일한 문장이라 생각하시면 됩니다.

pa->some();

그렇다면 이제 위와 같이 pa 가 포인터인것처럼 사용하시면 됩니다. unique_ptr-> 연산자를 오버로드해서 마치 포인터를 다루는 것과 같이 사용할 수 있게 하였습니다.

또한 이 unique_ptr 덕분에 RAII 패턴을 사용할 수 있습니다. pa 는 스택에 정의된 객체이기 때문에, do_something() 함수가 종료될 때 자동으로 소멸자가 호출됩니다. 그리고 이 unique_ptr 는 소멸자 안에서 자신이 가리키고 있는 자원을 해제해 주기 때문에, 자원이 잘 해제될 수 있었습니다.

만약에 unique_ptr 를 복사하려고 한다면 어떨까요?

#include <iostream>
#include <memory>

class A {
  int *data;

 public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

void do_something() {
  std::unique_ptr<A> pa(new A());

  // pb 도 객체를 가리키게 할 수 있을까?
  std::unique_ptr<A> pb = pa;
}

int main() { do_something(); }

컴파일 하였다면

컴파일 오류

'std::unique_ptr<A,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)': attempting to reference a deleted function

위와 같은 오류가 나오게 됩니다. 위 오류는, 삭제된 함수를 사용하려고 했다는 뜻인데, 삭제된 함수가 도대체 무슨 말일까요?

삭제된 함수

사용을 원치 않는 함수를 삭제시키는 방법은 C++ 11 에 추가된 기능 입니다. 아래와 같은 코드를 살펴봅시다.

#include <iostream>


class A {
 public:
  A(int a){};
  A(const A& a) = delete;
};

int main() {
  A a(3);  // 가능
  A b(a);  // 불가능 (복사 생성자는 삭제됨)
}

컴파일 하게 된다면 복사 생성자를 호출하는 부분에서 오류가 발생함을 알 수 있습니다. 왜냐하면,

A(const A& a) = delete;

위와 같이 복사 생성자를 명시적으로 삭제하였기 때문이지요. 따라서, 클래스 A 의 복사 생성자는 존재하지 않습니다. 위와 같이 = delete; 를 사용하게 되면, 프로그래머가 명시적으로 '이 함수는 쓰지 마!' 라고 표현할 수 있습니다. 혹시나 사용하더라도 컴파일 오류가 발생하게 됩니다.

unique_ptr 도 마찬가지로 unique_ptr 의 복사 생성자가 명시적으로 삭제되었습니다. 그 이유는 unique_ptr 는 어떠한 객체를 유일하게 소유해야 하기 때문이지요. 만일 unique_ptr 를 복사 생성할 수 있게 된다면, 특정 객체를 여러 개의 unique_ptr 들이 소유하게 되는 문제가 발생합니다. 따라서, 각각의 unique_ptr 들이 소멸될 때 전부 객체를 delete 하려 해서 앞서 말한 double free 버그가 발생하게 됩니다.

unique_ptr 소유권 이전하기

앞서 unique_ptr 는 복사가 되지 않는다고 하였지만, 소유권은 이전할 수 있습니다.

#include <iostream>
#include <memory>

class A {
  int *data;

 public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

void do_something() {
  std::unique_ptr<A> pa(new A());
  std::cout << "pa : ";
  pa->some();

  // pb 에 소유권을 이전.
  std::unique_ptr<A> pb = std::move(pa);
  std::cout << "pb : ";
  pb->some();
}

int main() { do_something(); }

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
pa : 일반 포인터와 동일하게 사용가능!
pb : 일반 포인터와 동일하게 사용가능!
자원을 해제함!

와 같이 나옵니다.

unique_ptr 은 복사 생성자는 정의되어 있지 않지만, 이동 생성자는 가능 합니다. 왜냐하면, 마치 소유권을 이동시킨다 라는 개념으로 생각하면 되기 때문이지요.

std::unique_ptr<A> pb = std::move(pa);

에서 위와 같이 papb 에 강제로 이동시켜버립니다. (여기서 퀴즈! std::move 가 왜 필요할까요?) 이제 pbnew A 로 생성된 객체의 소유권을 갖게 되고, pa 는 아무 것도 가리키고 있지 않게 됩니다. 실제로,

pa.get()

으로 pa 가 가리키고 있는 실제 주소값을 확인해보면 0 (nullptr) 이 나옵니다. 따라서 pa 를 이동시켜버린 이후에 pa->some() 을 하게 되면 문제가 발생하게 되겠지요!

따라서 소유권을 이동 시킨 이후에 기존의 unique_ptr 을 접근하지 않도록 조심해야 합니다.

주의 사항

소유권이 이전된 unique_ptr댕글링 포인터(dangling pointer) 라고 하며 이를 재 참조할 시에 런타임 오류가 발생하도록 합니다. 따라서 소유권 이전은, 댕글링 포인터를 절대 다시 참조 하지 않겠다는 확신 하에 이동해야 합니다.

unique_ptr 를 함수 인자로 전달하기

만약에 어떠한 unique_ptr 를 함수 인자로 전달하고 싶다면 어떨까요? 앞서 말했듯이, unique_ptr 는 복사 생성자가 없다고 하였습니다. 그렇다면, 그냥 함수에 레퍼런스에 전달하면 어떨까요?

#include <iostream>
#include <memory>

class A {
  int* data;

 public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  void do_sth(int a) {
    std::cout << "무언가를 한다!" << std::endl;
    data[0] = a;
  }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

// 올바르지 않은 전달 방식
void do_something(std::unique_ptr<A>& ptr) { ptr->do_sth(3); }

int main() {
  std::unique_ptr<A> pa(new A());
  do_something(pa);
}

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
무언가를 한다!
자원을 해제함!

와 같이 나옵니다.

일단, 함수 내부로 unique_ptr 가 잘 전달 되었음을 알 수 있습니다. 하지만, 위와 같이 함수에 unique_ptr 을 전달하는 것이 문맥 상 맞는 코드 일까요?

앞서 말했듯이 unique_ptr 은 어떠한 객체의 소유권 을 의미한다고 말했습니다. 하지만, 위와 같이 레퍼런스로 unique_ptr 을 전달했다면, do_something 함수 내부에서 ptr 은 더이상 유일한 소유권을 의미하지 않습니다.

물론 ptr 은 레퍼런스 이기 때문에, do_something 함수가 종료되면서 pa 가 가리키고 있는 객체를 파괴하지는 않습니다. 하지만, pa유일하게 소유하고 있던 객체는 이제, 적어도 do_something 함수 내부에서는 ptr 을 통해서도 소유할 수 있게 된다는 것입니다. 즉, unique_ptr 은 소유권을 의미한다는 원칙에 위배되는 것이지요.

따라서, unique_ptr 의 레퍼런스를 사용하는 것은 unique_ptr 를 소유권 이라는 중요한 의미를 망각한 채 단순히 포인터의 단순한 Wrapper 로 사용하는 것에 불과합니다.

그렇다면, 함수에 올바르게 unique_ptr 를 전달하는 방법이 있을까요? 이는 단순합니다. 그냥 원래의 포인터 주소값을 전달해주면 됩니다.

#include <iostream>
#include <memory>

class A {
  int* data;

 public:
  A() {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  void do_sth(int a) {
    std::cout << "무언가를 한다!" << std::endl;
    data[0] = a;
  }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

void do_something(A* ptr) { ptr->do_sth(3); }

int main() {
  std::unique_ptr<A> pa(new A());
  do_something(pa.get());
}

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
무언가를 한다!
자원을 해제함!

와 같이 나옵니다.

unique_ptrget 함수를 호출하면, 실제 객체의 주소값을 리턴해줍니다. 위 경우 do_something 함수가 일반적인 포인터를 받고 있습니다. 이렇게 된다면, 소유권이라는 의미는 버린 채, do_something 함수 내부에서 객체에 접근할 수 있는 권한을 주는 것입니다.

정리해보자면,

  • unique_ptr 은 어떤 객체의 유일한 소유권을 나타내는 포인터 이며, unique_ptr 가 소멸될 때, 가리키던 객체 역시 소멸된다.

  • 만약에 다른 함수에서 unique_ptr 가 소유한 객체에 일시적으로 접근하고 싶다면, get 을 통해 해당 객체의 포인터를 전달하면 된다.

  • 만약에 소유권을 이동하고자 한다면, unique_ptrmove 하면 된다.

unique_ptr 을 쉽게 생성하기

C++ 14 부터 unique_ptr 을 간단히 만들 수 있는 std::make_unique 함수를 제공합니다.

#include <iostream>
#include <memory>

class Foo {
  int a, b;

 public:
  Foo(int a, int b) : a(a), b(b) { std::cout << "생성자 호출!" << std::endl; }
  void print() { std::cout << "a : " << a << ", b : " << b << std::endl; }
  ~Foo() { std::cout << "소멸자 호출!" << std::endl; }
};

int main() {
  auto ptr = std::make_unique<Foo>(3, 5);
  ptr->print();
}

성공적으로 컴파일 하였다면

실행 결과

생성자 호출!
a : 3, b : 5
소멸자 호출!

와 같이 잘 작동함을 알 수 있습니다. make_unique 함수는 아예 템플릿 인자로 전달된 클래스의 생성자에 인자들에 직접 완벽한 전달 을 수행합니다. 따라서 기존 처럼 불필요하게

std::unique_ptr<Foo> ptr(new Foo(3, 5));

할 필요 없이 간단히 make_unique 로 만들 수 있습니다.

unique_ptr 를 원소로 가지는 컨테이너

자 이제 마지막으로, unique_ptr 를 원소로 가지는 STL 컨테이너에 대해 알아보도록 합시다. 사실, unique_ptr 은 다른 타입들과 큰 차이는 없지만, 복사 생성자가 없다 라는 특성 때문에 처음에 사용하시는 분들이 많은 애를 먹는 경우를 종종 보았습니다.

#include <iostream>
#include <memory>
#include <vector>

class A {
  int *data;

 public:
  A(int i) {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
    data[0] = i;
  }

  void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

int main() {
  std::vector<std::unique_ptr<A>> vec;
  std::unique_ptr<A> pa(new A(1));

  vec.push_back(pa);  // ??
}

컴파일 하였다면 아래와 같은 무시무시한 컴파일 오류를 맛보게 될 것입니다.

컴파일 오류

In file included from /usr/include/x86_64-linux-gnu/c++/7/bits/c++allocator.h:33:0,
                 from /usr/include/c++/7/bits/allocator.h:46,
                 from /usr/include/c++/7/string:41,
                 from /usr/include/c++/7/bits/locale_classes.h:40,
                 from /usr/include/c++/7/bits/ios_base.h:41,
                 from /usr/include/c++/7/ios:42,
                 from /usr/include/c++/7/ostream:38,
                 from /usr/include/c++/7/iostream:39,
                 from 13.1.7.cc:1:
/usr/include/c++/7/ext/new_allocator.h: In instantiation of 'void __gnu_cxx::new_allocator<_Tp>::construct(_Up*, _Args&& ...) [with _Up = std::unique_ptr<A>; _Args = {const std::unique_ptr<A, std::default_delete<A> >&}; _Tp = std::unique_ptr<A>]':
/usr/include/c++/7/bits/alloc_traits.h:475:4:   required from 'static void std::allocator_traits<std::allocator<_CharT> >::construct(std::allocator_traits<std::allocator<_CharT> >::allocator_type&, _Up*, _Args&& ...) [with _Up = std::unique_ptr<A>; _Args = {const std::unique_ptr<A, std::default_delete<A> >&}; _Tp = std::unique_ptr<A>; std::allocator_traits<std::allocator<_CharT> >::allocator_type = std::allocator<std::unique_ptr<A> >]'
/usr/include/c++/7/bits/stl_vector.h:943:30:   required from 'void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::unique_ptr<A>; _Alloc = std::allocator<std::unique_ptr<A> >; std::vector<_Tp, _Alloc>::value_type = std::unique_ptr<A>]'
13.1.7.cc:32:19:   required from here
/usr/include/c++/7/ext/new_allocator.h:136:4: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = A; _Dp = std::default_delete<A>]'
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/c++/7/memory:80:0,
                 from 13.1.7.cc:2:
/usr/include/c++/7/bits/unique_ptr.h:388:7: note: declared here
       unique_ptr(const unique_ptr&) = delete;
       ^~~~~

이와 같은 오류가 발생하는 이유는 당연합니다. 역시, 삭제된 unique_ptr 의 복사 생성자에 접근하였기 때문이지요. 기본적으로 vectorpush_back 함수는 전달된 인자를 복사해서 집어 넣기 때문에 위와 같은 문제가 발생하게 되는 것이지요.

이를 방지하기 위해서는 명시적으로 pavector 안으로 이동 시켜주어야만 합니다. 즉 push_back 의 우측값 레퍼런스를 받는 버전이 오버로딩 될 수 있도록 말이지요.

int main() {
  std::vector<std::unique_ptr<A>> vec;
  std::unique_ptr<A> pa(new A(1));

  vec.push_back(std::move(pa));  // 잘 실행됨
}

와 같이 하면 잘 컴파일 됩니다.

하지만 재미있게도, emplace_back 함수를 이용하면, vector 안에 unique_ptr 을 직접 생성 하면서 집어넣을 수 도 있습니다. 즉, 불필요한 이동 과정을 생략할 수 있다는 것입니다.

#include <iostream>
#include <memory>
#include <vector>

class A {
  int *data;

 public:
  A(int i) {
    std::cout << "자원을 획득함!" << std::endl;
    data = new int[100];
    data[0] = i;
  }

  void some() { std::cout << "값 : " << data[0] << std::endl; }

  ~A() {
    std::cout << "자원을 해제함!" << std::endl;
    delete[] data;
  }
};

int main() {
  std::vector<std::unique_ptr<A>> vec;

  // vec.push_back(std::unique_ptr<A>(new A(1))); 과 동일
  vec.emplace_back(new A(1));

  vec.back()->some();
}

성공적으로 컴파일 하였다면

실행 결과

자원을 획득함!
값 : 1
자원을 해제함!

와 같이 나옵니다.

emplace_back 함수는 전달된 인자를 완벽한 전달(perfect forwarding) 을 통해, 직접 unique_ptr<A> 의 생성자에 전달 해서, vector 맨 뒤에 unique_ptr<A> 객체를 생성해버리게 됩니다. 따라서, 위에서 처럼 불필요한 이동 연산이 필요 없게 됩니다 (왜냐하면 vector 맨 뒤에 생성하기 때문에!)

참고로 emplace_back 을 사용 시에 어떠한 생성자가 호출되는지 주의 하셔야 합니다. 예를 들어서

std::vector<int> v;
v.emplace_back(100000);

을 하게 되면, 100000 이란 int 값을 v 에 추가하게 되지만

std::vector<std::vector<int>> v;
v.emplace_back(100000);

를 하게 되면 원소가 100000 개 들어있는 벡터를 v 에 추가하게 됩니다.

생각 해보기

문제 1

unique_ptr 을 어떤식으로 구현할 수 있을지 생각해보세요 (난이도 : 중상)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 43 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요

    댓글을 불러오는 중입니다..