C++ move 에러 | "use after move" 크래시와 이동 의미론 실수 해결
이 글의 핵심
C++ move 에러에 대한 실전 가이드입니다.
들어가며: “std::move를 썼더니 크래시가 나요"
"이동 후에 객체를 사용했더니 이상해요”
C++11의 이동 의미론(Move Semantics)은 불필요한 복사를 제거해 성능을 높이지만, 잘못 사용하면 크래시가 발생합니다.
// ❌ use after move
std::string str = "Hello";
std::string str2 = std::move(str); // str의 내용을 str2로 이동
std::cout << str << '\n'; // ❌ 이동된 객체 사용 → 미정의 동작
이 글에서 다루는 것:
- use after move 버그
- std::move의 동작 원리
- 이동 생성자/대입 연산자
- 반환값 최적화 (RVO)
- 자주 나오는 move 에러 10가지
목차
1. std::move란?
std::move는 캐스팅일 뿐
std::move는 실제로 이동하지 않습니다. 단지 lvalue를 rvalue로 캐스팅할 뿐입니다.
std::string str = "Hello";
std::string str2 = std::move(str);
// ^^^^^^^^^^^
// lvalue → rvalue 캐스팅
// 실제 이동은 이동 생성자가 수행
// std::string::string(std::string&& other)
이동 vs 복사
// 복사
std::vector<int> vec1(1000000, 42);
std::vector<int> vec2 = vec1; // 100만 개 복사 (느림)
// 이동
std::vector<int> vec3(1000000, 42);
std::vector<int> vec4 = std::move(vec3); // 포인터만 이동 (빠름)
// vec3는 이제 빈 벡터
2. use after move 버그
문제 코드
// ❌ use after move
std::string str = "Hello";
std::string str2 = std::move(str);
std::cout << str << '\n'; // ❌ 이동된 객체 사용
std::cout << str.size() << '\n'; // ❌ 미정의 동작
이동 후 상태: 유효하지만 불특정 (valid but unspecified).
안전한 연산:
- 소멸자 호출
- 재할당 (
str = "World";) clear(),reset()등
위험한 연산:
- 값 읽기 (
str.size(),str[0]) - 멤버 함수 호출 (
str.append())
해결법
// ✅ 이동 후 재할당
std::string str = "Hello";
std::string str2 = std::move(str);
str = "World"; // 재할당 (안전)
std::cout << str << '\n'; // "World"
// ✅ 이동 후 사용 안 함
std::string str3 = "Hello";
std::string str4 = std::move(str3);
// str3 사용 안 함
3. 이동 생성자/대입 연산자
이동 생성자 구현
class MyClass {
int* data_;
size_t size_;
public:
// 이동 생성자
MyClass(MyClass&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 원본을 빈 상태로
other.size_ = 0;
}
// 이동 대입 연산자
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data_; // 기존 리소스 해제
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
~MyClass() {
delete[] data_;
}
};
주의: noexcept 키워드가 중요합니다 (STL 최적화).
Rule of Five
class MyClass {
public:
// 1. 소멸자
~MyClass();
// 2. 복사 생성자
MyClass(const MyClass& other);
// 3. 복사 대입 연산자
MyClass& operator=(const MyClass& other);
// 4. 이동 생성자
MyClass(MyClass&& other) noexcept;
// 5. 이동 대입 연산자
MyClass& operator=(MyClass&& other) noexcept;
};
규칙: 하나를 정의하면 다섯 개 모두 고려해야 합니다.
4. 반환값 최적화 (RVO)
RVO란?
RVO(Return Value Optimization)는 컴파일러가 반환값 복사를 제거하는 최적화입니다.
// 복사도 이동도 없음 (RVO)
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return vec; // RVO: 복사/이동 없음
}
std::vector<int> result = createVector(); // 직접 생성
std::move를 쓰면 안 되는 경우
// ❌ RVO 방해
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return std::move(vec); // ❌ RVO 방해 → 이동 발생
}
// ✅ 올바른 코드 (std::move 없이)
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return vec; // RVO
}
규칙: 지역 변수를 반환할 때는 std::move를 쓰지 마세요.
5. 자주 나오는 에러 10가지
에러 1: use after move
// ❌ 이동 후 사용
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1);
std::cout << *ptr1 << '\n'; // ❌ nullptr 역참조 → 크래시
에러 2: const 객체 이동
// ❌ const는 이동 불가
const std::string str = "Hello";
std::string str2 = std::move(str); // 이동이 아니라 복사!
// std::move는 const T&& 를 반환하지만,
// 이동 생성자는 T&& (비const)를 받으므로 복사 생성자 호출
에러 3: 반환값에 std::move
// ❌ RVO 방해
std::vector<int> foo() {
std::vector<int> vec = {1, 2, 3};
return std::move(vec); // ❌ 불필요
}
// ✅ 올바른 코드
std::vector<int> foo() {
std::vector<int> vec = {1, 2, 3};
return vec; // RVO
}
에러 4: 이동 생성자 없음
// ❌ 이동 생성자 없음
class MyClass {
std::unique_ptr<int> ptr_;
public:
// 복사 생성자만 정의
MyClass(const MyClass& other) {
// unique_ptr는 복사 불가 → 컴파일 에러
}
};
// ✅ 이동 생성자 추가
class MyClass {
std::unique_ptr<int> ptr_;
public:
MyClass(MyClass&& other) noexcept = default; // 기본 이동 생성자
};
에러 5: 이동 후 자기 대입
// ❌ 자기 대입 체크 없음
MyClass& operator=(MyClass&& other) noexcept {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
return *this;
}
MyClass obj;
obj = std::move(obj); // 자기 대입 → data_ 해제 후 nullptr 대입 → 크래시
// ✅ 자기 대입 체크
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) { // 자기 대입 체크
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
에러 6: 함수 인자에 std::move
// ❌ 불필요한 std::move
void process(std::string s) { // 값 전달 (이미 복사 또는 이동)
// ...
}
std::string str = "Hello";
process(std::move(str)); // 불필요 (값 전달이므로 자동 이동)
// ✅ 올바른 코드
void process(std::string s) {
// ...
}
std::string str = "Hello";
process(std::move(str)); // OK (명시적 이동)
// 또는
process(str); // 복사 (str을 계속 사용할 거면)
에러 7: 우측값 참조를 반환
// ❌ 우측값 참조 반환
std::string&& foo() {
std::string str = "Hello";
return std::move(str); // ❌ 지역 변수 참조 반환
}
// ✅ 값 반환
std::string foo() {
std::string str = "Hello";
return str; // RVO
}
에러 8: 이동 불가능한 타입
// ❌ 이동 불가능
class NonMovable {
public:
NonMovable(NonMovable&&) = delete; // 이동 생성자 삭제
};
NonMovable obj1;
NonMovable obj2 = std::move(obj1); // 컴파일 에러
// error: use of deleted function 'NonMovable::NonMovable(NonMovable&&)'
에러 9: 이동 후 벡터 크기 가정
// ❌ 이동 후 크기 가정
std::vector<int> vec1(1000, 42);
std::vector<int> vec2 = std::move(vec1);
// vec1.size()는 0일 수도, 1000일 수도 있음 (구현 의존)
for (int x : vec1) { // ❌ 이동된 벡터 순회
// ...
}
// ✅ 이동 후 사용 안 함
std::vector<int> vec3(1000, 42);
std::vector<int> vec4 = std::move(vec3);
// vec3 사용 안 함
에러 10: 완벽한 전달 실수
// ❌ 우측값을 좌측값으로 전달
template <typename T>
void wrapper(T&& arg) {
process(arg); // ❌ arg는 좌측값 (이름이 있으므로)
}
// ✅ std::forward 사용
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 우측값은 우측값으로 전달
}
실전 사례 분석
사례 1: 벡터 이동 최적화
Before:
// ❌ 불필요한 복사
std::vector<int> createData() {
std::vector<int> data(1000000, 42);
return data; // RVO (복사 없음)
}
void process() {
std::vector<int> result = createData(); // RVO
}
After: 이미 최적화됨 (std::move 불필요).
사례 2: unique_ptr 소유권 이전
class ResourceManager {
std::vector<std::unique_ptr<Resource>> resources_;
public:
void add(std::unique_ptr<Resource> res) {
resources_.push_back(std::move(res)); // 소유권 이전
}
std::unique_ptr<Resource> take(size_t idx) {
auto res = std::move(resources_[idx]); // 소유권 이전
resources_.erase(resources_.begin() + idx);
return res; // RVO (std::move 불필요)
}
};
사례 3: 이동 전용 타입
// 복사 금지, 이동만 허용
class MoveOnly {
std::unique_ptr<int> data_;
public:
MoveOnly() = default;
// 복사 금지
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
// 이동 허용
MoveOnly(MoveOnly&&) noexcept = default;
MoveOnly& operator=(MoveOnly&&) noexcept = default;
};
// 사용
MoveOnly obj1;
MoveOnly obj2 = std::move(obj1); // OK
// MoveOnly obj3 = obj1; // 컴파일 에러
정리
move 에러 방지 체크리스트
- std::move 후에 객체를 사용하지 않는가?
- 반환값에 불필요한 std::move를 쓰지 않았는가?
- 이동 생성자에 noexcept를 붙였는가?
- const 객체를 std::move하지 않았는가?
- 자기 대입을 체크하는가? (이동 대입 연산자)
std::move 사용 규칙
| 상황 | std::move 사용 | 이유 |
|---|---|---|
| 지역 변수 반환 | ❌ 불필요 | RVO |
| 소유권 이전 | ✅ 필요 | unique_ptr 등 |
| 벡터에 추가 | ✅ 필요 | 복사 방지 |
| 함수 인자 (값 전달) | ✅ 선택적 | 명시적 이동 |
| const 객체 | ❌ 의미 없음 | 복사됨 |
핵심 규칙
- std::move는 캐스팅일 뿐 (실제 이동은 이동 생성자)
- 이동 후 객체는 사용 금지 (재할당만 가능)
- 반환값에 std::move 금지 (RVO 방해)
- 이동 생성자에 noexcept (STL 최적화)
- Rule of Five 준수
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 이동 의미론 | std::move 완벽 가이드
- C++ 우측값 참조 | rvalue reference 가이드
- C++ 완벽한 전달 | std::forward 가이드
- C++ Rule of Five | 복사·이동 생성자 완벽 가이드
마치며
이동 의미론은 C++11의 핵심 기능이지만, 잘못 사용하면 크래시가 발생합니다.
핵심 원칙:
- std::move 후 객체는 사용 금지
- 반환값에 std::move 금지 (RVO)
- 이동 생성자에 noexcept
- Rule of Five 준수
std::move는 성능 최적화의 핵심이지만, 남용하지 마세요. 대부분의 경우 컴파일러가 자동으로 최적화합니다.
다음 단계: 이동 의미론을 이해했다면, C++ 완벽한 전달과 C++ 우측값 참조로 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |