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, 이미 만든 TT가 무겁거나 변환 단계가 많을 때
// 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_backemplace_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

네 가지 규칙

  1. 복잡한 객체 → emplace
  2. 단순 타입 → push
  3. 명확성 우선 → push
  4. 성능 중요 → 벤치마크

다음 단계: emplace를 이해했다면, C++ 이동 시맨틱 가이드에서 더 깊이 배워보세요.


같이 보면 좋은 글

자주 묻는 질문 (FAQ)

Q. map에서 emplace와 try_emplace는 무엇이 다른가요?

A. emplace는 키가 이미 있어도 삽입 시도를 위해 노드(pair)를 먼저 만들 수 있어서, 인자로 넘긴 값이 이동된 채 버려질 수 있습니다. C++17의 try_emplace는 키가 없을 때만 값을 생성하고, 키가 있으면 인자를 건드리지 않고 기존 원소의 반복자와 false를 돌려줍니다. 본문의 조건부 삽입 예시처럼 캐시에 없을 때만 넣는 코드라면 try_emplace가 의도가 더 분명하고 불필요한 생성도 피할 수 있습니다.