[2026] C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
이 글의 핵심
이동 의미론(move semantics)은 C++11에서 추가된 기능이라, 예전 C++ 책이나 레거시 코드만 보다 오면 “복사만 있는 줄 알았는데, 이동이 뭐지?”라고 느낄 수 있습니다. 비유하면 이사할 때 가구를 통째로… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며: 벡터를 반환하면 복사가 너무 많다
“큰 벡터를 반환하면 프로그램이 느려요”
이동 의미론(move semantics)은 C++11에서 추가된 기능이라, 예전 C++ 책이나 레거시 코드만 보다 오면 “복사만 있는 줄 알았는데, 이동이 뭐지?”라고 느낄 수 있습니다. 비유하면 “이사할 때 가구를 통째로 들고 가는 것(이동)“과 “가구를 하나씩 복제해서 새 집에 놓는 것(복사)“의 차이입니다. 더 이상 쓰지 않는 객체는 복제할 필요 없이 소유권만 넘기면 되므로 이동이 훨씬 빠릅니다. 복사 vs 이동을 요약하면 아래와 같습니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
flowchart LR
subgraph copy[복사]
C1[원본] --> C2[데이터 복제]
C2 --> C3[대상]
C1 -.->|유지| C1
end
subgraph move[이동]
M1[원본] --> M2[포인터/핸들만 이전]
M2 --> M3[대상]
M1 -.->|빈 상태| M1
end
이 글에서는 왜 이동이 필요한지, lvalue(왼쪽에 올 수 있는 값—이름이 있거나 주소를 취할 수 있는 식)·rvalue(오른쪽에만 오는 값—임시 객체·리터럴 등, 이동 후 소멸해도 되는 식)를 왜 구분하는지, 실제 코드에서 어떻게 쓰면 되는지까지 단계별로 다룹니다.
스마트 포인터와 RAII는 “누가 버퍼를 소유하는가”를 객체로 묶고, 이동은 그 소유권을 O(1)로 넘기는 도구입니다. Rust의 소유권 이동은 기본이 이동이며 비싼 복사는 clone()으로 드러나는 점이 대비됩니다. 메모리 오남용·누수는 메모리 누수 가이드, Valgrind, 누수 탐지 실전에서 다룹니다.
함수에서 큰 벡터를 반환할 때 불필요한 복사가 발생했습니다.
이동 의미론은 “더 이상 쓰지 않는 객체의 자원(메모리·핸들)“을 복사하지 않고 넘겨서, 반환값·임시 객체·컨테이너 재할당 시 복사 비용을 크게 줄입니다. 컴파일러가 자동으로 이동을 적용하는 경우 RVO(Return Value Optimization—반환값을 복사/이동하지 않고 호출자 쪽 객체에 직접 만드는 컴파일러 최적화)가 반환 시 적용될 때도 많지만, 명시적으로 소유권을 넘길 때는 std::move를 쓰고, 이동 후 원본은 “유효하지만 unspecified” 상태로 두는 규칙을 지키는 것이 중요합니다.
문제의 코드 (C++03)에서는 vec가 100만 개 원소를 가진 벡터인데, return vec 시 복사 생성자가 호출되면 내부 버퍼 전체가 새 벡터로 복사됩니다. main에서 createLargeVector()의 반환값을 받을 때도 한 번 더 복사될 수 있어, 최대 두 번의 큰 메모리 복사가 발생합니다. C++03에는 이동이 없어서 “반환 시 자동 이동”도 없었고, 그래서 큰 컨테이너를 반환하는 것이 비용이 커서 참조 인자로 받는 관례가 많았습니다.
추가 문제 시나리오
시나리오 1: JSON 파싱 결과 전달
대용량 JSON을 파싱한 nlohmann::json 객체를 여러 함수에 전달할 때, 복사만 사용하면 메모리 사용량이 급증합니다. 이동을 활용하면 포인터만 넘기므로 메모리 효율이 좋아집니다.
시나리오 2: 스레드 풀 작업 큐
std::function이나 std::packaged_task를 큐에 넣을 때 복사하면 내부 캡처된 객체까지 복사됩니다. std::move로 이동하면 캡처된 큰 벡터·맵을 복제하지 않고 큐로 넘길 수 있습니다.
시나리오 3: 네트워크 버퍼 전달
수신한 std::vector<uint8_t> 버퍼를 파싱 함수로 넘길 때, 복사 시 패킷 크기만큼 메모리 할당과 복사가 발생합니다. 이동으로 넘기면 O(1)에 전달할 수 있습니다.
시나리오 4: 빌더 패턴에서 객체 조립
빌더가 여러 단계에서 std::string, std::vector 등을 누적한 뒤 최종 객체를 반환할 때, 이동을 쓰지 않으면 각 단계마다 복사가 발생합니다.
Move로 해결 (C++11):
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::vector<int> createLargeVector() {
std::vector<int> vec(1000000);
// 데이터 채우기...
return vec; // ✅ 이동 (복사 없음)
}
int main() {
std::vector<int> data = createLargeVector(); // ✅ 이동
// 복사 없음, 포인터만 이동
}
같은 코드인데 왜 다르게 동작할까?
소스 코드는 동일합니다. 차이는 언어 규칙입니다. C++11 표준에서는 “함수 반환 시 곧 파괴될 지역 객체”를 자동으로 rvalue로 취급하도록 정했습니다. 그래서 return vec;에서 복사 생략(RVO, Return Value Optimization)이 적용되지 않더라도, 오버로드 해석 시 이동 생성자가 선택됩니다. C++03에는 이동이 없으므로 복사 생성자만 호출되던 것과 대비되는 부분입니다. 즉, std::move를 붙이지 않아도 같은 코드를 C++11로 컴파일하면 컴파일러가 이동을 사용합니다.
이 글을 읽으면:
- 이동 의미론의 개념을 이해할 수 있습니다.
- lvalue와 rvalue의 차이를 알 수 있습니다.
- std::move를 올바르게 사용할 수 있습니다.
- 실전에서 성능을 최적화할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- lvalue와 rvalue
- rvalue 참조
- 이동 생성자와 이동 대입
- std::move
- 실전 최적화
- 완전한 이동 의미론 예제
- 자주 발생하는 에러와 해결법
- 성능 벤치마크
- 프로덕션 패턴
1. lvalue와 rvalue
기본 개념
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o lvalue_rvalue lvalue_rvalue.cpp && ./lvalue_rvalue
#include <iostream>
int main() {
int x = 10; // x는 lvalue (이름 있음, 주소 있음)
int y = 20; // y는 lvalue
int z = x + y; // x + y는 rvalue (임시 값, 주소 없음)
int* p = &x; // ✅ OK: lvalue의 주소
std::cout << z << " " << *p << "\n";
return 0;
}
실행 결과: 30 10 (z와 *p 값)이 한 줄로 출력됩니다.
lvalue:
- 이름이 있는 변수
- 주소를 가질 수 있음
- 대입 연산자 왼쪽에 올 수 있음 rvalue:
- 임시 값
- 주소를 가질 수 없음
- 대입 연산자 오른쪽에만 처음에는 “이름 있음/없음”이 왜 중요하지? 싶을 수 있습니다. 나중에 나오는 이동은 “이 값은 곧 버려질 거니까, 복사하지 말고 가져가도 돼”라고 컴파일러에게 알려 주는 것인데, 그걸 구분하는 기준이 바로 lvalue/rvalue입니다. 여기서는 일단 “lvalue = 장소가 정해진 값, rvalue = 임시 값” 정도만 기억해 두면 됩니다.
왜 lvalue와 rvalue를 구분하나요?
실생활 비유: lvalue는 “내 책상 위 컵”처럼 위치가 정해진 것입니다. 누군가 가져가면 원래 자리는 비게 됩니다. rvalue는 “택배 상자 안 물건”처럼 이동해도 상관없는 임시입니다. C++에서는 “이동해도 되는 값(rvalue)“을 구분해 두면, 복사 대신 포인터만 바꾸는 이동을 적용할 수 있어 성능이 좋아집니다. 그래서 T&&(rvalue 참조)로 “이 값은 이동해도 된다”고 표시하고, 이동 생성자·이동 대입만 그 타입으로 받도록 합니다. 요약하면, lvalue/rvalue 구분이 곧 이동 문법의 기반이라고 보면 됩니다.
함수 반환값
함수 반환값은 대부분 rvalue입니다(임시 객체이기 때문). 그래서 getValue()처럼 값 반환 함수는 rvalue이고, 반환된 임시를 받는 쪽에서 이동을 활용할 수 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
int getValue() {
return 42;
}
int& getRef() {
static int x = 10;
return x;
}
int main() {
int a = getValue(); // getValue()는 rvalue
int& b = getRef(); // getRef()는 lvalue
// getValue() = 100; // ❌ 에러: rvalue
getRef() = 100; // ✅ OK: lvalue
}
2. rvalue 참조
기본 문법
일반 참조(T&)는 lvalue에만 붙일 수 있습니다. rvalue 참조(T&&) 는 “임시 값이나 곧 파괴될 값”에만 붙일 수 있게 만든 타입입니다. 이렇게 구분해 두어야, “이 인자는 이동해도 된다”는 뜻으로 T&&를 쓰고, 그때만 이동 생성자·이동 대입을 호출할 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
int x = 10;
int& lref = x; // lvalue 참조
// int& lref2 = 42; // ❌ 에러: rvalue를 lvalue 참조로
int&& rref = 42; // ✅ rvalue 참조
// int&& rref2 = x; // ❌ 에러: lvalue를 rvalue 참조로
코드 설명:
int& lref = x;: 일반 참조는 이름 있는 변수(lvalue)에만 붙일 수 있습니다.int& lref2 = 42;: 42는 임시 값(rvalue)이므로 일반 참조로 받을 수 없어 컴파일 에러가 발생합니다.int&& rref = 42;: rvalue 참조(&&)는 임시 값(rvalue)을 받을 수 있습니다.int&& rref2 = x;: x는 lvalue이므로 rvalue 참조로 받을 수 없어 컴파일 에러가 발생합니다.
const lvalue 참조
const T&는 예전부터 “임시 값도 받을 수 있는 참조”로 많이 썼습니다. 다만 const이기 때문에 수정·이동이 불가능합니다. 그래서 “값을 가져오기만 하고 이동은 하지 않을 때” 쓰는 용도이고, 이동이 목적이면 T&&를 써야 합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
const int& ref1 = 10; // ✅ OK: const lvalue 참조는 rvalue 받을 수 있음
const int& ref2 = x; // ✅ OK: lvalue도 받을 수 있음
// 하지만 수정 불가
// ref1 = 20; // ❌ 에러
오버로딩
같은 이름의 함수를 T&와 T&&로 오버로딩하면, 호출 시 전달된 값이 lvalue인지 rvalue인지에 따라 다른 함수가 선택됩니다. 아래처럼 쓰면 “lvalue면 복사, rvalue면 이동”처럼 분기할 수 있어서, 나중에 템플릿과 perfect forwarding을 할 때도 같은 아이디어가 쓰입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void process(int& x) {
std::cout << "lvalue: " << x << "\n";
}
void process(int&& x) {
std::cout << "rvalue: " << x << "\n";
}
int main() {
int a = 10;
process(a); // lvalue: 10
process(20); // rvalue: 20
}
코드 설명:
process(int& x): lvalue를 받는 버전입니다. 이름 있는 변수가 전달되면 이 함수가 호출됩니다.process(int&& x): rvalue를 받는 버전입니다. 임시 값이나 리터럴이 전달되면 이 함수가 호출됩니다.process(a):a는 이름 있는 변수(lvalue)이므로 첫 번째 함수가 호출됩니다.process(20):20은 임시 값(rvalue)이므로 두 번째 함수가 호출됩니다.- 이런 오버로딩을 통해 lvalue는 복사, rvalue는 이동처럼 다르게 처리할 수 있습니다.
3. 이동 생성자와 이동 대입
복사 vs 이동
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Buffer {
int* data;
size_t size;
public:
// 생성자
Buffer(size_t s) : size(s), data(new int[s]) {
std::cout << "Constructor\n";
}
// 복사 생성자 (느림)
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
std::cout << "Copy constructor\n";
}
// 이동 생성자 (빠름)
Buffer(Buffer&& other) noexcept
: size(other.size), data(other.data) {
other.data = nullptr;
other.size = 0;
std::cout << "Move constructor\n";
}
~Buffer() {
delete[] data;
}
};
int main() {
Buffer b1(1000);
Buffer b2 = b1; // Copy constructor (복사)
Buffer b3 = std::move(b1); // Move constructor (이동)
}
코드 상세 설명: 복사 생성자 (느림):
data(new int[other.size]): 새로운 메모리를 할당합니다. 1000개 int면 4KB 할당.std::copy(...): 원본의 모든 데이터를 새 메모리로 복사합니다. 1000개 int를 하나씩 복사하므로 느립니다.- 결과: 원본과 복사본이 각자 독립적인 메모리를 가집니다. 이동 생성자 (빠름):
data(other.data): 원본의 포인터만 복사합니다. 메모리 할당 없음!size(other.size): 크기 정보도 복사합니다.other.data = nullptr: 핵심! 원본의 포인터를 nullptr로 설정합니다.other.size = 0: 원본의 크기도 0으로 설정합니다.- 결과: 포인터만 옮기므로 매우 빠름 (O(1)). 원본은 빈 상태가 됩니다.
왜
other.data = nullptr가 필수인가?: - 이동 후에도
other의 소멸자는 호출됩니다. - 소멸자에서
delete[] data를 실행하는데, nullptr로 설정하지 않으면 같은 메모리를 두 번 해제하는 버그가 발생합니다. delete[] nullptr는 안전하게 아무 일도 하지 않습니다.noexcept의 중요성:std::vector는 재할당 시 이동 생성자가noexcept이면 이동을 사용하고, 아니면 복사를 사용합니다.- 이동 중 예외가 발생하면 일부만 옮겨진 상태가 되어 복구가 어렵기 때문입니다.
왜
other.data = nullptr를 넣을까요?
이동 후에도other는 소멸자가 호출됩니다. 그때delete[] data가 실행되므로,other.data를 그대로 두면 같은 메모리를 두 번 해제하는 버그가 납니다. 그래서 이동한 쪽은 포인터를 가져가고, 원본은nullptr로 바꿔 두어 “이 객체는 더 이상 그 리소스를 소유하지 않는다”고 만드는 것이 필수입니다.
이동 대입 연산자
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Buffer {
int* data;
size_t size;
public:
// 이동 대입 연산자
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
// 기존 리소스 해제
delete[] data;
// 리소스 이동
data = other.data;
size = other.size;
// 원본 무효화
other.data = nullptr;
other.size = 0;
std::cout << "Move assignment\n";
}
return *this;
}
};
코드 상세 설명:
1. 자기 대입 검사 (if (this != &other)):
a = std::move(a);같은 자기 대입을 방지합니다.- 자기 대입 시 자신의 리소스를 해제한 후 다시 가져오려 하면 문제가 발생합니다.
2. 기존 리소스 해제 (
delete[] data;): - 이동 대입은 이미 존재하는 객체에 대입하는 것이므로, 기존에 가지고 있던 메모리를 먼저 해제해야 합니다.
- 이동 생성자와의 차이점: 생성자는 새 객체를 만드는 것이므로 기존 리소스가 없습니다. 3. 리소스 이동:
data = other.data;: 원본의 포인터를 가져옵니다.size = other.size;: 크기 정보도 가져옵니다. 4. 원본 무효화:other.data = nullptr;: 원본이 소멸될 때 이미 이동한 메모리를 해제하지 않도록 합니다.other.size = 0;: 원본을 빈 상태로 만듭니다. 이동 생성자 vs 이동 대입 연산자: | 항목 | 이동 생성자 | 이동 대입 연산자 | |------|-------------|------------------| | 호출 시점 | 새 객체 생성 시 | 기존 객체에 대입 시 | | 기존 리소스 | 없음 | 해제 필요 | | 예시 |Buffer b = std::move(a);|b = std::move(a);|
Rule of Five (다섯 가지 규칙)
동적 메모리, 파일 핸들, 소켓처럼 스스로 관리하는 리소스가 있는 클래스를 만들 때는, 복사/이동을 명시적으로 정의하거나 막아야 합니다. 이때 함께 고려하는 다섯 가지를 Rule of Five(다섯 가지 규칙)라고 부릅니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Resource {
public:
// 1. 소멸자
~Resource();
// 2. 복사 생성자
Resource(const Resource& other);
// 3. 복사 대입 연산자
Resource& operator=(const Resource& other);
// 4. 이동 생성자
Resource(Resource&& other) noexcept;
// 5. 이동 대입 연산자
Resource& operator=(Resource&& other) noexcept;
};
4. std::move
기본 사용법
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::string str1 = "Hello";
std::string str2 = std::move(str1); // str1의 내용을 str2로 이동
std::cout << "str1: " << str1 << "\n"; // "" (비어있음)
std::cout << "str2: " << str2 << "\n"; // "Hello"
주의: std::move는 실제로 이동하지 않습니다. lvalue를 rvalue로 캐스팅만 할 뿐입니다. 실제 “이동”은 이동 생성자나 이동 대입 연산자가 그 rvalue를 받을 때 일어납니다. 예: std::string str2 = std::move(str1);에서 move(str1)은 “str1을 rvalue처럼 취급해 달라”는 신호만 주고, str2의 이동 생성자가 str1의 내부 버퍼를 가져가며 이동이 수행됩니다.
또 한 가지: 한 번 이동한 객체는 “값을 빼앗긴 상태”이므로, 그 객체를 다시 사용하지 말고, 필요하면 새로 대입해서 쓰는 것이 안전합니다. 같은 변수에 std::move를 여러 번 쓸 수도 있지만, 이미 비어 있는 객체를 또 이동하는 꼴이 되어 혼란만 커집니다.
컨테이너에서 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::vector<std::string> vec1 = {"a", "b", "c"};
std::vector<std::string> vec2 = std::move(vec1);
std::cout << "vec1 size: " << vec1.size() << "\n"; // 0
std::cout << "vec2 size: " << vec2.size() << "\n"; // 3
함수 인자로 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void process(std::string str) {
std::cout << str << "\n";
}
int main() {
std::string text = "Hello";
process(text); // 복사
process(std::move(text)); // 이동
// text는 이제 비어있음
std::cout << "text: " << text << "\n"; // ""
}
unique_ptr 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// std::unique_ptr<int> ptr2 = ptr1; // ❌ 에러: 복사 불가
std::unique_ptr<int> ptr2 = std::move(ptr1); // ✅ 이동
if (!ptr1) {
std::cout << "ptr1 is null\n";
}
std::cout << "*ptr2 = " << *ptr2 << "\n"; // 42
5. 실전 최적화
패턴 1: 벡터에 추가
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::vector<std::string> names;
std::string name = "Alice";
// ❌ 복사
names.push_back(name);
// ✅ 이동
names.push_back(std::move(name));
// ✅ 더 나은 방법: emplace_back
names.emplace_back("Bob");
패턴 2: swap 구현
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
template <typename T>
void mySwap(T& a, T& b) {
T temp = std::move(a); // a를 temp로 이동
a = std::move(b); // b를 a로 이동
b = std::move(temp); // temp를 b로 이동
}
코드 상세 설명: 전통적인 swap (복사 사용): 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
T temp = a; // 복사 생성자 호출
a = b; // 복사 대입 연산자 호출
b = temp; // 복사 대입 연산자 호출
// 총 3번의 복사 발생!
이동을 사용한 swap (훨씬 빠름):
T temp = std::move(a);: a를 rvalue로 캐스팅하여 이동 생성자 호출. a의 내용이 temp로 이동하고 a는 빈 상태.a = std::move(b);: b를 rvalue로 캐스팅하여 이동 대입 연산자 호출. b의 내용이 a로 이동하고 b는 빈 상태.b = std::move(temp);: temp를 rvalue로 캐스팅하여 이동 대입 연산자 호출. temp의 내용(원래 a의 내용)이 b로 이동.- 결과: 포인터만 3번 교환하므로 매우 빠름. 큰 벡터나 문자열을 swap할 때 성능 차이가 큽니다. 성능 비교 (1MB 벡터 swap):
- 복사 사용: 3MB 복사 (매우 느림)
- 이동 사용: 포인터 3개 교환 (매우 빠름)
패턴 3: 반환값 최적화
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ RVO (Return Value Optimization)
std::vector<int> createVector() {
std::vector<int> vec(1000);
return vec; // 이동 (또는 RVO)
}
// ❌ std::move 불필요
std::vector<int> createVector2() {
std::vector<int> vec(1000);
return std::move(vec); // 불필요! RVO 방해
}
코드 상세 설명: RVO (Return Value Optimization)란?:
- 컴파일러가 반환값을 복사/이동 없이 호출자의 메모리에 직접 생성하는 최적화입니다.
- 예:
std::vector<int> result = createVector();에서vec이result의 위치에 바로 만들어집니다. 왜return std::move(vec);가 나쁜가?:
- RVO 조건:
- 반환하는 객체가 지역 변수이고
- 반환 타입과 같은 타입이면
- 컴파일러가 RVO를 적용할 수 있습니다.
return vec;의 동작 (좋음):- 컴파일러가 RVO를 시도합니다.
- RVO가 불가능하면 자동으로 이동합니다.
- 최선: 복사/이동 없음 (RVO)
- 차선: 이동 1번
return std::move(vec);의 동작 (나쁨):std::move(vec)는 rvalue이므로 RVO 조건을 깨뜨립니다.- 컴파일러가 RVO를 적용할 수 없게 됩니다.
- 무조건 이동 1번 발생
- RVO 기회를 날려버림!
결론: 지역 변수를 반환할 때는
std::move없이 그냥return vec;만 쓰세요. 컴파일러가 알아서 최적화합니다.return vec;만 쓰면 컴파일러가 반환값을 받을 자리에 바로 만들 수 있어서(RVO) 복사·이동 자체를 없앨 수 있습니다. 반면return std::move(vec);를 쓰면 “이미 만든 vec을 이동해서 반환하라”고 컴파일러에 강제해 버려서, RVO 조건을 깨고 오히려 이동 한 번을 강제하게 됩니다. 그래서 지역 객체를 그대로 반환할 때는std::move를 붙이지 않는 것이 좋습니다.
패턴 4: 멤버 초기화
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Container {
std::vector<int> data;
public:
// ✅ 이동 생성자 활용
Container(std::vector<int>&& vec) : data(std::move(vec)) {}
// 또는
void setData(std::vector<int>&& vec) {
data = std::move(vec);
}
};
int main() {
std::vector<int> temp = {1, 2, 3};
Container c(std::move(temp));
}
코드 상세 설명:
왜 vec이 이미 rvalue 참조인데 또 std::move를 써야 하나?:
이것이 많은 사람들이 헷갈려하는 부분입니다!
- 함수 매개변수는 항상 lvalue:
Container(std::vector<int>&& vec) // vec는 rvalue 참조 타입vec의 타입은 rvalue 참조(std::vector<int>&&)입니다.- 하지만
vec자체는 이름이 있는 변수이므로 lvalue입니다! - 함수 내부에서
vec를 그냥 쓰면 lvalue로 취급됩니다.
- 초기화 리스트에서의 동작:
: data(vec) // ❌ 복사! vec는 lvalue이므로 : data(std::move(vec)) // ✅ 이동! vec를 rvalue로 캐스팅 - 전체 흐름:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
std::vector<int> temp = {1, 2, 3};
Container c(std::move(temp)); // 1. temp를 rvalue로 캐스팅
// ↓ Container 생성자 호출
Container(std::vector<int>&& vec) // 2. vec는 rvalue 참조 타입이지만 lvalue
: data(std::move(vec)) // 3. vec를 다시 rvalue로 캐스팅하여 이동
핵심 규칙:
- rvalue 참조 타입(
T&&)은 “이동 가능한 값을 받는다”는 의미 - 하지만 이름 있는 변수는 항상 lvalue
- 실제로 이동하려면
std::move로 다시 rvalue로 캐스팅해야 함 이것이 Perfect Forwarding에서std::forward를 쓰는 이유이기도 합니다!
패턴 5: 조건부 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::string processString(std::string str, bool modify) {
if (modify) {
str += " modified";
return str; // 이동
}
return str; // 이동
}
int main() {
std::string text = "Hello";
std::string result = processString(std::move(text), true);
}
6. 완전한 이동 의미론 예제
예제 1: 리소스 관리 클래스 (Rule of Five 완전 구현)
동적 메모리, 파일 핸들 등을 관리하는 클래스의 이동 의미론을 완전히 구현한 예제입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <algorithm>
#include <utility>
class ManagedBuffer {
int* data_;
size_t size_;
public:
// 기본 생성자
explicit ManagedBuffer(size_t size) : size_(size), data_(new int[size]) {
std::fill(data_, data_ + size_, 0);
std::cout << "Constructor(" << size_ << ")\n";
}
// 복사 생성자
ManagedBuffer(const ManagedBuffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructor\n";
}
// 이동 생성자
ManagedBuffer(ManagedBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move constructor\n";
}
// 복사 대입 연산자
ManagedBuffer& operator=(const ManagedBuffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy assignment\n";
}
return *this;
}
// 이동 대입 연산자
ManagedBuffer& operator=(ManagedBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move assignment\n";
}
return *this;
}
~ManagedBuffer() {
delete[] data_;
std::cout << "Destructor\n";
}
};
int main() {
ManagedBuffer a(100);
ManagedBuffer b = std::move(a); // Move constructor
ManagedBuffer c(50);
c = std::move(b); // Move assignment
}
실행 결과: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Constructor(100)
Move constructor
Constructor(50)
Move assignment
Destructor
Destructor
Destructor
예제 2: 빌더 패턴에서 이동 활용
여러 단계에서 데이터를 누적한 뒤 최종 객체를 반환하는 빌더 패턴입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <vector>
#include <utility>
class ConfigBuilder {
std::string name_;
std::vector<std::string> tags_;
std::vector<int> values_;
public:
ConfigBuilder& setName(std::string name) {
name_ = std::move(name); // 호출자가 넘긴 임시/이동 가능 값 활용
return *this;
}
ConfigBuilder& addTag(std::string tag) {
tags_.push_back(std::move(tag));
return *this;
}
ConfigBuilder& addValue(int value) {
values_.push_back(value);
return *this;
}
// 최종 Config 객체 반환 - 이동으로 효율적
struct Config {
std::string name;
std::vector<std::string> tags;
std::vector<int> values;
};
Config build() {
return Config{
std::move(name_),
std::move(tags_),
std::move(values_)
};
}
};
int main() {
ConfigBuilder builder;
builder.setName("my-service")
.addTag("production")
.addTag("v1")
.addValue(42)
.addValue(100);
auto config = builder.build(); // 모든 멤버가 이동으로 전달
}
예제 3: 팩토리 함수와 이동
팩토리에서 생성한 객체를 호출자에게 효율적으로 전달하는 패턴입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <vector>
#include <string>
// unique_ptr 반환: 소유권 이전의 표준 패턴
std::unique_ptr<std::vector<int>> createFilteredVector(
const std::vector<int>& source, int threshold) {
auto result = std::make_unique<std::vector<int>>();
for (int x : source) {
if (x > threshold) result->push_back(x);
}
return result; // RVO 또는 이동
}
// 값 반환: RVO/이동으로 복사 없음
std::vector<std::string> loadLines(const std::string& path) {
std::vector<std::string> lines;
// 파일에서 읽어 lines에 추가...
return lines; // std::move 불필요, 컴파일러가 최적화
}
int main() {
std::vector<int> data = {1, 5, 10, 15, 20};
auto filtered = createFilteredVector(data, 8); // 이동
auto lines = loadLines("config.txt"); // RVO 또는 이동
}
주의사항
이동 후 사용 금지
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::string str = "Hello";
std::string str2 = std::move(str);
// ❌ 위험: 이동된 객체 사용
std::cout << str << "\n"; // 정의되지 않은 동작(undefined behavior, UB) (보통 빈 문자열)
// ✅ 재할당은 OK
str = "World";
std::cout << str << "\n"; // "World"
이동 후 원본은 “유효하지만 unspecified(규격에서 값을 지정하지 않음)” 상태입니다. 표준이 값을 보장하지 않으므로 어떤 구현이든 올 수 있습니다. 다만 std::string처럼 표준 라이브러리 타입은 대부분 이동 후 빈 문자열로 두도록 명시해 두었기 때문에, 실무에서는 빈 문자열이 나오는 경우가 많습니다. 그래도 “이동한 뒤에는 쓰지 않는다”고 생각하고 코드를 짜는 것이 안전합니다.
noexcept 중요성
noexcept는 “이 함수는 예외를 던지지 않는다”고 컴파일러에 알려 주는 C++11 키워드입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class MyClass {
public:
// ✅ noexcept 지정 (중요!)
MyClass(MyClass&& other) noexcept {
// ...
}
MyClass& operator=(MyClass&& other) noexcept {
// ...
return *this;
}
};
이유: std::vector는 재할당 시 기존 원소를 새 버퍼로 옮길 때, 이동 생성자가 noexcept(예외를 던지지 않음)이면 이동을 쓰고, 아니면 복사를 씁니다. 이동 중 예외가 나면 “일부만 옮겨진” 상태가 되어 복구하기 어렵기 때문입니다. 따라서 이동이 예외를 던지지 않는다면 반드시 noexcept를 붙이는 것이 좋습니다.
예시: std::vector<MyClass>에 원소를 많이 넣어 재할당이 일어나면, 컴파일러는 MyClass의 이동 생성자 선언을 보고 noexcept 여부를 확인합니다. noexcept이면 원소마다 이동 생성자만 호출하고, 아니면 복사 생성자를 써서 더 느려질 수 있습니다.
const rvalue 참조 금지
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 나쁜 예
void process(const std::string&& str) {
// const이므로 이동 불가!
}
// ✅ 좋은 예
void process(std::string&& str) {
// 이동 가능
}
7. 자주 발생하는 에러와 해결법
에러 1: 이동 후 원본 사용 (Use-After-Move)
증상: 이동한 객체를 다시 사용하면 빈 값, 크래시, 또는 정의되지 않은 동작(UB) 발생.
원인: std::move 후 원본이 “유효하지만 unspecified” 상태인데 사용.
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
std::vector<int> vec = {1, 2, 3};
std::vector<int> other = std::move(vec);
vec.push_back(4); // 위험: vec는 비어 있거나 불안정한 상태
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
std::vector<int> vec = {1, 2, 3};
std::vector<int> other = std::move(vec);
// vec 사용 금지. 필요하면 새로 할당:
vec = {1, 2, 3, 4}; // 또는 vec.clear(); vec.push_back(4);
정적 분석 도구: Clang-Tidy의 bugprone-use-after-move 체크로 검출 가능.
에러 2: return std::move(vec)로 RVO 방해
증상: 반환값 최적화가 적용되지 않아 불필요한 이동 1회 발생.
원인: 지역 변수를 반환할 때 std::move를 붙이면 RVO 조건이 깨짐.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
std::vector<int> create() {
std::vector<int> vec(1000);
return std::move(vec); // RVO 방해!
}
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
std::vector<int> create() {
std::vector<int> vec(1000);
return vec; // RVO 또는 이동, 컴파일러가 최적화
}
에러 3: rvalue 참조 매개변수에서 std::move 누락
증상: 이동을 의도했는데 복사가 발생.
원인: T&& 매개변수는 이름이 있으므로 lvalue. std::move로 다시 rvalue로 캐스팅해야 함.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
class Wrapper {
std::vector<int> data;
public:
Wrapper(std::vector<int>&& vec) : data(vec) {} // 복사 발생!
};
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
class Wrapper {
std::vector<int> data;
public:
Wrapper(std::vector<int>&& vec) : data(std::move(vec)) {}
};
에러 4: 이동 생성자에서 noexcept 누락
증상: std::vector 재할당 시 이동 대신 복사가 사용되어 성능 저하.
원인: std::vector는 재할당 시 이동 생성자가 noexcept일 때만 이동 사용.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
class MyClass {
public:
MyClass(MyClass&& other) { // noexcept 없음
// ...
}
};
// std::vector<MyClass> 재할당 시 복사 사용 → 느림
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// ...
}
};
에러 5: std::move를 const 객체에 적용
증상: 이동이 의도대로 동작하지 않음 (복사가 호출됨).
원인: std::move는 static_cast<T&&>로 캐스팅만 함. const T를 T&&로 바꿔도 이동 생성자는 const T&를 받을 수 없어 복사 생성자가 선택됨.
// ❌ 잘못된 코드
const std::string str = "Hello";
std::string other = std::move(str); // 복사! (const이므로 이동 불가)
해결법:
// ✅ 올바른 코드
std::string str = "Hello";
std::string other = std::move(str); // 이동
에러 6: std::move를 기본 타입에 사용
증상: 불필요한 코드, 의미 없음.
원인: int, double 등 기본 타입은 복사가 비용이 거의 없고, 이동 의미론이 없음.
// ❌ 불필요
int x = 42;
int y = std::move(x); // 복사와 동일, std::move 의미 없음
해결법:
// ✅ 기본 타입은 그냥 복사
int x = 42;
int y = x;
에러 7: 자기 대입 검사 누락 (이동 대입)
증상: a = std::move(a); 시 자기 리소를 해제한 뒤 다시 가져오려 하면 문제.
원인: 이동 대입 연산자에서 this != &other 검사 누락.
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
Buffer& operator=(Buffer&& other) noexcept {
delete[] data; // 이게 자기 자신이면 data 해제됨
data = other.data; // other.data도 이미 해제됨
other.data = nullptr;
return *this;
}
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
8. 성능 벤치마크
벤치마크 1: vector 복사 vs 이동
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <vector>
#include <string>
#include <iostream>
void benchmarkVectorCopyVsMove() {
const size_t count = 10000;
const size_t strSize = 1000;
auto start = std::chrono::high_resolution_clock::now();
std::vector<std::string> vec1(count, std::string(strSize, 'x'));
std::vector<std::string> vec2 = vec1; // 복사
auto copyUs = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now() - start).count();
start = std::chrono::high_resolution_clock::now();
std::vector<std::string> vec3(count, std::string(strSize, 'x'));
std::vector<std::string> vec4 = std::move(vec3); // 이동
auto moveUs = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now() - start).count();
std::cout << "Copy: " << copyUs << " μs, Move: " << moveUs << " μs, Speedup: "
<< (double)copyUs / moveUs << "x\n";
}
예상 결과: 복사 550ms, 이동 0.010.5ms. 이동이 10~100배 이상 빠른 경우가 많습니다.
벤치마크 2: push_back vs emplace_back vs move
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// push_back(copy): 매번 복사
for (int i = 0; i < 100000; ++i) {
std::string s(100, 'x');
v.push_back(s); // 복사
}
// push_back(move): 매번 이동
for (int i = 0; i < 100000; ++i) {
std::string s(100, 'x');
v.push_back(std::move(s)); // 이동
}
// emplace_back: 직접 생성 (가장 빠름)
for (int i = 0; i < 100000; ++i) {
v.emplace_back(100, 'x');
}
대략적인 순서: emplace_back ≤ push_back(move) < push_back(copy). swap은 복사 기반 대비 이동이 수십~수백 배 빠릅니다.
벤치마크 요약 표
| 연산 | 복사 비용 | 이동 비용 | 비고 |
|---|---|---|---|
| vector 대입 (10K 문자열) | O(n) 메모리 복사 | O(1) 포인터 교환 | 이동이 10~100배 빠름 |
| swap (1MB 벡터) | 3MB 복사 | 포인터 3개 교환 | 이동이 수백 배 빠름 |
| push_back (10만 회) | 매번 복사 | 매번 이동 | emplace_back이 가장 빠름 |
| 함수 반환 (vector) | 복사 또는 RVO | 이동 또는 RVO | return vec; 권장 |
| 실제 수치는 CPU, 메모리 속도, 컴파일러에 따라 달라집니다. 중요한 것은 이동이 복사보다 비용이 훨씬 작다는 점입니다. 큰 컨테이너나 리소스를 넘길 때 이동을 쓰면 체감 성능이 크게 나아질 수 있습니다. |
9. 프로덕션 패턴
패턴 1: Pimpl + 이동
Pimpl(pointer to implementation)에서 구현체를 이동할 때는 unique_ptr을 이동합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// widget.hpp
#include <memory>
class Widget {
struct Impl;
std::unique_ptr<Impl> pImpl;
public:
Widget();
Widget(Widget&&) noexcept = default;
Widget& operator=(Widget&&) noexcept = default;
// 복사는 명시적으로 삭제하거나 별도 구현
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
};
패턴 2: 작업 큐에 이동으로 전달
스레드 풀에 작업을 넣을 때 std::packaged_task나 std::function을 이동으로 전달합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <queue>
#include <mutex>
#include <future>
#include <functional>
class TaskQueue {
std::queue<std::function<void()>> queue_;
std::mutex mutex_;
public:
template<typename F>
void submit(F&& f) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::forward<F>(f)); // Perfect forwarding
}
void submit(std::packaged_task<int()> task) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push([t = std::move(task)]() mutable { t(); }); // 이동 필수
}
};
패턴 3: 반환값 최적화 체크리스트
프로덕션에서 반환값을 다룰 때:
- 지역 변수 반환:
return vec;(std::move 불필요) - 복합 타입:
return {a, std::move(b)};(멤버별 이동) - 조건부 반환:
return condition ? a : b;(둘 다 같은 타입이면 RVO 가능) -
unique_ptr반환:return ptr;(이동 또는 RVO)
패턴 4: 이동 가능한 타입 설계
커스텀 타입을 만들 때 이동을 지원하는 체크리스트:
- 이동 생성자:
T(T&&) noexcept - 이동 대입:
T& operator=(T&&) noexcept - 이동 후 원본:
other.ptr = nullptr등으로 무효화 -
noexcept지정 (vector 등 STL 호환) - 자기 대입 검사 (이동 대입)
패턴 5: API 설계 시 이동 vs 복사
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 값으로 받고 이동: 호출자가 복사/이동 선택
void process(std::string data) {
storage_.push_back(std::move(data));
}
// ✅ rvalue만 받을 때: 이동만 허용
void takeOwnership(std::unique_ptr<Resource> ptr) {
resource_ = std::move(ptr);
}
// ✅ const 참조: 복사만, 이동 불가
void readOnly(const std::string& s) {
// s는 수정/이동 불가
}
선택 가이드:
- 호출자가 소유권을 넘기고 싶을 때:
T또는T&& - 읽기만 할 때:
const T& - 소유권 이전만 허용:
std::unique_ptr<T>또는T&&
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
이 글에서 다루는 키워드 (관련 검색어)
C++ 이동 의미론, move semantics, rvalue reference, std::move, 복사 vs 이동, 이동 생성자 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 설명 |
|---|---|
| lvalue | 이름 있는 변수 |
| rvalue | 임시 값 |
| rvalue 참조 | T&& |
| std::move | lvalue를 rvalue로 캐스팅 |
| 이동 생성자 | T(T&& other) noexcept |
| 이동 대입 | T& operator=(T&& other) noexcept |
| noexcept | 필수 (vector 최적화) |
| 핵심 원칙: |
- 큰 객체는 이동 활용
- 이동 후 객체 사용 금지
- noexcept 지정 필수
- 반환값에 std::move 불필요 (RVO)
- unique_ptr은 항상 이동
구현 체크리스트
- 이동 생성자·이동 대입에
noexcept지정 - rvalue 참조 매개변수에서
std::move로 멤버 초기화 - 이동 대입 시
this != &other자기 대입 검사 - 지역 변수 반환 시
return vec;(std::move 사용 금지) - 이동 후 원본 사용 금지
- Clang-Tidy
bugprone-use-after-move검사 활용
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++11 이동 의미론(move semantics) 완벽 가이드. lvalue vs rvalue 차이, rvalue 참조(&&), std::move 사용법, 이동 생성자·이동 대입 연산자 구현, RVO(Return … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.