Hacking Modern C++ #2

BrakieBrakie
17 Nov 2025

Move Semantics와 std::unique_ptr의 관계

C++11이 등장하면서 “소유권(Ownership)”이라는 개념이 언어 차원에서 정식으로 정립되었다. 그 중심에 Move Semanticsstd::unique_ptr가 있다. 두 기능은 서로 밀접하게 연결되어 있으며, 사실상 unique_ptr은 움직임(이동)을 전제하지 않으면 성립하기 힘든 구조다.

1. unique_ptr이 등장한 이유: 단일 소유권을 언어적으로 모델링

기존 C++98에서는 스마트 포인터가 존재하긴 했지만, 단일 소유권을 안전하게 표현하는 정식 표준 도구가 없었다.
문제점:
  • raw pointer는 누가 delete해야 하는지 알 수 없음
  • 복사하면 “이 포인터를 누가 소유하는가?”라는 개념이 무너짐
  • 메모리 누수, double delete 위험 유발
이를 해결하기 위해 C++11은 단일 소유권(Unique Ownership)을 명확히 표현하는 std::unique_ptr을 도입했다.

2. unique_ptr은 “복사 불가능하고 이동만 가능한 객체”

핵심 규칙은 단 하나다:
unique_ptr은 복사(copy)할 수 없고 이동(move)만 가능하다.
즉, 다음은 금지:
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = p1; // ERROR: copy ctor 삭제됨
이유:
  • 두 포인터가 동일한 heap 자원을 관리하면 double delete 발생
  • unique_ptr의 존재 목적 자체가 “소유자는 한 명만”이라는 제약
하지만 이동은 허용된다:
std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // OK: 소유권 이동
→ 이 움직임을 가능하게 하는 언어 기능이 바로 Move Semantics다.

3. unique_ptr이 Move Semantics에 기대는 핵심 구조

unique_ptr의 내부 구현 핵심을 요약하면 다음 형태다:
template <typename T>
class unique_ptr {
    T* ptr;

public:
    unique_ptr(const unique_ptr&) = delete;            // 복사 금지
    unique_ptr& operator=(const unique_ptr&) = delete; // 복사 금지

    unique_ptr(unique_ptr&& other) noexcept            // 이동 생성자
        : ptr(other.ptr)
    {
        other.ptr = nullptr;
    }

    unique_ptr& operator=(unique_ptr&& other) noexcept // 이동 대입
    {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
};
👉 여기서 보는 것처럼 unique_ptr의 개념 자체가 move constructor & move assignment 위에 서 있다.
만약 move semantics가 없었다면 unique_ptr은 다음 중 하나가 되었을 것:
  1. 복사도 이동도 안 되는 객체 → vector나 함수 return 값으로 쓸 수 없음
  2. 억지로 복사를 허용하는 구조 → unique ownership 철학 붕괴
따라서 unique_ptr은 move semantics를 전제로 설계된 대표적 예이다.

4. 컨테이너와 unique_ptr: move 덕분에 문제 없이 동작

C++11 이전에는 “소유권 기반 객체를 컨테이너에 담는 것” 자체가 사실상 불가능했다.
하지만 unique_ptr은 move만 허용하므로 다음 코드가 자연스럽다:
std::vector<std::unique_ptr<int>> vec;

vec.push_back(std::make_unique<int>(10));   // move 발생
vec.push_back(std::make_unique<int>(20));   // move 발생
push_back → 내부적으로 move constructor 호출 = 빠르고 안전하게 소유권 이전
이 구조는 move semantics가 없다면 절대 구현 불가다.

5. 함수 리턴과 unique_ptr: move 덕분에 값 반환이 자연스러움

std::unique_ptr<int> createPtr() {
    auto p = std::make_unique<int>(42);
    return p; // move 발생(NRVO와 결합하면 매우 효율적)
}
unique_ptr을 리턴하면 소유권이 호출자에게 자연스럽게 넘어간다. 이 또한 move semantics가 없었다면 불가능.

6. Raw Pointer 대비 명확한 소유권 모델

Move semantics + unique_ptr 조합은 기존 C++98의 포인터 사용 모델을 완전히 대체한다.
패턴 C++98 (raw ptr) C++11( unique_ptr )
소유권 명확성 알 수 없음 한 객체가 명확히 소유
복사 시 위험 double delete 위험 복사 자체가 금지
이동 의미 없음 소유권 이동이 정식 기능
컨테이너 저장 위험 안전·명확
함수 리턴 누수 위험 안전하게 소유권 이전
unique_ptr은 move semantics 없이는 성립이 불가능한 구조이며, 두 기능은 모던 C++ 메모리 모델의 핵심 축이라고 할 수 있다.

7. 결론: unique_ptr은 move semantics의 대표적 적용 사례

정리하면 다음과 같다:
  • unique_ptr은 복사 금지, 이동 허용이라는 구조적 특징을 가진다.
  • 이 철학을 구현하기 위해 move constructor가 필수적이며, move semantics가 없다면 unique_ptr은 존재할 수 없다.
  • 덕분에 현대 C++에서는 소유권 이전이 명시적으로 표현되어 메모리 누수, double free 같은 문제를 구조적으로 차단할 수 있다.
즉,
unique_ptr은 Move Semantics의 가장 강력하고 실전적인 활용 사례이며, 모던 C++의 자원 관리 모델을 대표하는 타입이다.
Buy Me A Coffee

Similar Posts