본문 바로가기
프로그래밍/C & C++

[C++] 메모리 관리 (3) - 스마트 포인터 (Smart Pointer)

by 별준 2022. 2. 8.

References

Contents

  • 스마트 포인터 (Smart Pointer)
  • unique_ptr, shared_ptr, weak_ptr

[C++] 메모리 관리 (1) - 동적 메모리, 배열과 포인터

[C++] 메모리 관리 (2) - 메모리 문제 유형과 해결 방법

이전 포스팅 마지막 부분에서 스마트 포인터인 unque_ptr을 사용해서 메모리 누수를 방지하는 방법을 간단히 보여주었습니다. 지난 두 포스팅에 이어서 이번에는 메모리를 쉽게 관리해주는 C++의 스마트 포인터에 대해서 알아보도록 하겠습니다.

 


5. 스마트 포인터 Smart Pointer

방금까지 살펴봤듯이 C++에서 메모리 관리는 에러와 버그의 원인입니다. 메모리 관리와 관련된 버그 중 상당수는 동적 메모리 할당과 포인터에서 발생합니다. 프로그램에서 메모리를 동적으로 할당하는 일이 많아지고 객체끼리 수많은 포인터를 주고받다 보면 각 포인터에 대해 정확한 시점에 delete를 단 한 번만 호출하지 못하는 실수가 발생하기 쉽습니다. 여기서 실수를 한다면 심각한 문제가 발생합니다. 동적으로 할당한 메모리를 여러 번 해제하면 메모리 상태가 손상되거나 치명적인 런타임 에러가 발생합니다. 또한 동적으로 할당한 메모리를 깜박 잊고 해제하지 않으면 메모리 누수 현상이 발생합니다.

 

스마트 포인터를 사용하면 동적으로 동적으로 할당한 메모리를 관리하기 쉽습니다. 특히 메모리 누수를 방지하려면 스마트 포인터를 적극 활용하는 것이 좋습니다. 기본적으로 스마트 포인터는 메모리뿐만 아니라 동적으로 할당한 모든 리소스를 가리킵니다. 스마트 포인터가 스코프를 벗어나거나 리셋되면 거기에 할당된 리소스가 자동으로 해제됩니다. 스마트 포인터는 함수 스코프 내에서 동적으로 할당된 리소스를 관리하는 데 사용될 수도 있고, 클래스의 데이터 멤버로 사용할 수도 있습니다. 또한, 동적으로 할당된 리소스의 소유권을 함수의 인수로 넘겨줄 때도 스마트 포인터를 활용합니다.

 

C++은 스마트 포인터를 지원하는 기능을 언어 차원에서 다양하게 제공합니다. 첫째, 템플릿을 이용하면 모든 포인터 타입에 대해 타입 세이프한 스마트 포인터 클래스를 작성할 수 있습니다. 둘째, 연산자 오버로딩을 이용하여 스마트 포인터 객체에 대한 인터페이스를 제공해서 스마트 포인터 객체를 일반 포인터처럼 활용할 수 있습니다. 특히 *와 ->, [] 연산자를 오버로딩하면 스마트 포인터 객체를 일반 포인터처럼 역참조할 수 있습니다.

 

스마트 포인터의 종류는 다양합니다. 가장 간단한 것은 리소스에 대한 유일한 소유권(unique ownership)을 받는 것입니다. 유일한 소유권을 받음으로써, 스마트 포인터가 스코프를 벗어나거나 리셋되면 참조하던 리소스를 해제합니다. 표준 라이브러리에서 제공하는 std::unique_ptr이 바로 이러한 단독 소유권 방식을 지원합니다.

 

조금 더 고급 형태의 스마트 포인터는 공유 소유권(shared ownership)을 허용합니다. 이러한 스마트 포인터가 스코프를 벗어나거나 리셋되면 참조된 리소스가 해당 리소스를 참조하는 마지막 스마트 포인터일 경우에만 해제합니다. 표준 라이브러리에서는 공유 소유권을 지원하는 std::shared_ptr을 제공합니다.

 

std::unique_ptr과 std::shared_ptr은 <memory> 헤더에 정의되어 있습니다.

 

5.1 unique_ptr

unique_ptr은 리소스에 대해 단독 소유권을 가집니다. unique_ptr이 파괴되거나 리셋될 때, 리소스는 자동으로 해제됩니다. 한 가지 장점은 메모리와 리소스가 항상 해제된다는 것인데, 심지어 return문이 수행되거나 예외가 발생할 때도 해제됩니다. 이는 각 return 문이 수행되기 전에 리소스를 해제해 줄 필요가 없기 때문에 코딩이 간단해집니다.

 

5.1.1 unique_ptr 생성

다음과 같이 Simple 객체를 free store에 할당한 뒤 이를 해제하지 않고 함수가 끝나서 메모리 누수 현상이 발생하는 경우를 살펴보겠습니다.

void leaky()
{
  Simple* mySimplePtr { new Simple() };
  mySimplePtr->go();
}

코드를 작성할 때마다 항상 동적으로 할당한 메모리를 제대로 해제한다고 여기기 쉽지만, 그렇지 않을 가능성이 더 높습니다.

다음의 함수를 살펴보겠습니다.

void couldBeLeaky()
{
  Simple* mySimplePtr = new Simple();
  mySimplePtr->go();
  delete mySimplePtr;
}

이 함수는 Simple 객체를 동적으로 할당하고 사용한 뒤 delete를 호출합니다. 이렇게 해도 메모리 누수가 발생할 가능성은 남아 있습니다. go() 메소드에서 예외가 발생하면 delete가 실행되지 않기 때문입니다.

 

대신 앞에서 본 두 예제 코드는 모두 std::make_unique()라는 헬퍼 함수를 사용하여 unique_ptr로 구현해야 합니다. unique_ptr은 제너릭 스마트 포인터이므로, 어떤 종류의 메모리도 가리킬 수 있습니다. unique_ptr은 클래스 템플릿이고, make_unique()는 함수 템플릿입니다. 따라서, <> 사이에 템플릿 파라미터가 필요하며, 이는 unique_ptr로 가리키고자 하는 메모리의 타입을 지정해주어야 합니다.

 

다음 함수는 raw pointer 대신 unique_ptr을 사용합니다. Simple 객체는 더 이상 명시적으로 해제되지 않습니다. 하지만 unique_ptr 인스턴스가 스코프를 벗어날 때(함수의 끝에서 또는 예외가 발생했을 때), 자동으로 unique_ptr의 소멸자에서 Simple 객체를 해제합니다.

void notLeaky()
{
  auto mySimpleSmartPtr { std::make_unique<Simple>() };
  mySimpleSmartPtr->go();
}

위 코드는 make_unique()(since C++14)와 auto 키워드를 혼합하여 사용했습니다. 그래서 간단히 포인터의 타입인 Simple만 지정해주었습니다. Simple의 생성자에서 매개변수를 받는다면 make_unique() 호출문의 소괄호 사이에 지정해주면 됩니다. 예를 들어, Simple(int, int)라면 make_unique<Simple>(1, 2);와 같이 생성자 인수를 전달할 수 있습니다.

 

make_unique()는 값 초기화를 사용합니다. 예를 들어, 기본 타입(Primitive types)는 0으로, 객체는 기본 생성자로 초기화됩니다. 만약 이 값 초기화를 사용할 필요가 없고 성능이 중요한 코드에서의 인스턴스라면 C++20에서 도입되는 기본 초기화를 사용하는 make_unique_for_overwrite() 함수를 사용할 수 있습니다. 기본 타입에서 이는 완전히 초기화되지 않으며 그 위치에서의 메모리가 어떠한 값이라도 포함할 수 있습니다. 반면 객체는 여전히 기본 생성자로 초기화됩니다.

 

make_unique()를 지원하지 않는 컴파일러의 경우에는 아래와 같이 생성자를 직접 호출하여 unique_ptr을 생성할 수도 있습니다.

std::unique_ptr<Simple> mySimpleSmartPtr(new Simple());
std::unique_ptr<Simple> mySimpleSmartPtr{ new Simple() };

 

(since C++20) 클래스 템플릿 인수 추론(class template argument deduction; CTAD)를 사용하여 컴파일러가 클래스 템플릿의 템플릿 타입을 인수를 기반으로하여 추론할 수 있습니다. 예를 들면, vector v{1, 2}는 vector<int> v{1, 2}를 대신하여 사용될 수 있습니다. 하지만 unique_ptr은 CTAD가 동작하지 않으며, 템플릿 타입을 생략할 수 없습니다.

 

C++17 이전에 타입을 오직 한 번만 지정해줄 뿐만 아니라 안전상의 이유로 make_unique()를 사용해야 했습니다.

다음의 foo()라는 함수의 호출을 살펴보겠습니다.

foo(std::unique_ptr<Simple>{ new Simple() }, std::unique_ptr<Bar>{ new Bar{ data() } });

만약 Simple 또는 Bar의 생성자 또는 data() 함수에서 예외가 발생한다면, 컴파일러 최적화에 따라서 Simple 또는 Bar 객체가 누수될 수 있습니다. 반면 make_unique()를 사용하면 누수가 발생하지 않습니다.

foo(std::make_unique<Simple>(), std::make_unique<Bar>(data());

C++17부터는 위의 두 호출 모두 안전하지만, 가독성을 감안하면 make_unique()를 사용하는 것이 더 좋습니다.

 

 

5.1.2 unique_ptr 사용 방법

표준 스마트 포인터의 대표적인 장점은 문법을 새로 익히지 않아도 된다는 것입니다. 스마트 포인터는 일반 포인터와 똑같이 *나 ->로 역참조합니다. 예를 들어, 앞에서 본 예제에서 go() 메소드를 호출할 때 -> 연산자를 사용했습니다.

mySimpleSmartPtr->go();

다음과 같이 일반 포인터처럼 작성해도 됩니다.

(*mySimpleSmartPtr).go();

 

get() 메소드를 이용하면 내부 포인터에 직접 접근할 수 있습니다. 이는 일반 포인터만 전달할 수 있는 함수에 스마트 포인터를 전달할 때 유용합니다. 예를 들어 다음의 함수가 있다고 가정해봅시다.

void processData(Simple* simple) { /* Use the simple pointer... */ }

그러면 이 함수를 다음과 같이 호출할 수 있습니다.

auto mySimpleSmartPtr = std::make_unique_ptr<Simple>();
processData(mySimpleSmartPtr.get());

 

reset() 메소드를 사용하면 unique_ptr의 내부 포인터를 해제하고, 필요하다면 이를 다른 포인터로 변경할 수 있습니다. 예를 들면 다음과 같습니다.

mySimpleSmartPtr.reset();             // Free resource and set to nullptr
mySimpleSmartPtr.reset(new Simple()); // Free resource and set to a new Simple instance

 

release() 메소드를 사용하면 unique_ptr과 내부 포인터의 관계를 끊을 수 있습니다. release() 메소드는 리소스에 대한 내부 포인터를 리턴한 뒤 스마트 포인터를 nullptr로 설정합니다. 그러면 스마트 포인터는 그 리소스에 대한 소유권을 잃으며, 리소스를 다 쓴 뒤에 반드시 직접 해제해야 합니다. 예를 들면 다음과 같습니다.

Simple* simple = mySimpleSmartPtr.release(); // release ownership
// use the simple pointer...
delete simple
simple = nullptr;

 

unique_ptr은 단독 소유권을 가지고 있기 때문에 복사할 수 없습니다. 그러나 std::move() 유틸리티를 사용하면 하나의 unique_ptr을 다른 곳으로 이동할 수 있는데, 복사라기보다는 이동의 개념입니다. 다음과 같이 소유권을 명시적으로 이전하는 용도로 많이 사용합니다.

class Foo
{
public:
  Foo(std::unique_ptr<int> data) : m_data( std::move(data) } {}

private:
  std::unique_ptr<int> m_data;
};

auto myIntSmartPtr { std::make_unique<int>(42) };
Foo f{ std::move(myIntSmartPtr) };

 

5.1.3 unique_ptr과 C-스타일 배열

unique_ptr은 기존 C 스타일의 동적 할당 배열을 저장하는 데 적합합니다. 예를 들어 정수 10개를 가진 C 스타일의 동적 할당 배열을 다음과 같이 표현할 수 있습니다.

auto myVariableSizedArray { std::make_unique<int[]>(10) };

myVariableSizedArray의 타입은 unique_ptr<int[]> 이고, 배열 문법을 사용하여 각 요소에 액세스할 수 있습니다.

myVariableSizedArray[1] = 123;

 

이렇게 unique_ptr로 C 스타일의 동적 하랑 배열을 저장할 수는 있지만, 이보다는 std::array나 std::vector와 같은 표준 라이브러리 컨테이너를 사용하는 것이 좋습니다.

 

5.1.4 Custom Deleters

기본적으로 unique_ptr은 표준 new와 delete 연산자를 사용하여 메모리를 할당하거나 해제합니다. 하지만 다음과 같이 메모리를 할당하고 해제하는 방식을 변경할 수 있습니다.

int* my_alloc(int value)
{
  return new int{ value };
}
void my_free(int* p)
{
  delete p;
}

int main()
{
  std::unique_ptr<int, decltype(&my_free)> myIntSmartPtr{ my_alloc(42), my_free };
}

이 코드는 my_alloc()으로 정수 하나를 위한 메모리를 할당하고, unique_ptr은 my_free() 함수를 호출하여 메모리를 해제합니다. 이러한 unique_ptr의 기능을 제공하는 이유는 메모리가 아닌 다른 리소스를 관리하기에 편하기 때문입니다. 예를 들어, 파일이나 네트워크 소켓 등을 가리키던 unique_ptr이 스코프를 벗어날 때 이러한 리소스를 자동으로 닫는데 활용할 수 있습니다.

 

아쉽게도 unique_ptr에 custom deleter를 작성하는 문법은 조금 지저분합니다. 작성하는 custome deleter의 타입을 템플릿 타입 매개변수로 지정하기 때문입니다. 앞의 예에서 decltype(&my_free)를 사용했는데, 이는 my_free()함수의 타입의 포인터를 리턴합니다. shared_ptr로 custom deleter를 작성하는 것은 이보다 쉽습니다. 아래에서 어떻게 스코프에서 벗어낫을 때 custom deleter로 파일을 닫을 수 있는지 살펴보겠습니다.

 

 

5.2 shared_ptr

때때로 여러 객체에서 동일한 포인터의 복사본이 필요할 때가 있습니다. unique_ptr은 복사될 수 없고, 따라서 이러한 경우에 사용될 수 없습니다. 대신 std::shared_ptr은 공유 소유권(shared ownership)을 지원하고 복사될 수 있는 스마트 포인터입니다. shared_ptr은 참조 카운팅(reference counting)이라는 것을 통해 실제 리소스가 해제되는 타이밍을 결정하는데, 이는 잠시 뒤에 살펴보도록 하겠습니다.

 

shared_ptr의 생성과 사용 방법

shared_ptr의 사용법은 unique_ptr과 유사합니다. shared_ptr 하나를 생성하기 위해서는 make_shared()를 사용하여 생성하는데, 이렇게 하는 것이 shared_ptr을 직접 생성하는 것보다 효율적입니다.

예를 들면, 다음과 같습니다.

auto mySimpleSmartPtr = std::make_shared<Simple>();
shared_ptr을 생성할 때 항상 make_shared()를 사용하는 것을 권장합니다.

unique_ptr과 마찬가지로 shared_ptr에도 CTAD는 동작하지 않습니다. 따라서 템플릿 타입을 꼭 지정해주어야 합니다.

 

make_shared()는 make_unique()와 유사하게 값 초기화를 사용합니다. 만약 이를 원하지 않는다면 C++20부터 도입되는 make_shared_for_overwrite()를 사용하면 됩니다.

 

C++17부터 shared_ptr도 unique_ptr처럼 C-스타일의 동적 할당 배열에 대한 포인터를 저장할 수 있습니다. 추가적으로 C++20부터는 make_shared()를 사용하여 저장할 수도 있습니다. 그러나 왠만하면 표준 라이브러리 컨테이너를 사용하는 것을 권장합니다.

 

shared_ptr도 get()reset() 메소드를 지원합니다. 큰 차이점은 reset()을 호출하면 오직 마지막 shared_ptr이 제거되거나 reset될 때 리소스를 해제한다는 점입니다. 참고로 shared_ptr은 release()를 지원하지 않습니다.

현재 동일한 리소스를 공유하는 shared_ptr의 개수는 use_count() 메소드를 통해 알아낼 수 있습니다.

 

unique_ptr과 마찬가지로, shared_ptr도 기본적으로 표준 new/delete 연산자를 사용하여 메모리를 할당하고 해제하며, C 스타일의 배열을 저장할 때는 new[]/delete[] 연산자를 기본으로 사용합니다. 이는 다음과 같이 변경할 수 있습니다.

int* my_alloc(int value)
{
  return new int{ value };
}
void my_free(int* p)
{
  delete p;
}

int main()
{
  std::shared_ptr<int> myIntSmartPtr{ my_alloc(42), my_free };
}

보다시피 custom deleter의 타입을 템플릿 타입 매개변수로 지정하지 않아도 됩니다. 따라서 unique_ptr에 대해 custom deleter를 작성할 때보다 훨씬 간편합니다.

 

다음 코드는 shared_ptr로 파일 포인터를 저장하는 예시를 보여줍니다. shared_ptr이 파괴될 때(이 경우 스코프를 벗어날 때), close()를 호출하면서 파일 포인터가 자동으로 닫힙니다. 참고로 C++에는 파일을 다루는 객체지향 클래스를 별도로 제공하는데, 이 클래스도 파일을 자동으로 닫아줍니다. 지금 예제는 shared_ptr을 메모리가 아닌 다른 리소스에도 사용할 수 있다는 것을 보여주기 위해 기본 C 함수인 fopen()과 fclose()를 사용했습니다.

void close(FILE* filePtr)
{
  if (filePtr == nullptr)
    return;
  
  fclose(filePtr);
  std::cout << "File closed." << std::endl;
}

int main()
{
  FILE* f { fopen("data.txt", "w") };
  std::shared_ptr<FILE> filePtr { f, close };
  
  if (filePtr == nullptr) {
    std::cerr << "Error opening file." << std::endl;
  }
  else {
    std::cout << "File opened." << std::endl;
    // use filePtr
  }
}

 

5.2.1 Reference Counting 참조 카운팅

shared_ptr처럼 공유 소유권을 가진 스마트 포인터가 스코프를 벗어나거나 리셋될 때, 만약 이 스마트 포인터가 마지막 포인터라면 참조하던 리소스를 해제합니다. 이는 shared_ptr 표준 라이브러리 스마트 포인터에서 참조 카운팅(reference counting)을 사용하고 있기 때문입니다.

 

참조 카운팅은 어떤 클래스의 인스턴스의 개수나 현재 사용 중인 특정한 객체를 추적하는 메커니즘입니다. 참조 카운팅을 지원하는 스마트 포인터는 얼마나 많은 스마트 포인터가 하나의 실제 포인터나 하나의 객체를 참조하도록 구성되었는지 추적합니다. 이러한 참조 카운팅 스마트 포인터가 복사될 때마다 동일한 리소스를 가리키는 새 인스턴스가 생성되고 참조 카운트가 증가합니다. 이러한 스마트 포인터 인스턴스가 스코프를 벗어나거나 리셋되면 참조 카운트는 감소합니다. 참조 카운트가 0으로 떨어지면, 리소스의 다른 소유자가 없기 때문에 스마트 포인터는 리소스를 해제합니다.

 

참조 카운팅 스마트 포인터는 double deletion과 같은 많은 메모리 관리 이슈들을 해결했습니다. 예를 들어, 다음 코드처럼 두 개의 raw 포인터가 같은 메모리를 가리키고 있다고 가정해봅시다.

다음의 코드는 예제에 사용될 Simple 클래스입니다.

class Simple
{
public:
  Simple() { std::cout << "Simple constructor called!" << std::endl; }
  ~Simple() { std::cout << "Simple destructor called!" << std::endl; }
};

 

Simple* mySimple1 { new Simple() };
Simple* mySimple2 { mySimple1 };    // Make a copy of the pointer.

이제 두 raw 포인터를 둘 다 delete하면 double deletion이 발생합니다.

delete mySimple2;
delete mySimple1;

물론 이상적으로 이러한 코드를 찾을 수 없겠지만, 함수 호출의 여러 계층에서 이미 한 함수가 해제한 메모리를 다른 함수가 해제할 수 있습니다.

 

shared_ptr을 사용하면 double deletion 문제를 피할 수 있습니다.

auto smartPtr1 { std::make_shared<Simple>() };
auto smartPtr2 { smartPtr1 };

이 경우에 두 스마트 포인터가 스코프를 벗어나거나 리셋되면, 정확히 한번만 Simple 인스턴스가 해제됩니다.

 

하지만, 이는 raw 포인터가 없을 때만 올바르게 동작합니다. 예를 들어, new를 사용하여 메모리를 할당한 다음 동일한 raw 포인터를 참조하여 두 개의 shared_ptr 인스턴스를 생성한다고 가정해봅시다.

Simple* mySimple { new Simple() };
std::shared_ptr<Simple> smartPtr1 { mySimple };
std::shared_ptr<Simple> smartPtr2 { mySimple };

두 스마트 포인터는 그들이 파괴될 때 동일한 객체를 서로 삭제하려고 시도합니다. 컴파일러에 따라 다르겠지만, 프로그램이 죽을 수도 있습니다. 만약 제대로 실행된다면 다음과 같은 결과가 출력됩니다.

생성자는 한 번 호출되고, 소멸자는 두 번 호출되는 이상한 현상이 발생합니다. unique_ptr로 작성할 때도 똑같은 문제가 발생합니다. 참조 카운팅을 지원하는 shared_ptr 클래스로도 이런 일이 발생해서 의아해할 수 있지만 C++ 표준에 따른 정상적인 동작입니다. 여러 shared_ptr 인스턴스를 동일한 메모리를 가리키도록 하는 안전한 방법은 이러한 shared_ptr을 단순히 복사하면 됩니다.

auto smartPtr1 = std::make_shared<Simple>();
std::shared_ptr<Simple> smartPtr2(smartPtr1);

 

5.2.2 Casting a shared_ptr

특정 타입의 raw 포인터를 다른 타입의 포인터로 캐스팅할 수 있는 것처럼 특정 타입을 저장하는 shared_ptr도 다른 타입의 shared_ptr로 캐스팅할 수 있습니다. 물론 캐스팅할 수 있는 타입에 제약이 있습니다. 모든 캐스팅이 유효한 것은 아닙니다. 

 

shared_ptr을 캐스팅하는데 사용할 수 있는 함수는 const_pointer_cast(), dynamic_pointer_cast(), static_pointer_cast(), reinterpret_pointer_cast() 가 있습니다. 이들은 비 스마트 포인터 캐스팅 함수 const_cast(), dynamic_cast(), static_cast(), reinterpret_cast()와 비슷하게 동작합니다.

 

5.2.3 Aliasing 앨리어싱

shared_ptr은 앨리어싱(aliasing)을 지원합니다. 그래서 한 포인터(owned pointer)를 다른 shared_ptr과 공유하면서 다른 객체(stored pointer)를 가리킬 수 있습니다. 예를 들어, shared_ptr이 객체를 가리키는 동시에 그 객체의 멤버도 가리키도록 할 수 있습니다. 

코드로 표현하면 다음과 같습니다.

class Foo
{
public:
  Foo(int value) : m_data { value } {}
  int m_data;
};

auto foo { std::make_shared<Foo>(42) };
auto aliasing { std::shared_ptr<int>{ foo, &foo->m_data } };

여기서 두 shared_ptr(foo와 aliasing)이 모두 삭제될 때만 Foo 객체가 삭제됩니다.

 

소유한 포인터(owned pointer)는 참조 카운팅에 사용되는 반면, 저장된 포인터(stored pointer)는 포인터를 역참조하거나 그 포인터에 대해 get()을 호출할 때 리턴됩니다.

 

5.3 weak_ptr

shared_ptr과 관련하여 C++에서는 weak_ptr이라는 또 다른 스마트 포인터를 지원합니다. weak_ptr은 shared_ptr이 가리키는 리소스의 레퍼런스를 관리하는데 사용됩니다.

weak_ptr은 리소스를 직접 소유하지 않기 때문에 shared_ptr이 해당 리소스를 해제하는데 아무런 영향을 미치지 않습니다. weak_ptr은 삭제될 때(ex, 스코프를 벗어날 때) 가리키던 리소를 삭제하지 않습니다. 그러나 shared_ptr이 그 리소스를 해제했는지 알아내는데 사용할 수 있습니다. weak_ptr의 생성자는 shared_ptr이나 다른 weak_ptr을 인수로 받습니다. 

weak_ptr에 저장된 포인터에 접근하려면, shared_ptr로 변환해야 하는데, 변환하는 방법은 다음과 같이 두 가지가 있습니다.

  • weak_ptr 인스턴스의 lock() 메소드를 사용하여 shared_ptr을 리턴받습니다. 이때, shared_ptr에 연결된 weak_ptr이 해제되면 shared_ptr의 값은 nullptr이 됩니다.
  • shared_ptr의 생성자에 weak_ptr을 인수로 전달해서 shared_ptr을 새로 생성합니다. 이때 shared_ptr에 연결된 weak_ptr이 해제되면 std::bad_weak_ptr 예외가 발생합니다.

다음은 weak_ptr을 사용하는 예제 코드입니다.

void useResource(std::weak_ptr<Simple>& weakSimple)
{
  auto resource{ weakSimple.lock() };
  if (resource) {
    std::cout << "Resource still alive." << std::endl;
  }
  else {
    std::cout << "Resource has been freed!" << std::endl;
  }
}

int main()
{
  auto sharedSimple{ std::make_shared<Simple>() };
  std::weak_ptr<Simple> weakSimple{ sharedSimple };
  
  // Try to use the weak_ptr
  useResource(weakSimple);
  
  // Reset the shared_ptr
  // Since there is only 1 shared_ptr to the Simple resource, this will
  // free the resource, even though there is still a weak_ptr alive.
  sharedSimple.reset();
  
  // Try to use the weak_ptr a second time.
  useResource(weakSimple);
}

위 코드를 실행하면 결과는 다음과 같습니다.

C++17부터는 weak_ptr 또한 C 스타일의 배열을 지원합니다.

 

5.4 Passing to Functions

파라미터 중 하나를 포인터로 전달받는 함수는 오직 소유권이 전달되거나 소유권을 공유할 때만 스마트 포인터를 전달받을 수 있습니다. shared_ptr의 소유권을 공유하기 위해서는 파라미터에 shared_ptr을 값으로 전달하면 됩니다. 비슷하게, unique_ptr의 소유권을 넘겨주려면, 간단히 파라미터에 unique_ptr을 값으로 전달하면 됩니다. unique_ptr의 경우에는 move semantics(이동 의미론)를 사용해야하는데, 이에 관해서는 아래의 이전 포스팅을 살펴보셔도 좋고,

[C++] Move Semantics

 

[C++] Move Semantics

References 씹어먹는 C++ (https://modoocode.com/228) http://thbecker.net/articles/rvalue_references/section_07.html Contents Move Semantics(std::move) 2021.08.11 - [C & C++] - [C++] 우측값 참조(rvalu..

junstar92.tistory.com

다음에 알아볼 주제로 클래스에 관한 것들을 자세히 살펴볼 예정인데, 그때 한 번 더 다루어 볼 예정입니다.

 

만약 소유권을 전달하거나 공유하지 않는다면, 이 함수는 간단히 non-const 레퍼런스나, const 레퍼런스, 또는 raw 포인터를 전달받아야 합니다. const shared_ptr<T>&나 const unique_ptr<T>&는 말이 안되는 파라미터입니다.

 

5.5 Returning from Functions

표준 스마트 포인터인 shared_ptr, unique_ptr, weak_ptr은 (named) return value optimization, (N)RVO (반환값 최적화)와 move semantics(이동 의미론) 덕분에 쉽고 효율적으로 함수에서 값으로 리턴할 수 있습니다. 지금 move semantics은 크게 중요한 부분은 아니기 때문에 넘어가고, 다음 포스팅에서 다룰 예정입니다.

여기서 중요한 것은 이동 의미론을 이용하면 함수에서 스마트 포인터를 리턴하는 과정을 굉장히 효율적으로 처리할 수 있다는 점만 알고 있으면 됩니다. 예를 들어, create() 함수를 다음과 같이 작성해서 main() 함수에서 호출할 수 있습니다.

std::unique_ptr<Simple> create()
{
  auto ptr = std::make_unique<Simple>();
  // do something with ptr..
  return ptr;
}

int main()
{
  std::unique_ptr<Simple> mySmartPtr1{ create() };
  auto mySmartPtr2{ create() };
}

 

5.6 enable_shared_from_this

std::enable_shared_from_this을 상속받으면 객체의 메소드에서 shared_ptr이나 weak_ptr을 안전하게 리턴할 수 있습니다. 이 클래스의 기본 클래스없이, this에 대해 유효한 shared_ptr이나 weak_ptr을 리턴하는 한 가지 방법은 클래스의 멤버에 weak_ptr을 추가하고 이것의 복사본을 리턴하거나 이것으로 생성된 shared_ptr을 리턴하는 것입니다. enable_shared_from_this 클래스는 이로부터 파생되는 클래스에 다음 두 개의 메소드를 추가합니다.

  • shared_from_this() : 객체의 소유권을 공유하는 shared_ptr을 리턴합니다.
  • weak_from_this() : 객체의 소유권을 추적하는 weak_ptr을 리턴합니다.

이 기능에 대해서 자세하게는 아니지만, 아래 예제 코드를 통해 간단하게 사용 방법을 살펴보겠습니다. shared_from_this()와 weak_from_this()는 public 메소드입니다만, public 인터페이스들에서 혼동이 될 수 있어 아래 예제 코드에서는 Foo 클래스에 getPointer()라는 메소드를 정의하고 있습니다.

class Foo : public std::enable_shared_from_this<Foo>
{
public:
  std::shared_ptr<Foo> getPointer() {
    return shared_from_this();
  }
};

int main()
{
  auto ptr1{ std::make_shared<Foo>() };
  auto ptr2{ ptr1->getPointer() };
}

객체에서 shared_from_this() 사용은 그 객체의 포인터가 이미 shared_ptr에 저장되어 있어야 합니다. 그렇지 않다면 bad_weak_ptr 예외가 발생합니다. 위 예제 코드에서는 main()문에서 make_shared()로 ptr1이라는 shared_ptr이 생성되었고 이는 Foo 인스턴스를 포함하고 있습니다. 반면에, weak_from_this()는 항상 호출할 수 있지만, 만약 객체의 포인터가 아직 shared_ptr에 저장되어 있지 않다면 빈 weak_ptr을 리턴합니다.

 

다음 코드는 완전히 잘못 구현한 getPointer() 메소드를 보여주고 있습니다.

class Foo
{
public:
  std::shared_ptr<Foo> getPointer() {
    return std::shared_ptr<Foo>(this);
  }
};

만약 동일한 main() 문에서 위처럼 구현된 Foo 클래스를 사용하면 double deletion이 발생합니다. 동일한 포인터를 가리키는 독립적인 shared_ptr(ptr1과 ptr2)가 생성되고, 스코프가 벗어나면 두 shared_ptr은 동일한 객체를 삭제하려고 시도합니다.

 

5.7 auto_ptr (old and removed)

C++11 이전에는 표준 라이브러리에서 스마트 포인터를 간단히 구현한 auto_ptr을 제공했습니다. 다만, auto_ptr은 몇 가지 심각한 문제점이 있었습니다. 그중 하나는 vector와 같은 표준 라이브러리 컨테이너 안에서는 제대로 동작하지 않는다는 점입니다. C++11과 C++14부터는 auto_ptr을 공식적으로 지원하지 않는다고 했으며, C++17부터는 완전히 삭제되면서 그 빈자리는 unique_ptr과 shared_ptr이 대체하였습니다. 따라서 auto_ptr은 절대 사용하면 안됩니다.

댓글