[Modern C++] 13-1. unique_ptr

윤정민·2023년 7월 28일
0

C++

목록 보기
33/46

1. 자원 관리의 중요성

  • c++이후에 나온 많은 언어들은 대부분 가비지 콜렉터라 불리는 자원 청소기가 기본적으로 내장되어 있음
    • GC: 프로그램 상에서 더 이상 쓰지 않는 자원을 자동으로 해제해 주는 역할
  • c++에서 한 번 획득한 자원은 직접 해제해주지 않는 이상 프로그램이 종료되기 전 까지 영원히 남아 있음
  • 예시 코드
    • do_something에서 소멸자를 호출하지 않아 생성된 객체를 가리키던pa는 메모리에서 사라지게 됨
    • 따라서 Heap어딘가에 클래스 A의 객체가 남아있지만, 그 주소 값을 가지고 있는 포인터는 메모리 상에 존재하지 않게 됨
    • 이 객체는 영원히 해제되지 못한 채 힙에서 자리만 차지하게 되어 메모리 누수가 발생
#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) 만큼의 메모리 누수 발생
}

2. RAII(Resource Acquisition Is Initializaton)

  • 위와 같은 문제를 예방하기 위해 c++에서 자원을 관리하는 방법으로 다음과 같은 디자인 패턴을 제시함
  • 자원 관리를 스택에 할당한 객체를 통해 수행하는 것
    • 예외가 발생해 함수를 빠져나가더라도, 그 함수의 스택에 정의되어 있는 모든 객체들은 빠짐없이 소멸자가 호출되니, 이 소멸자들 안에 다 사용한 자원을 해제하는 루틴을 넣어 자원관리를 할 수 있음
    • 예를 들어 위의 경우 pa는 객체가 아니기 때문에 소멸자가 호출되지 않음, 그렇다면 그 대신 pa를 일반적인 포인터가 아닌 포인터 객체로 만들어서 자신이 소멸될 때 자신이 가리키고 있는 데이터도 같이 delete시키는 것.smart pointer

3. unique_ptr

  • 특정 객체에 유일한 소유권을 부여하는 포인터 객체
    • 만약 소유권이 유일하지 않다면 동일한 객체를 두 포인터가 delete시키는 double free 버그가 발생가능
  • unique_ptr은 복사 생성자가 명시적으로 삭제되었기 때문에 복사 생성자를 사용할 수 없음
    A(const A& a) = delete;
  • 예시 코드
#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(); }
  • 실행 결과
자원을 획득함!
일반 포인터와 동일하게 사용가능!
자원을 해제함!

3.1. unique_ptr 소유권 이전하기

  • 복사 생성자는 정의되어 있지 않지만, 이동 생성자는 가능
    • 소유권을 이동시킬 수 있기 때문
  • 예시 코드
    • 소유권 이전
      std::unique_ptr<A> pb = std::move(pa);
    • pa가 가리키는 객체 확인: nullptr
      pa.get();

3.2. unique_ptr을 함수인자로 전달하기

  • unique_ptr은 그냥 함수에 레퍼런스 전달하듯이 사용하면 소유권이라는 의미를 망각한 채 단순히 포인터의 wrapper로 사용하는 것에 불과하게 됨

  • 따라서 레퍼런스로 사용하기 위해 원래 포인터의 주소값을 전달해주자

    void do_something(A* ptr) { ptr->do_sth(3); }
    
    int main() {
      std::unique_ptr<A> pa(new A());
      do_something(pa.get());
    }

3.3. unique_ptr 쉽게 생성하기

  • c++14부터 unique_ptr을 간단히 만들 수 있는 std::make_unique함수를 제공
    • 템플릿 인자로 전달된 클래스의 생성자에 인자들에 직접 perfect forwarding을 수행
  • 예제 코드
#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
소멸자 호출!

3.4. unique_ptr을 원소로 가지는 컨테이너

  • unique_ptr은 복사생성자가 없기 때문에 vector 컨테이너를 사용할 때 push_back 함수를 사용한다면 컴파일 에러가 발생
  • 따라서 명시적으로 vector안으로 이동시켜 주어야 함
int main() {
  std::vector<std::unique_ptr<A>> vec;
  std::unique_ptr<A> pa(new A(1));

  vec.push_back(std::move(pa));  // push_back의 우측값 레퍼런스를 받는 버전이 오버로딩되어 실행됨
}
profile
그냥 하자

0개의 댓글