본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::unique_ptr (11, 14)

by snowoods 2025. 10. 13.

Modern C++

 

std::unique_ptr

 

개요

std::unique_ptr는 C++11부터 도입된 스마트 포인터로, 동적으로 할당된 객체에 대한 독점적인 소유권을 관리합니다. unique_ptr가 소멸될 때 (예: 범위를 벗어날 때) 관리하던 객체도 자동으로 소멸시켜, 메모리 누수를 방지하고 예외 안전성을 높입니다. 복사가 불가능하고 이동만 가능하다는 특징이 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++11: std::unique_ptr 도입
  • C++14: std::make_unique 헬퍼 함수 도입

 

내용 설명

std::unique_ptr는 이름 그대로 포인터가 가리키는 객체에 대한 유일한(unique) 소유권을 가집니다. 이는 복사 생성자나 복사 대입 연산자가 없어 복사가 원천적으로 불가능하며, 소유권은 std::move를 통해서만 이전될 수 있음을 의미합니다.

unique_ptr의 가장 큰 장점은 범위 기반 리소스 관리(RAII, Resource Acquisition Is Initialization)를 단순화한다는 것입니다. unique_ptr 객체가 범위를 벗어나면 소멸자가 호출되고, 이 소멸자는 자신이 관리하던 객체를 자동으로 delete 해줍니다. 이로 인해 개발자가 delete를 수동으로 호출할 필요가 없어지므로 메모리 누수 실수를 크게 줄일 수 있습니다.

C++14부터는 std::make_unique 함수를 사용하는 것이 권장됩니다. 이 함수는 코드를 더 간결하게 만들고, 예외 발생 시에도 메모리 누수가 발생하지 않도록 보장하여 예외 안전성을 높여줍니다.

 

예제 코드

#include <iostream>
#include <memory>

class ScopeTest
{
public:
    ScopeTest(int val) : m_val(val)
    {
        std::cout << "Constructor: " << m_val << '\n';
    }

    ~ScopeTest()
    {
        std::cout << "Destructor!\n";
    }

    void test()
    {
        std::cout << "Test!\n";
    }

private:
    int m_val;
};

// std::unique_ptr를 사용하여 자동 메모리 관리
void f1()
{
    auto t = std::make_unique<ScopeTest>(10);
    t->test();
} // t가 범위를 벗어나면서 ScopeTest 객체가 자동으로 소멸됨

// new/delete를 사용한 수동 메모리 관리
void f2()
{
    // t2 포인터 변수는 stack에 생성되고, heap에는 ScopeTest 객체가 생성되고 t2 포인터가 가리킴
    auto *t2 = new ScopeTest(10);
    t2->test();
    delete t2; // 수동으로 delete 호출 필요
}

int main()
{
    f1();
    std::cout << '\n';
    f2();

    return 0;
}

 

실행 결과

Constructor: 10
Test!
Destructor!

Constructor: 10
Test!
Destructor!

 

활용팁

  • std::make_unique 사용: C++14 이상 환경에서는 new를 직접 사용하는 대신 std::make_unique를 사용하세요. 코드가 간결해지고 예외 안전성이 향상됩니다.
  • 기본 스마트 포인터로 사용: 동적으로 할당된 객체를 관리할 때, 공유 소유권(std::shared_ptr)이 명확하게 필요하지 않은 이상 std::unique_ptr를 기본으로 사용하는 것이 좋습니다.
  • 소유권 이전: 함수에서 생성한 객체의 소유권을 호출자에게 넘겨줄 때 std::unique_ptr를 반환 값으로 사용하면 편리합니다.
  • 레거시 API와 연동: get() 멤버 함수를 사용하면 관리 중인 객체의 원시 포인터(raw pointer)를 얻을 수 있습니다. 단, 이 포인터를 delete해서는 안 됩니다.
  • 소유권 해제 및 재설정: release()unique_ptr의 소유권을 포기하고 원시 포인터를 반환하며, reset()은 현재 관리하는 객체를 파괴하고 선택적으로 새로운 객체의 소유권을 가질 수 있게 합니다.