포스트

값과 소유권 ③ 이동 시맨틱과 스마트 포인터

모던 C++의 뼈대. 복사가 낭비인 상황에서 나온 우측값 레퍼런스와 이동 시맨틱, std::move의 진짜 의미, 그리고 소유권을 코드로 표현하는 스마트 포인터(unique_ptr/shared_ptr/weak_ptr)까지 예제로 정리한다.

값과 소유권 ③ 이동 시맨틱과 스마트 포인터

모던 C++ 학습 로드맵고급(값과 소유권) 단계입니다. 앞 글: ② 클래스와 자원 관리

앞 글의 Rule of Three는 복사를 깊은 복사로 만들어 안전하게 했습니다. 그런데 곧 없어질 임시 객체까지 통째로 복제하는 건 낭비입니다. 여기서 모던 C++(C++11)의 핵심, 이동 시맨틱이 나옵니다. 이 장이 고급 단계에서 가장 중요합니다.

복사는 왜 낭비인가

1
2
Buffer make() { return Buffer(1000); }  // 함수가 임시 객체를 반환
Buffer b = make();                       // 임시 객체를 b로 "복사"?

make()가 만든 임시 객체는 이 줄이 끝나면 사라집니다. 그걸 깊은 복사로 1000개를 새로 할당해 베낀 뒤 원본을 버리는 건 낭비죠. 어차피 버려질 객체라면, 내부 포인터를 그냥 훔쳐오면 됩니다. 이게 이동입니다.

우측값 레퍼런스와 이동 생성자

“곧 사라질 임시 객체”를 가리키는 새로운 참조가 우측값 레퍼런스 T&&입니다. 이걸 받는 이동 생성자를 정의하면, 복사 대신 자원을 넘겨받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Buffer {
    int* data;
    int size;
public:
    // 이동 생성자 — 복제하지 않고 포인터만 가져온다
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;   // ★ 원본을 비워, 소멸 시 이중 해제 방지
        other.size = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {   // 이동 대입
        if (this == &other) return *this;
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
    // ... 앞 글의 복사 생성자/대입/소멸자도 함께
};

복사 셋(Rule of Three)에 이동 둘이 더해져 Rule of Five가 됩니다. 핵심은 이동 생성자가 원본을 비워두는 것 — 그래야 원본이 소멸할 때 이미 넘긴 자원을 또 해제하지 않습니다.

std::move는 “옮기지” 않는다

가장 흔한 오해입니다. std::move는 아무것도 이동시키지 않습니다. “이 객체는 이제 훔쳐가도 된다”고 표시(캐스트)만 합니다. 실제 이동은 그 표시를 받은 이동 생성자/대입이 합니다.

1
2
3
Buffer a(1000);
Buffer b = std::move(a);   // a를 우측값으로 캐스트 → 이동 생성자 호출
// 이 시점부터 a는 "비어 있는 유효한 상태" — 다시 쓰면 안 된다

그래서 std::move 후의 원본은 건드리지 말아야 합니다(재대입은 가능).

스마트 포인터 — 소유권을 타입으로 표현

이동 시맨틱을 이해하면 스마트 포인터가 자연스럽게 들어옵니다. 스마트 포인터는 RAII로 new/delete를 감싸, 누가 이 자원을 소유하는가를 타입으로 드러냅니다. 이제 delete를 직접 쓸 일이 사라집니다.

타입소유 방식언제
unique_ptr유일 소유 — 복사 불가, 이동만소유자가 하나일 때 (대부분의 경우)
shared_ptr공유 소유 — 참조 카운트여러 곳이 같은 자원을 나눠 가질 때
weak_ptr소유하지 않는 관찰자shared_ptr 순환 참조를 끊을 때
1
2
3
4
5
6
7
auto p = std::make_unique<Widget>();   // 유일 소유
auto q = std::move(p);                 // 소유권 이전 — p는 이제 비어 있음
// unique_ptr는 복사가 안 되므로 이동으로만 넘긴다

auto s1 = std::make_shared<Widget>();  // 참조 카운트 1
auto s2 = s1;                          // 복사 OK, 카운트 2
// s1, s2가 모두 사라지면 카운트 0 → 자동 delete

unique_ptr가 “복사 불가, 이동만”인 게 바로 앞에서 배운 이동 시맨틱의 실전 사례입니다.

자주 막히는 지점

  • std::move 후 원본 사용 — 비워진 객체를 읽어 버그. 이동 후엔 재대입만.
  • shared_ptr 순환 참조 — 서로를 shared_ptr로 가리키면 카운트가 0이 안 되어 누수. 한쪽을 weak_ptr로.
  • new로 만들어 shared_ptr에 넣기make_shared를 쓰세요. 할당이 한 번으로 줄고 예외 안전합니다.
  • 이동 생성자에 noexcept 누락vector 등이 재할당 시 이동 대신 복사로 폴백해 성능이 죽습니다.

통과 기준

  • 복사와 이동의 차이를, “임시 객체를 왜 복제하지 않는가”로 설명할 수 있다.
  • std::move가 실제로 하는 일(캐스트)을 정확히 말할 수 있다.
  • unique_ptrshared_ptr를 언제 쓰는지, weak_ptr가 왜 필요한지 구분할 수 있다.

다음은 STL — 컨테이너와 알고리즘입니다. 이제 자원 관리를 표준 라이브러리가 대신 해주는 세계로 들어갑니다.

Reference

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.