C++/Concepts

C++ std::move는 진짜 객체를 이동시킬까?

Enchantée 2026. 6. 12. 12:18
728x90
반응형

C++을 공부하다 보면 std::move라는 표현을 자주 만나게 됩니다.

이름만 보면 객체를 어딘가로 “이동”시키는 함수처럼 보입니다.
하지만 실제로 std::move는 객체를 직접 이동시키지 않습니다.

조금 이상하게 들릴 수 있지만, 이 개념을 정확히 이해해야 C++의 move semantics, smart pointer, 성능 최적화, 객체 생명주기를 제대로 다룰 수 있습니다.

이번 글에서는 std::move가 실제로 무엇을 하는지, 그리고 실무에서 어떤 점을 조심해야 하는지 정리해보겠습니다.

 


1. lvalue와 rvalue 간단 정리

std::move를 이해하려면 lvalue와 rvalue를 알아야 합니다.

간단히 말하면 다음과 같습니다.

구분의미예시

lvalue 이름이 있고 다시 접근할 수 있는 값 변수
rvalue 임시 값, 곧 사라질 값 리터럴, 임시 객체

예를 들어:

int x = 10;
int y = x + 1;

여기서 x는 이름이 있는 변수이므로 lvalue입니다.
반면 x + 1은 계산 결과로 만들어진 임시 값이므로 rvalue입니다.

C++에서는 rvalue에 대해서 “어차피 곧 사라질 값이니까 내부 자원을 재사용해도 된다”고 판단할 수 있습니다.

이 아이디어가 move semantics의 핵심입니다.

 


2. std::move는 무엇인가?

먼저 결론부터 말하면 다음과 같습니다.

std::move는 객체를 이동시키는 함수가 아니라, 객체를 rvalue로 캐스팅하는 함수입니다.

 

즉, std::move 자체가 데이터를 옮기는 것은 아닙니다.
단지 컴파일러에게 이렇게 알려주는 역할을 합니다.

 

“이 객체는 이제 이동해도 괜찮은 값처럼 취급해줘.”

 

예를 들어 다음 코드를 보겠습니다.

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a = "hello";
    std::string b = std::move(a);

    std::cout << "a: " << a << '\n';
    std::cout << "b: " << b << '\n';
}

이 코드에서 std::move(a)는 a를 직접 비우거나, b로 문자열을 복사해주는 함수가 아닙니다.

a를 rvalue처럼 취급하게 만들고, 그 결과 std::string의 move constructor가 호출될 수 있도록 도와줍니다.

 


3. 복사와 이동의 차이

다음과 같은 클래스가 있다고 가정해보겠습니다.

#include <iostream>
#include <vector>

class Data {
public:
    std::vector<int> values;

    Data() {
        values = {1, 2, 3, 4, 5};
    }
};

이 객체를 복사하면 내부의 vector 데이터도 새로 복사해야 합니다.

Data a;
Data b = a;  // copy

하지만 이동은 다릅니다.

Data a;
Data b = std::move(a);  // move

이 경우 a가 가지고 있던 내부 자원을 b가 가져갈 수 있습니다.
데이터를 새로 복사하는 것보다 훨씬 효율적일 수 있습니다.

특히 std::vector, std::string, std::unique_ptr처럼 내부에 동적 자원을 가진 객체에서는 move가 성능상 유리할 수 있습니다.

 


4. std::move는 이동을 보장하지 않는다

여기서 중요한 포인트가 있습니다.

std::move를 썼다고 해서 반드시 move가 일어나는 것은 아닙니다.

 

예를 들어 클래스에 move constructor가 없으면 어떻게 될까요?

class MyClass {
public:
    MyClass() = default;

    MyClass(const MyClass&) {
        std::cout << "copy constructor\n";
    }
};

이 클래스는 copy constructor만 가지고 있습니다.

MyClass a;
MyClass b = std::move(a);

이 경우 std::move(a)를 사용했지만 move constructor가 없기 때문에 copy constructor가 호출될 수 있습니다.

즉, std::move는 “이동해라”라는 명령이 아니라, “이동 가능한 값처럼 취급해도 된다”는 힌트에 가깝습니다.

실제로 이동을 수행하는 것은 해당 타입의 move constructor 또는 move assignment operator입니다.

 


5. 이동 후 객체는 어떻게 될까?

많이 하는 오해 중 하나는 다음입니다.

std::move를 사용하면 원본 객체는 무조건 비어 있다.

 

항상 그렇지는 않습니다.

std::string a = "hello";
std::string b = std::move(a);

이후 a는 유효한 상태입니다.
다만 값이 무엇인지는 타입의 구현에 따라 달라질 수 있습니다.

즉, move 이후의 객체는:

valid but unspecified state

라고 표현합니다.

한국어로 풀면:

객체는 여전히 파괴하거나 다시 대입할 수 있는 유효한 상태지만, 내부 값이 무엇인지는 기대하면 안 된다.

따라서 move 이후에는 원본 객체의 값을 읽어서 로직에 사용하는 것을 피하는 것이 좋습니다.

std::string a = "hello";
std::string b = std::move(a);

// 권장하지 않음
if (a.empty()) {
    // 구현에 따라 결과가 달라질 수 있음
}

대신 move 이후의 객체는 다시 값을 대입해서 사용하는 것이 안전합니다.

a = "world";

 


6. std::move를 사용할 때 주의할 점

6-1. const 객체에는 move가 잘 안 된다

다음 코드를 보겠습니다.

const std::string a = "hello";
std::string b = std::move(a);

std::move(a)를 사용했지만, a는 const 객체입니다.

move는 원본 객체의 내부 자원을 가져오는 작업인데, const 객체는 수정할 수 없습니다.
따라서 이 경우 move가 아니라 copy가 발생할 가능성이 큽니다.

즉, const 객체에 std::move를 사용하는 것은 대부분 기대한 효과를 내지 못합니다.


6-2. return에서 무조건 std::move를 쓰지 말자

다음과 같은 코드도 자주 보입니다.

std::string createName() {
    std::string name = "eunchan";
    return std::move(name);
}

얼핏 보면 좋아 보이지만, 오히려 최적화를 방해할 수 있습니다.

현대 C++ 컴파일러는 return value optimization, 즉 RVO를 통해 불필요한 복사를 제거할 수 있습니다.

따라서 보통은 이렇게 쓰는 것이 좋습니다.

std::string createName() {
    std::string name = "eunchan";
    return name;
}

반환할 때 무조건 std::move를 붙이는 습관은 피하는 것이 좋습니다.


6-3. move 이후 객체를 다시 사용하지 말자

다음 코드는 위험한 습관입니다.

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);

std::cout << a.size() << '\n';

이 코드가 항상 크래시 나는 것은 아닙니다.
하지만 a의 상태를 기대하고 사용하는 것은 좋지 않습니다.

move 이후 객체는 소멸시키거나, 다시 대입해서 사용하는 정도로 생각하는 것이 안전합니다.

a = {4, 5, 6};  // 다시 대입 후 사용

 


7. unique_ptr에서 std::move가 필요한 이유

std::unique_ptr은 복사가 불가능합니다.

#include <memory>

std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = p1;  // 컴파일 에러

왜냐하면 unique_ptr은 하나의 객체만 자원을 소유해야 하기 때문입니다.

소유권을 넘기고 싶다면 std::move를 사용해야 합니다.

std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1);

이제 p2가 자원을 소유하고, p1은 더 이상 해당 자원을 소유하지 않습니다.

이 예시는 std::move가 단순 성능 최적화뿐 아니라, 소유권 이전을 표현하는 데에도 사용된다는 것을 보여줍니다.

 


8. 실무에서는 어떻게 봐야 할까?

실무에서 std::move는 단순히 “복사보다 빠른 문법”이 아닙니다.

다음과 같은 의미를 가집니다.

  1. 이 객체의 자원을 더 이상 현재 위치에서 사용하지 않겠다.
  2. 소유권을 다른 객체로 넘겨도 된다.
  3. 불필요한 복사를 피하고 싶다.
  4. move 이후 원본 객체의 값에 의존하지 않겠다.

그래서 std::move를 사용할 때는 항상 스스로 질문해보는 것이 좋습니다.

이 객체를 정말 더 이상 사용하지 않을 것인가?

이 질문에 확실히 “그렇다”고 답할 수 있을 때 std::move를 사용하는 것이 좋습니다.


9. 정리

std::move는 C++에서 매우 자주 사용되지만, 이름 때문에 오해하기 쉬운 기능입니다.

핵심만 정리하면 다음과 같습니다.

  • std::move는 객체를 직접 이동시키지 않는다.
  • std::move는 객체를 rvalue로 캐스팅한다.
  • 실제 이동은 move constructor 또는 move assignment operator가 수행한다.
  • move 이후 객체는 유효하지만 값은 기대하면 안 된다.
  • const 객체에 std::move를 써도 move가 안 될 수 있다.
  • return에서 무조건 std::move를 쓰는 것은 좋지 않다.
  • unique_ptr처럼 소유권 이전이 필요한 타입에서는 매우 중요하다.

결국 std::move는 성능 최적화 문법이면서 동시에 소유권 이전의 의도를 표현하는 도구입니다.

C++을 실무에서 다루려면 단순히 문법을 외우기보다, 객체의 생명주기와 소유권이 어떻게 이동하는지를 이해하는 것이 중요합니다.

728x90
반응형