C++ emplace_back vs push_back: 실제 성능 차이와 언제 다른지
이 글의 핵심
emplace가 항상 빠르다고 모든 곳에 쓰면 explicit 생성자를 우회해 의도하지 않은 변환이 컴파일되거나, 중괄호 초기화 리스트를 그대로 넘길 수 없는 문제에 부딪힙니다. 예외 안전성 주의점과 예상보다 느린 성능·컴파일 에러 트러블슈팅을 거쳐 두 함수 중 무엇을 고를지 기준을 세웁니다.
emplace_back이 정말 더 빠른가
C++11에서 도입된 emplace는 제자리 생성(in-place construction)으로 불필요한 복사/이동을 제거합니다.
비유로 말씀드리면, push는 공장에서 만든 상자를 창고로 옮기기, emplace는 창고 안에서 부품을 조립해 상자를 완성하는 것에 가깝습니다. 한 번 덜 옮기면 그만큼 비용이 줄어듭니다.
언제 emplace 계열을, 언제 push 계열을 쓰나요?
| 관점 | push_back 등 | emplace_back 등 |
|---|---|---|
| 성능 | 이미 만든 객체를 이동하면 충분할 때 | 생성자 인자만 넘겨 임시 객체를 줄일 때 |
| 사용성 | 읽기 쉬운 경우가 많음 | explicit 생성자 호출 등 세밀한 제어 |
| 적용 시나리오 | int, 이미 만든 T | T가 무겁거나 변환 단계가 많을 때 |
// push_back: 임시 객체 생성 → 이동
std::vector<std::string> vec;
vec.push_back(std::string("Hello")); // 임시 객체 생성 → 이동
// emplace_back: 제자리 생성
std::vector<std::string> vec;
vec.emplace_back("Hello"); // 직접 생성 (복사/이동 없음)
emplace_back의 원리는 단순합니다. template<class... Args> emplace_back(Args&&... args)로 인자를 완벽 전달받아, 벡터가 확보한 메모리 위에서 T(std::forward<Args>(args)...)를 직접 호출합니다(placement new). 반면 push_back(const T&)/push_back(T&&)는 이미 완성된 T를 받으므로, "Hello"처럼 T가 아닌 인자를 넘기면 먼저 임시 T가 만들어지고 그것이 벡터 안으로 이동됩니다. 둘의 차이는 결국 “임시 객체 하나 + 이동 한 번”이며, 이 비용이 얼마나 큰지는 타입에 달려 있습니다. 이 점을 기억하면 아래의 벤치마크와 주의사항이 모두 같은 원리로 설명됩니다.
복사·이동 후 삽입 vs 제자리 생성
push_back: 만든 객체를 복사하거나 이동
class Data {
public:
Data(int x, int y) : x_(x), y_(y) {
std::cout << "Data(" << x_ << ", " << y_ << ") 생성\n";
}
Data(const Data& other) : x_(other.x_), y_(other.y_) {
std::cout << "Data 복사\n";
}
Data(Data&& other) noexcept : x_(other.x_), y_(other.y_) {
std::cout << "Data 이동\n";
}
private:
int x_, y_;
};
int main() {
std::vector<Data> vec;
vec.reserve(10); // 재할당 방지
// push_back: 임시 객체 생성 → 이동
vec.push_back(Data(1, 2));
// 출력:
// Data(1, 2) 생성
// Data 이동
}
emplace_back: 생성자 인자를 받아 제자리 생성
int main() {
std::vector<Data> vec;
vec.reserve(10);
// emplace_back: 제자리 생성
vec.emplace_back(1, 2);
// 출력:
// Data(1, 2) 생성
// (복사/이동 없음!)
}
항목별 비교표
| 항목 | push_back | emplace_back |
|---|---|---|
| 객체 생성 | 외부 → 이동 | 제자리 생성 |
| 생성자 인자 | 객체 전달 | 생성자 인자 전달 |
| 복사/이동 | 있음 | 없음 |
| explicit 생성자 | ❌ | ✅ |
| C++ 버전 | 모든 버전 | C++11 이후 |
벤치마크로 본 실제 차이
int 같은 단순 타입
#include <benchmark/benchmark.h>
// push_back (int)
static void BM_PushBack_Int(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
}
}
BENCHMARK(BM_PushBack_Int);
// emplace_back (int)
static void BM_EmplaceBack_Int(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> vec;
vec.reserve(1000);
for (int i = 0; i < 1000; ++i) {
vec.emplace_back(i);
}
}
}
BENCHMARK(BM_EmplaceBack_Int);
int처럼 사소한 타입은 “임시 객체를 만들고 이동”하는 것이 레지스터 값을 한 번 쓰는 것과 같아서, 최적화 빌드에서 두 함수는 사실상 같은 기계어로 컴파일됩니다. 이 벤치마크에서 의미 있는 차이가 나온다면 측정 잡음이거나, 결과 벡터를 쓰지 않아 컴파일러가 루프 일부를 없앤 경우일 가능성이 높으므로 benchmark::DoNotOptimize(vec.data()) 같은 장치를 넣고 다시 재 봐야 합니다.
string 멤버를 가진 복잡한 객체
struct ComplexData {
std::string name;
std::vector<int> values;
ComplexData(const std::string& n, size_t count)
: name(n), values(count, 0) {}
};
// push_back (ComplexData)
static void BM_PushBack_Complex(benchmark::State& state) {
for (auto _ : state) {
std::vector<ComplexData> vec;
vec.reserve(100);
for (int i = 0; i < 100; ++i) {
vec.push_back(ComplexData("data", 1000));
}
}
}
BENCHMARK(BM_PushBack_Complex);
// emplace_back (ComplexData)
static void BM_EmplaceBack_Complex(benchmark::State& state) {
for (auto _ : state) {
std::vector<ComplexData> vec;
vec.reserve(100);
for (int i = 0; i < 100; ++i) {
vec.emplace_back("data", 1000);
}
}
}
BENCHMARK(BM_EmplaceBack_Complex);
이 벤치마크의 결과는 생각보다 차이가 작게 나오는 경우가 많습니다. 두 버전 모두 values(count, 0)로 1000개짜리 벡터를 할당하고 0으로 채우는 비용이 똑같이 들고, push_back 쪽에서만 추가되는 것은 임시 ComplexData의 이동입니다. 이동은 std::string(짧은 문자열이라 SSO 버퍼 복사)과 std::vector(포인터 세 개 복사)를 옮기는 정도라, 할당·초기화 비용에 비하면 작습니다. 즉 “복잡한 객체라서 emplace가 훨씬 빠르다”기보다 “이동 한 번을 아낀다”가 정확한 설명이고, 실제 차이는 컴파일러·표준 라이브러리·CPU에 따라 달라지므로 자기 환경에서 측정해야 합니다. 차이가 크게 벌어지는 경우는 이동 생성자가 없어 복사로 대체되는 타입이나, std::array처럼 이동이 곧 전체 복사인 큰 값 타입입니다.
타입별로 차이가 생기는 곳
| 타입 | push_back(임시)에서 추가되는 비용 | emplace_back 이점 |
|---|---|---|
| int, double | 없음 (최적화로 동일) | 없음 |
| std::string, std::vector | 이동 한 번 (포인터·크기 몇 개 복사) | 작음 |
| std::array, 큰 POD 구조체 | 이동 = 전체 복사 | 큼 |
| 이동 불가·복사 비싼 타입 | 복사 한 번 | 큼 |
절대 시간은 환경마다 크게 다르므로 표에는 넣지 않았습니다. 판단 기준은 “그 타입의 이동이 얼마나 비싼가”입니다.
// 벤치마크 코드
struct LargeObject {
std::array<int, 256> data; // 1KB
LargeObject(int value) {
data.fill(value);
}
};
// push_back: 생성 + 이동 (1KB 복사)
// emplace_back: 제자리 생성 (복사 없음)
생성자 호출 횟수 세어 보기
class Tracker {
static int constructorCalls;
static int copyCalls;
static int moveCalls;
public:
Tracker(int x) {
++constructorCalls;
std::cout << "생성자: " << constructorCalls << "회\n";
}
Tracker(const Tracker&) {
++copyCalls;
std::cout << "복사: " << copyCalls << "회\n";
}
Tracker(Tracker&&) noexcept {
++moveCalls;
std::cout << "이동: " << moveCalls << "회\n";
}
};
// push_back(Tracker(42))
// 출력: 생성자: 1회, 이동: 1회
// emplace_back(42)
// 출력: 생성자: 1회 (이동 없음!)
RVO와 컴파일러 최적화가 차이를 줄이는 방식
임시 객체와 RVO
// RVO (Return Value Optimization)
std::string createString() {
return std::string("Hello"); // RVO 적용
}
std::vector<std::string> vec;
// push_back + RVO
vec.push_back(createString()); // 함수 안의 복사는 RVO로 생략되지만, 벡터 안으로의 이동 한 번은 남음
// emplace_back
vec.emplace_back("Hello"); // 제자리 생성
// 최적화 수준에 따라 성능 동일할 수 있음
RVO는 createString() 안에서 만든 객체를 호출자의 임시 객체 자리에 바로 만들어 주지만, 그 임시 객체가 push_back(T&&)의 매개변수에 묶인 뒤 벡터의 버퍼로 옮겨지는 이동은 생략되지 않습니다. 벡터 원소는 벡터가 관리하는 메모리에 있어야 하므로, 함수 반환값을 그 자리에 직접 만들 방법이 없기 때문입니다. 반환값을 넣는 경우라면 emplace_back(createString())도 같은 이동을 하므로 둘은 동일합니다.
-O0과 -O3에서 달라지는 결과
# 최적화 없음: 인라인되지 않아 임시 객체·이동 호출이 그대로 남음
g++ -O0 test.cpp
# 최적화 최대: 단순 타입은 차이가 사라짐
g++ -O3 test.cpp
# 어셈블리 확인
g++ -S -O3 test.cpp
# 생성된 어셈블리 코드 비교
emplace가 맞는 곳과 push가 맞는 곳
생성자 인자를 여러 개 받는 객체: emplace
// ✅ emplace: 복잡한 객체
struct Person {
std::string name;
int age;
std::vector<std::string> hobbies;
Person(const std::string& n, int a, std::initializer_list<std::string> h)
: name(n), age(a), hobbies(h) {}
};
int main() {
std::vector<Person> people;
// emplace_back: 생성자 인자 직접 전달
// 중괄호 목록은 템플릿 인자로 추론되지 않으므로 initializer_list 타입을 명시해야 함
people.emplace_back("Alice", 30, std::initializer_list<std::string>{"독서", "음악"});
// people.emplace_back("Bob", 25, {"운동", "게임"}); // ❌ 컴파일 에러
// push_back: 임시 객체 생성 필요 (Person 생성자 호출이라 {} 그대로 가능)
people.push_back(Person("Charlie", 35, {"요리"}));
}
주석으로 막은 줄이 emplace_back의 가장 흔한 컴파일 에러입니다. {"운동", "게임"} 같은 중괄호 초기화 목록은 그 자체로 타입이 없어서, Args&&...로 받는 템플릿 매개변수가 무엇인지 추론할 수 없습니다. GCC는 no matching function for call to 'emplace_back(const char [4], int, <brace-enclosed initializer list>)'와 함께 “couldn’t deduce template parameter”라는 설명을 냅니다. 반면 Person(...)을 직접 호출하면 생성자 매개변수 타입이 정해져 있으므로 중괄호 목록이 std::initializer_list<std::string>으로 변환됩니다. 해결책은 위처럼 타입을 명시하거나, 이런 경우에는 push_back(Person{...})을 쓰는 것입니다.
explicit 생성자를 가진 타입: emplace
// ✅ emplace: explicit 생성자
class File {
std::string path_;
public:
explicit File(const std::string& path) : path_(path) {}
};
int main() {
std::vector<File> files;
// emplace_back: explicit 생성자 호출 가능
files.emplace_back("data.txt");
// push_back: explicit 생성자 호출 불가
// files.push_back("data.txt"); // 컴파일 에러
files.push_back(File("data.txt")); // OK
}
이미 만들어 둔 객체: push
// ✅ push: 이미 생성된 객체
int main() {
std::vector<std::string> vec;
std::string str = "Hello";
// push_back: 명확한 의도
vec.push_back(str); // 복사
vec.push_back(std::move(str)); // 이동
// emplace_back: 불명확
vec.emplace_back(str); // 복사 (push_back과 동일)
}
int·double 같은 단순 타입: push
// ✅ push: 단순 타입
int main() {
std::vector<int> vec;
// push_back: 명확하고 간결
vec.push_back(42);
// emplace_back: 불필요
vec.emplace_back(42); // push_back과 동일
}
map·set·게임 엔티티·로그 버퍼·패킷 큐에 적용하기
map/unordered_map에 emplace
// ✅ emplace: map 삽입
std::map<std::string, Person> people;
// emplace: 제자리 생성
people.emplace("alice", Person("Alice", 30, {}));
// emplace with piecewise_construct
people.emplace(std::piecewise_construct,
std::forward_as_tuple("bob"),
std::forward_as_tuple("Bob", 25, std::initializer_list<std::string>{}));
// insert: 임시 객체 생성
people.insert({"charlie", Person("Charlie", 35, {})});
map의 원소는 std::pair<const Key, Value>라 emplace의 이점을 제대로 쓰려면 모양이 조금 복잡해집니다. 첫 번째 people.emplace("alice", Person(...))는 Person 임시 객체를 먼저 만들어 넘기므로 insert와 거의 다르지 않습니다. 키와 값을 각각 제자리에서 생성하려면 두 번째처럼 std::piecewise_construct와 forward_as_tuple로 “키 생성자 인자 묶음”과 “값 생성자 인자 묶음”을 따로 넘겨야 합니다. 이 문법이 번거로워 C++17에서 추가된 것이 아래의 try_emplace(key, value_args...)로, 키는 그대로 받고 나머지 인자로 값을 제자리 생성합니다.
set/unordered_set에 emplace
// ✅ emplace: set 삽입
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
bool operator<(const Point& other) const {
return x < other.x || (x == other.x && y < other.y);
}
};
std::set<Point> points;
// emplace: 제자리 생성
points.emplace(10, 20);
// insert: 임시 객체 생성
points.insert(Point(30, 40));
try_emplace로 조건부 삽입
// ✅ try_emplace: 조건부 삽입 (C++17)
std::map<std::string, std::string> cache;
// try_emplace: 키가 없을 때만 삽입
auto [it, inserted] = cache.try_emplace("key", "value");
if (inserted) {
std::cout << "삽입됨\n";
} else {
std::cout << "이미 존재\n";
}
// emplace: 키가 있어도 생성 시도
cache.emplace("key", "new_value"); // ⚠️ 생성 후 버림
게임 엔티티 생성
// ✅ 실무 예시: 게임 엔티티
struct Entity {
int id;
std::string name;
glm::vec3 position;
std::vector<Component*> components;
Entity(int id, const std::string& name, glm::vec3 pos)
: id(id), name(name), position(pos) {}
};
class EntityManager {
std::vector<Entity> entities_;
public:
void spawnEntity(int id, const std::string& name, glm::vec3 pos) {
// ✅ emplace_back: 복잡한 생성자
entities_.emplace_back(id, name, pos);
// push_back(Entity(id, name, pos))보다 효율적
}
void addEntity(Entity entity) {
// ✅ push_back: 이미 생성된 객체
entities_.push_back(std::move(entity));
}
};
// 사용
EntityManager manager;
manager.spawnEntity(1, "Player", {0.0f, 0.0f, 0.0f});
로그 버퍼
// ✅ 실무 예시: 로그 버퍼
struct LogEntry {
std::chrono::system_clock::time_point timestamp;
std::string level;
std::string message;
std::string file;
int line;
LogEntry(std::string level, std::string msg, std::string file, int line)
: timestamp(std::chrono::system_clock::now()),
level(std::move(level)),
message(std::move(msg)),
file(std::move(file)),
line(line) {}
};
class Logger {
std::vector<LogEntry> buffer_;
std::mutex mutex_;
public:
void log(const std::string& level, const std::string& msg,
const std::string& file, int line) {
std::lock_guard<std::mutex> lock(mutex_);
// ✅ emplace_back: 생성자 인자 직접 전달
buffer_.emplace_back(level, msg, file, line);
// 임시 LogEntry 객체 생성 없음
}
};
// 사용
Logger logger;
logger.log("ERROR", "File not found", __FILE__, __LINE__);
네트워크 패킷 큐
// ✅ 실무 예시: 패킷 큐
struct Packet {
uint32_t id;
std::vector<uint8_t> data;
std::chrono::steady_clock::time_point timestamp;
Packet(uint32_t id, std::vector<uint8_t> data)
: id(id),
data(std::move(data)),
timestamp(std::chrono::steady_clock::now()) {}
};
class PacketQueue {
std::deque<Packet> queue_;
std::mutex mutex_;
public:
void enqueue(uint32_t id, std::vector<uint8_t> data) {
std::lock_guard<std::mutex> lock(mutex_);
// ✅ emplace_back: 이동 의미론 활용
queue_.emplace_back(id, std::move(data));
}
std::optional<Packet> dequeue() {
std::lock_guard<std::mutex> lock(mutex_);
if (queue_.empty()) {
return std::nullopt;
}
Packet packet = std::move(queue_.front());
queue_.pop_front();
return packet;
}
};
explicit 우회·중괄호 목록·예외 안전성 함정
emplace는 explicit 생성자도 호출한다
// ❌ explicit 우회 가능
class Size {
size_t value_;
public:
explicit Size(size_t value) : value_(value) {}
};
int main() {
std::vector<Size> vec;
// push_back: explicit 생성자 호출 불가
// vec.push_back(10); // 컴파일 에러
// emplace_back: explicit 생성자 호출 가능
vec.emplace_back(10); // ⚠️ 의도하지 않은 변환 가능
}
emplace_back은 내부에서 직접 초기화(T(args...))를 하므로 explicit 생성자도 호출합니다. explicit은 “암묵적 변환으로 이 생성자를 쓰지 말라”는 표시인데, emplace는 그 의도를 우회합니다. 이것이 실제 버그가 되는 대표적인 예는 스마트 포인터입니다. std::vector<std::unique_ptr<Widget>> v; v.push_back(ptr);는 raw 포인터에서 unique_ptr로의 변환이 explicit이라 컴파일되지 않지만, v.emplace_back(ptr);는 컴파일됩니다. 이미 다른 곳이 소유하고 있는 포인터라면 두 곳에서 해제하는 이중 해제가 됩니다. std::vector<std::regex> v; v.emplace_back(nullptr);처럼 말이 안 되는 코드가 컴파일되어 실행 중에 터지는 사례도 잘 알려져 있습니다. 타입 검사를 원한다면 push_back이 더 안전한 기본값입니다.
중괄호 초기화 목록은 emplace로 넘길 수 없다
// ❌ 초기화 리스트 주의
int main() {
std::vector<std::vector<int>> vec;
// push_back: 명확
vec.push_back({1, 2, 3});
// emplace_back: 중괄호 목록은 전달 불가
// vec.emplace_back({1, 2, 3}); // ❌ 컴파일 에러 (타입 추론 불가)
vec.emplace_back(std::initializer_list<int>{1, 2, 3}); // ✅
vec.emplace_back(3, 100); // ⚠️ 다른 의미 (크기 3, 값 100 → {100, 100, 100})
}
emplace_back(3, 100)은 std::vector<int>(3, 100), 즉 “100이 세 개”인 벡터를 만듭니다. 반면 push_back({3, 100})은 원소가 3과 100인 벡터를 넣습니다. 괄호와 중괄호의 차이가 생성자 선택을 바꾸는 C++의 오래된 함정이 emplace에서 더 잘 드러나는 셈입니다.
raw 포인터를 넘길 때의 누수 위험
// ❌ 예외 안전성 주의
class Resource {
public:
Resource(int* ptr) {
if (ptr == nullptr) {
throw std::runtime_error("nullptr");
}
}
};
int main() {
std::vector<Resource> vec;
try {
// 생성자가 예외를 던지면 vector는 호출 전 상태 그대로 (강한 예외 보장)
vec.emplace_back(nullptr);
} catch (...) {
// vec.size()는 변하지 않음
}
}
vector::emplace_back과 push_back은 끝에 추가할 때 강한 예외 보장을 제공합니다. 생성자나 재할당 중 예외가 나면 벡터는 호출 전 상태로 남습니다(단, 이동 생성자가 noexcept가 아니고 복사할 수도 없는 타입이면 보장이 약해집니다). 그래서 위 코드 자체는 안전합니다.
emplace에서 실제로 문제가 되는 예외 안전성 함정은 자원을 raw 포인터로 넘길 때입니다.
std::vector<std::unique_ptr<Widget>> widgets;
widgets.emplace_back(new Widget); // ⚠️ 재할당 중 bad_alloc이 나면 Widget 누수
widgets.push_back(std::make_unique<Widget>()); // ✅ unique_ptr이 먼저 소유권을 가짐
emplace_back(new Widget)은 new로 객체를 만든 뒤 벡터가 공간을 확보하는데, 이때 재할당이 실패하면 unique_ptr이 아직 만들어지지 않아 Widget을 해제할 주체가 없습니다. make_unique로 먼저 소유권을 잡아 두면 어느 시점에 예외가 나도 누수가 없습니다. Effective Modern C++ 항목 42가 이 사례를 들어 “emplace가 항상 낫지는 않다”고 설명합니다.
모든 곳에 emplace를 쓰는 습관
// ❌ 실수: 불필요한 emplace
std::vector<int> vec;
vec.emplace_back(42); // push_back(42)와 동일
std::vector<std::string> names;
std::string name = "Alice";
names.emplace_back(name); // push_back(name)과 동일 (복사)
// ✅ 적절한 사용
std::vector<std::string> names;
names.emplace_back("Alice"); // 생성자 인자 직접 전달
names.push_back(name); // 이미 생성된 객체는 push_back
emplace로 바꿨는데 여전히 느릴 때
증상:
// 예상: emplace가 빠를 것
std::vector<std::string> vec;
for (int i = 0; i < 1000000; ++i) {
vec.emplace_back("test"); // 여전히 느림
}
원인: reserve() 누락으로 재할당 발생
재할당은 emplace와 push의 차이보다 훨씬 큰 비용입니다. 용량이 차면 벡터는 더 큰 버퍼(보통 1.5~2배)를 새로 할당하고 기존 원소를 모두 옮긴 뒤 이전 버퍼를 해제합니다. 원소 100만 개를 넣는 동안 이런 재할당이 수십 번 일어나고, 원소 타입의 이동 생성자가 noexcept가 아니면 강한 예외 보장을 지키기 위해 이동 대신 복사를 합니다. 직접 만든 클래스에 이동 생성자를 정의했다면 noexcept를 붙였는지 확인하는 것이 emplace로 바꾸는 것보다 더 큰 효과를 내는 경우가 많습니다.
해결:
std::vector<std::string> vec;
vec.reserve(1000000); // ✅ 재할당 방지
for (int i = 0; i < 1000000; ++i) {
vec.emplace_back("test");
}
emplace_back({1, 2})가 컴파일되지 않을 때
증상:
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
std::vector<Point> points;
points.emplace_back(1, 2); // ✅ OK
points.emplace_back({1, 2}); // ❌ 컴파일 에러
원인: 중괄호 초기화는 생성자 인자가 아님
해결:
points.push_back({1, 2}); // ✅ 중괄호 초기화
points.emplace_back(1, 2); // ✅ 생성자 인자
코드 리뷰에서 쓰는 선택 기준
사용 결정 플로우차트
객체를 컨테이너에 추가
↓
이미 생성된 객체인가?
├─ Yes → push_back/push
└─ No → 계속
↓
단순 타입인가? (int, double 등)
├─ Yes → push_back (명확성)
└─ No → 계속
↓
생성자 인자가 복잡한가?
├─ Yes → emplace_back
└─ No → push_back (명확성)
리뷰 체크 포인트
// 🔍 리뷰 시 확인사항
// 1. 이미 생성된 객체
std::string str = "Hello";
vec.emplace_back(str); // ⚠️ push_back이 더 명확
// 2. 단순 타입
vec.emplace_back(42); // ⚠️ push_back(42)와 동일
// 3. 복잡한 생성자
vec.emplace_back("Alice", 30, std::vector<std::string>{"hobby1", "hobby2"});
// ✅ emplace가 적합
// 4. reserve() 확인
vec.emplace_back(...); // ⚠️ reserve() 있는가?
직접 측정하는 벤치마크 템플릿
template <typename Container, typename... Args>
void benchmarkInsertion(const std::string& name, Args&&... args) {
auto start = std::chrono::high_resolution_clock::now();
Container container;
container.reserve(1000000);
for (int i = 0; i < 1000000; ++i) {
container.emplace_back(std::forward<Args>(args)...);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << name << ": " << duration.count() << "ms\n";
}
// 사용
benchmarkInsertion<std::vector<std::string>>("emplace", "test");
emplace와 push 선택 요약
상황별 선택표
| 상황 | 사용 |
|---|---|
| 복잡한 객체 | emplace |
| 생성자 인자 전달 | emplace |
| explicit 생성자 | emplace |
| 이미 생성된 객체 | push |
| 단순 타입 | push |
| 명확성 우선 | push |
네 가지 규칙
- 복잡한 객체 → emplace
- 단순 타입 → push
- 명확성 우선 → push
- 성능 중요 → 벤치마크
다음 단계: emplace를 이해했다면, C++ 이동 시맨틱 가이드에서 더 깊이 배워보세요.
같이 보면 좋은 글
자주 묻는 질문 (FAQ)
Q. map에서 emplace와 try_emplace는 무엇이 다른가요?
A. emplace는 키가 이미 있어도 삽입 시도를 위해 노드(pair)를 먼저 만들 수 있어서, 인자로 넘긴 값이 이동된 채 버려질 수 있습니다. C++17의 try_emplace는 키가 없을 때만 값을 생성하고, 키가 있으면 인자를 건드리지 않고 기존 원소의 반복자와 false를 돌려줍니다. 본문의 조건부 삽입 예시처럼 캐시에 없을 때만 넣는 코드라면 try_emplace가 의도가 더 분명하고 불필요한 생성도 피할 수 있습니다.