핵심 정리
std::string a = "hello";
std::string b = std::move(a); // a의 자원을 b가 넘겨받을 수 있음
// a는 유효하지만 내용은 보장하지 않음, b는 "hello"
std::vector<std::string> v;
v.push_back(std::move(b)); // b의 소유권을 벡터로 이전문법
lvalue vs rvalue — 이동이 가능한 대상
lvalue는 이름이 있는, 이후에도 사용할 수 있는 값입니다. rvalue는 임시 값으로 이후에 사용하지 않습니다. 이동(move)은 "이 값은 더 이상 필요 없으니 내부 자원을 가져가도 된다"는 표현입니다.
std::string s = "hello"; // s는 lvalue — 이름이 있음
std::string t = s; // 복사 — s는 그대로 남음
std::string u = std::move(s); // 이동 — s의 자원을 u가 가져갈 수 있음
// s는 여전히 유효하지만 내용은 미지정 (파괴, 재할당은 가능)
// 임시 rvalue — 자동으로 이동
std::string make() { return std::string("world"); }
std::string v = make(); // 컴파일러가 자동으로 이동(또는 NRVO)std::move — 이동이 아니라 캐스팅
std::move는 실제로 이동을 수행하지 않습니다. 단지 lvalue를 rvalue 참조로 캐스팅해서 이동 생성자/이동 대입이 선택되도록 합니다. 실제 이동은 수신 측 생성자/대입 연산자가 수행합니다.
// std::move의 실제 동작 — 캐스팅만 함
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
// 이동이 의미 있는 타입 vs 없는 타입
std::string s = "hello";
int i = 42;
std::string t = std::move(s); // ✅ 이동 효과 있음 — 버퍼 이전
int j = std::move(i); // ⚠️ 이동 효과 없음 — int는 복사와 동일이동 생성자·이동 대입 연산자 — 소유권 이전 패턴
내부 자원을 가진 클래스는 이동 생성자와 이동 대입 연산자를 구현해야 불필요한 복사를 막을 수 있습니다.
class Buffer {
int* data_;
size_t size_;
public:
// 이동 생성자 — 소유권 이전, 원본을 nullptr로
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 원본이 double-free 하지 않도록
other.size_ = 0;
}
// 이동 대입 연산자 — 기존 자원 해제 후 이전
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // 기존 자원 해제
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
~Buffer() { delete[] data_; }
};이동 후 원본 상태 — 유효하지만 미지정
이동 후 원본 객체는 유효하지만 미지정(valid but unspecified) 상태입니다. 표준 라이브러리 타입도 "비어 있다"까지는 일반적으로 보장하지 않습니다. 파괴하거나 새 값을 대입하는 것은 안전하지만, 기존 내용을 기대하고 읽는 코드는 피해야 합니다.
std::vector<int> v{1, 2, 3};
std::vector<int> w = std::move(v);
std::cout << w.size(); // 3 ✅
std::cout << v.size(); // 구현에 따라 다를 수 있음 — 값 기대 금지
// ✅ 이동 후 재사용은 가능 (재할당 후)
v = {4, 5, 6}; // 다시 할당하면 정상 사용 가능체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 더 이상 필요 없는 lvalue를 전달 | std::move(val) |
| 함수 반환 값 | 컴파일러 NRVO/이동 자동 적용 |
| 이동 지원 클래스 작성 | 이동 생성자 + 이동 대입 noexcept |
| int, double 등 기본 타입 | std::move 효과 없음 (복사와 동일) |
| 이동 후 원본 상태 | 유효하지만 내용 불확실 — 재사용 전 재할당 |
주의할 점
std::move 후 원본을 읽으면 안 됩니다. 또한 const 객체에 std::move를 써도 이동이 일어나지 않습니다.
// ❌ 이동 후 원본 읽기
std::string s = "hello";
std::string t = std::move(s);
std::cout << s; // 미지정 상태 — "hello"가 아닐 수 있음
// ❌ const 객체에 std::move — 복사가 일어남 (이동 아님)
const std::string cs = "world";
std::string u = std::move(cs); // const T&& → const T& 오버로드 선택 → 복사
// ❌ 반환값에 불필요한 std::move — NRVO를 방해할 수 있음
std::string bad() {
std::string result = "hello";
return std::move(result); // 불필요한 pessimizing move 가능
}
// ✅ 그냥 반환 — 컴파일러가 NRVO 또는 이동 자동 선택
std::string good() {
std::string result = "hello";
return result;
}참고 링크
2 sources