값과 소유권 ③ 이동 시맨틱과 스마트 포인터
모던 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_ptr와shared_ptr를 언제 쓰는지,weak_ptr가 왜 필요한지 구분할 수 있다.
다음은 STL — 컨테이너와 알고리즘입니다. 이제 자원 관리를 표준 라이브러리가 대신 해주는 세계로 들어갑니다.
Reference
- 씹어먹는 C++ 강좌 12~13강 (modoocode) — 우측값 레퍼런스·이동·스마트 포인터의 한글 상세
- cppreference — std::move
- cppreference — Smart pointers
- Effective Modern C++ Item 23~25 (이동 시맨틱), 18~21 (스마트 포인터)