C++ move 에러 | "use after move" 크래시와 이동 의미론 실수 해결

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란?
  2. use after move 버그
  3. 이동 생성자/대입 연산자
  4. 반환값 최적화 (RVO)
  5. 자주 나오는 에러 10가지
  6. 정리

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 객체❌ 의미 없음복사됨

핵심 규칙

  1. std::move는 캐스팅일 뿐 (실제 이동은 이동 생성자)
  2. 이동 후 객체는 사용 금지 (재할당만 가능)
  3. 반환값에 std::move 금지 (RVO 방해)
  4. 이동 생성자에 noexcept (STL 최적화)
  5. Rule of Five 준수

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 이동 의미론 | std::move 완벽 가이드
  • C++ 우측값 참조 | rvalue reference 가이드
  • C++ 완벽한 전달 | std::forward 가이드
  • C++ Rule of Five | 복사·이동 생성자 완벽 가이드

마치며

이동 의미론은 C++11의 핵심 기능이지만, 잘못 사용하면 크래시가 발생합니다.

핵심 원칙:

  1. std::move 후 객체는 사용 금지
  2. 반환값에 std::move 금지 (RVO)
  3. 이동 생성자에 noexcept
  4. Rule of Five 준수

std::move성능 최적화의 핵심이지만, 남용하지 마세요. 대부분의 경우 컴파일러가 자동으로 최적화합니다.

다음 단계: 이동 의미론을 이해했다면, C++ 완벽한 전달과 C++ 우측값 참조로 더 깊이 배워보세요.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |