C++ 개발에서 std::vector는 가장 자주 사용하는 컨테이너 중 하나입니다. 하지만 많은 개발자들이 push_back과 emplace_back의 차이를 명확히 이해하지 못하거나, move semantics를 제대로 활용하지 못해 불필요한 성능 손실을 경험합니다. 특히 대용량 데이터를 다루는 3D 그래픽스, 컴퓨터 비전, 게임 개발 등의 분야에서는 이러한 최적화가 매우 중요합니다.
| 비교 항목 | push_back | emplace_back |
|---|---|---|
| 사용 방식 | 완성된 객체를 컨테이너에 복사 또는 이동 | 컨테이너 내부에서 직접 객체를 생성 |
| 인자 전달 | 객체 그 자체만 전달 가능 | 생성자 인자를 그대로 전달 가능 |
| 임시 객체 생성 | 항상 필요함 (rvalue 전달 시 move 시도) | 임시 객체 생성 없이 바로 컨테이너 내부에서 생성 |
| 도입 시기 | C++98부터 존재 | C++11부터 도입 |
std::vector<std::string> v;
std::string s = "hello";
v.push_back(s); // 복사 생성자 호출
v.push_back(std::move(s)); // 이동 생성자 호출
v.push_back("world"); // 임시 std::string 생성 → 이동 또는 복사
s를 복사해서 벡터에 삽입 (복사 생성자 호출)std::move(s)를 통해 이동 생성자 호출"world" 리터럴을 std::string 임시 객체로 만든 후, 벡터로 이동std::vector<std::string> v;
v.emplace_back("hello"); // 바로 벡터 내부에서 "hello" 생성
v.emplace_back(5, 'x'); // std::string(5, 'x') → "xxxxx"
"hello"를 전달하면 임시 std::string 객체를 만들지 않고 바로 벡터 메모리에서 std::string("hello") 생성(5, 'x') 같이 생성자 인자를 직접 넘길 수도 있음 (push_back으로는 불가능)std::vector, std::list, std::map 등이 컨테이너std::vector<std::string> names; // names = 컨테이너
names.push_back("Alice"); // "Alice" = 객체
names.push_back("Bob"); // "Bob" = 객체
| 구분 | lvalue | rvalue |
|---|---|---|
| 정의 | 메모리 주소가 있고, 이름을 통해 참조할 수 있는 값 | 표현식 계산 중에 잠깐 생성되는 임시 값 |
| 수명 | 표현식이 끝나도 계속 살아있음 | 표현식이 끝나면 바로 사라짐 |
| 주소 연산자(&) | 대부분 가능 | 불가능 |
| 예시 | 변수, 배열 요소, 참조 | 상수, 리터럴, 연산 결과, 임시 객체 |
int x = 10; // x는 lvalue (메모리 주소 有)
int y = x; // x는 여전히 lvalue
int z = x + 5; // (x + 5)는 rvalue → 계산 후 바로 사라짐
std::string a = "hello"; // a: lvalue
std::string b = a; // b: lvalue, a는 복사됨
std::string c = a + "!!"; // (a + "!!") 결과: rvalue
std::move(obj)는 obj를 rvalue로 강제 캐스팅합니다. 실제 데이터를 이동하는 것이 아니라, "이 객체를 더 이상 사용하지 않으니 마음껏 이동해도 된다"는 신호를 줍니다.
| 상황 | push_back | emplace_back |
|---|---|---|
| 이미 완성된 객체 삽입 | push_back(obj) → 복사push_back(std::move(obj)) → 이동 | 성능 거의 동일 |
| 임시 객체 삽입 | 임시 객체 생성 후 이동 | 벡터 내부에서 직접 생성 → 더 효율적 |
| 생성자 인자 직접 전달 | 불가능 | 가능, 중간 객체 없이 바로 생성 |
std::vector<std::vector<int>> matrix;
std::vector<std::vector<int>> sourceData = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
for (size_t i = 0; i < sourceData.size(); i++) {
matrix.emplace_back(sourceData[i]); // 비효율적!
}
문제점:
sourceData[i]는 lvaluestd::vector<std::vector<int>> matrix;
std::vector<std::vector<int>> sourceData = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
for (size_t i = 0; i < sourceData.size(); i++) {
matrix.emplace_back(std::move(sourceData[i])); // 최적화!
}
개선점:
std::move로 rvalue로 캐스팅| 항목 | std::move 없음 | std::move 사용 |
|---|---|---|
| 호출되는 생성자 | 복사 생성자 | 이동 생성자 |
| 성능 | O(n) (전체 복사) | O(1) (포인터 이동) |
| 원본 상태 | 유지됨 | 비워짐 (메모리 소유권 이전) |
| 권장 여부 | ❌ (비효율) | ✅ (권장) |
emplace_back이 더 효율적push_back(std::move(obj))와 emplace_back(std::move(obj)) 차이 거의 없음emplace_backpush_back(std::move(obj))❌ 잘못된 이해: "emplace_back은 항상 move를 한다"
✅ 올바른 이해: "emplace_back은 인자에 따라 복사 또는 이동을 선택한다"
C++ 벡터 최적화는 단순히 emplace_back을 사용하는 것으로 끝나지 않습니다. lvalue/rvalue 개념을 이해하고, 적절한 상황에서 std::move를 활용하며, 컨테이너와 객체의 관계를 명확히 파악해야 진정한 성능 개선을 얻을 수 있습니다.
특히 대용량 데이터를 다루는 프로젝트에서는 이러한 최적화 기법들이 전체 성능에 큰 영향을 미칠 수 있으므로, 개발 초기 단계부터 올바른 패턴을 적용하는 것이 중요합니다.