C++/Concepts

C++ push_back과 emplace_back 차이

Enchantée 2026. 7. 9. 22:21
728x90
반응형

std::vector에 object를 넣을 때 push_back과 emplace_back 중 무엇을 써야 하는지 자주 고민하게 됩니다.

많은 글에서 emplace_back이 더 빠르다고 설명하지만, 실무 코드에서는 이 문장만 기억하면 오히려 잘못된 선택을 하기 쉽습니다.

emplace_back은 magic optimization이 아니라 container 내부에서 object를 직접 construct할 수 있게 해주는 API입니다.

 

push_back과 emplace_back의 차이는 빠르고 느림보다, 이미 만들어진 object를 넣는가 아니면 container 안에서 바로 만들 것인가에 있습니다.

 

이번 글에서는 C++17 기준으로 push_back과 emplace_back이 어떤 상황에서 copy, move, direct construction을 만드는지 코드 출력으로 확인하고, 실무에서 어떤 기준으로 선택하면 좋은지 정리해보겠습니다.

 

push_back은 이미 만들어진 object를 container에 넣고, emplace_back은 인자를 받아 container 내부에서 object를 직접 construct할 수 있습니다.

 


1. push_back과 emplace_back은 무엇이 다른가?

std::vector::push_back은 vector에 이미 만들어진 object를 추가합니다.

인자로 lvalue를 넘기면 copy가 일어날 수 있고, rvalue를 넘기면 move가 일어날 수 있습니다.

std::vector::emplace_back은 object를 만들기 위한 constructor argument를 받아 vector 내부 storage에서 object를 직접 construct합니다.

구분 push_back emplace_back
입력 이미 만들어진 object object를 만들 constructor argument
주요 동작 copy 또는 move로 element 추가 container storage에서 direct construction
잘 맞는 상황 이미 object가 있거나 반환값을 넣는 경우 container 안에서 새 object를 바로 만들 경우
주의할 점 불필요한 temporary나 copy 가능성 constructor 선택이 코드에서 덜 명확할 수 있음

핵심은 emplace_back이 항상 더 빠르다는 것이 아닙니다.

이미 만들어진 object를 넣는 상황에서는 push_back이 더 명확할 수 있고, 새 element를 constructor argument로 바로 만들 수 있는 상황에서는 emplace_back이 자연스럽습니다.

 


2. 왜 중요한가?

container에 object를 넣는 코드는 서비스의 hot path에 자주 등장합니다.

request를 parsing해서 vector에 담거나, log event를 모으거나, game object를 update list에 넣거나, DB row를 result list로 바꾸는 코드가 모두 여기에 해당합니다.

작은 object라면 차이가 거의 없을 수 있지만, string, buffer, file handle wrapper, network request object처럼 copy/move 비용이나 lifetime이 중요한 타입에서는 차이가 보일 수 있습니다.

  1. copy constructor가 호출되는지 확인해야 한다.
  2. move constructor가 noexcept인지 확인해야 한다.
  3. vector reallocation 때문에 기존 element가 move될 수 있음을 이해해야 한다.
  4. reserve 여부가 push_back과 emplace_back 차이보다 더 큰 영향을 줄 수 있다.

즉, push_back과 emplace_back을 비교할 때는 API 이름만 볼 것이 아니라 object construction, move, reallocation을 같이 봐야 합니다.

 


3. 그림으로 이해하기

단순화하면 push_back과 emplace_back의 흐름은 다음처럼 볼 수 있습니다.

 

push_back은 object가 먼저 존재하고 vector로 들어가는 흐름이고, emplace_back은 constructor argument가 vector 내부 storage에서 곧바로 object가 되는 흐름입니다.

 

단, 이 그림은 reallocation이 없는 상황을 단순화한 것입니다.

vector capacity가 부족하면 새 storage를 확보하고 기존 element를 move하거나 copy해야 합니다.

그래서 대량 삽입에서는 push_back과 emplace_back 중 무엇을 고르느냐보다 reserve를 적절히 호출했는지가 더 중요할 수 있습니다.

 


4. C++17 코드로 확인하기

다음 코드는 Task object가 construct, copy, move될 때마다 메시지를 출력합니다.

예제에서는 reserve를 호출해 reallocation 영향을 줄이고, push_back과 emplace_back 자체의 차이에 집중합니다.

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

struct Task {
    std::string title;
    int priority;

    Task(std::string title, int priority)
        : title(std::move(title)), priority(priority)
    {
        std::cout << "construct " << this->title << '\n';
    }

    Task(const Task& other)
        : title(other.title), priority(other.priority)
    {
        std::cout << "copy " << title << '\n';
    }

    Task(Task&& other) noexcept
        : title(std::move(other.title)), priority(other.priority)
    {
        std::cout << "move " << title << '\n';
    }
};

int main()
{
    std::cout << "[push_back]\n";
    std::vector<Task> pushed;
    pushed.reserve(2);

    Task existing("existing", 1);
    pushed.push_back(existing);
    pushed.push_back(Task("temporary", 2));

    std::cout << "\n[emplace_back]\n";
    std::vector<Task> emplaced;
    emplaced.reserve(2);

    emplaced.emplace_back("direct", 3);
    emplaced.emplace_back("another-direct", 4);
}

 

실행 결과

[push_back]
construct existing
copy existing
construct temporary
move temporary

[emplace_back]
construct direct
construct another-direct

 

existing은 이미 만들어진 lvalue object입니다.

pushed.push_back(existing)은 vector 안에 새 element를 만들기 위해 copy constructor를 호출합니다.

pushed.push_back(Task("temporary", 2))는 temporary object를 만든 뒤 vector 내부로 move합니다.

반면 emplace_back("direct", 3)은 Task constructor argument를 받아 vector storage 안에서 바로 Task를 construct합니다.

 


5. reallocation을 빼고 보면 안 된다

위 예제에서는 차이를 명확히 보려고 reserve(2)를 호출했습니다.

하지만 실제 vector는 capacity가 부족하면 더 큰 storage를 새로 잡고 기존 element를 옮깁니다.

이때 기존 element의 move 또는 copy가 발생할 수 있습니다.

상황 발생할 수 있는 일 실무 대응
capacity가 충분함 새 element만 추가 push/emplace 차이를 비교하기 쉬움
capacity가 부족함 새 storage 확보 후 기존 element 이동 예상 개수가 있으면 reserve 사용
move가 noexcept가 아님 vector가 strong exception guarantee 때문에 copy를 선택할 수 있음 resource-owning type은 noexcept move 검토
element가 무겁고 개수가 많음 reallocation 비용이 push/emplace 차이보다 커질 수 있음 성능 측정과 capacity 전략 우선

그래서 “emplace_back으로 바꾸면 빨라진다”보다 먼저 봐야 할 질문은 “vector가 몇 번 reallocation되는가?”입니다.

대량 삽입 경로에서 예상 element 수를 알고 있다면 reserve를 먼저 호출하는 것이 더 큰 개선이 될 수 있습니다.

 


6. 언제 push_back을 쓰고 언제 emplace_back을 쓸까?

실무에서는 다음 기준으로 나누면 과한 고민을 줄일 수 있습니다.

상황 추천 이유
이미 object가 있다 push_back 또는 push_back(std::move(obj)) 의도가 명확하고 constructor argument를 다시 풀 필요가 없음
새 object를 바로 만들 수 있다 emplace_back(args...) temporary 없이 container 내부에서 직접 construct 가능
factory 함수가 object를 반환한다 push_back(make_task()) 이미 완성된 object를 반환하므로 push_back이 자연스러움
constructor overload가 많다 명확한 쪽 선택 emplace_back은 어떤 constructor가 선택되는지 코드에서 덜 선명할 수 있음
읽기 쉬움이 더 중요하다 무조건 emplace_back 고집 금지 작은 타입에서는 성능 차이보다 의도 전달이 중요할 수 있음

특히 이미 object가 있는 상황에서 emplace_back(std::move(obj))를 쓰는 것은 push_back(std::move(obj))보다 특별히 더 좋은 의미를 주지 않는 경우가 많습니다.

반대로 emplace_back("name", 10)처럼 constructor argument를 바로 넘길 수 있다면 emplace_back이 자연스럽습니다.

C++17부터 std::vector::emplace_back은 추가된 element에 대한 reference를 반환하므로, 바로 초기화 후 추가 설정이 필요한 경우에도 사용할 수 있습니다.

 


7. 자주 하는 오해

  1. emplace_back은 항상 push_back보다 빠르다.
    constructor argument로 직접 만들 수 있을 때 이점이 있을 수 있지만, 이미 object가 있거나 reallocation 비용이 큰 경우 차이가 작거나 없을 수 있습니다.
  2. emplace_back을 쓰면 move가 절대 발생하지 않는다.
    새 element 자체는 direct construction될 수 있지만, vector reallocation이 일어나면 기존 element는 move 또는 copy될 수 있습니다.
  3. push_back은 항상 copy한다.
    rvalue를 넘기면 move constructor가 사용될 수 있습니다. push_back(std::move(obj))는 명확한 move 의도를 표현합니다.
  4. reserve는 선택 사항일 뿐이다.
    예상 element 수가 큰 상황에서는 reserve가 push_back/emplace_back 선택보다 성능에 더 큰 영향을 줄 수 있습니다.
  5. emplace_back이 더 Modern C++다운 코드다.
    Modern C++다운 코드는 API 이름이 아니라 ownership, lifetime, readability, 성능 근거가 명확한 코드입니다.

항상 emplace_back을 쓰는 습관보다, object가 이미 있는지와 vector capacity가 충분한지를 먼저 확인하는 습관이 더 중요합니다.

 


8. 실무에서는 어떻게 볼까?

코드 리뷰에서 push_back과 emplace_back을 볼 때는 API 이름보다 object 생성 흐름을 봐야 합니다.

이미 만들어진 object를 vector에 넣고 있다면 push_back이 의도를 더 잘 드러낼 수 있습니다.

반대로 parser가 field 값을 읽어서 바로 result object를 만들고 있다면 emplace_back이 불필요한 temporary를 줄이는 좋은 선택입니다.

성능이 중요하다면 micro benchmark보다 실제 data size, reserve 여부, allocator 동작, move/copy 비용을 함께 봐야 합니다.

작은 struct 몇 개를 넣는 코드에서 emplace_back으로 바꿔도 체감 차이가 없을 수 있지만, 큰 string buffer나 resource-owning object가 많은 path에서는 construction 흐름이 의미를 가질 수 있습니다.

결국 좋은 기준은 “container 안에서 새로 만들 것인가, 이미 만들어진 object를 넣을 것인가”입니다.

 


9. 정리

push_back은 이미 만들어진 object를 vector에 추가할 때 자연스러운 API입니다.

emplace_back은 constructor argument를 받아 vector 내부 storage에서 object를 직접 construct할 수 있습니다.

emplace_back이 항상 더 빠른 것은 아니며, reallocation이 발생하면 기존 element 이동 비용이 생길 수 있습니다.

대량 삽입에서는 push_back/emplace_back 선택보다 reserve 전략이 더 큰 영향을 줄 수 있습니다.

실무에서는 성능보다 먼저 readability, object lifetime, move/copy 비용, capacity 변화를 함께 보고 선택해야 합니다.

 

emplace_back을 기본값처럼 쓰기보다, 새 object를 container 안에서 직접 만들 수 있을 때 선택하는 도구로 이해하는 것이 안전합니다.

 


728x90
반응형