vector
의 삽입 메서드를 보면, 3개의 이름을 가진 별도의 메서드가 있는 것을 알 수 있다. 언뜻 보기에는 별 다른 차이가 없어보이는 메서드가 3개나 존재한다. 왜 3개나 되는 삽입 메서드가 존재하는지 알아보자.
1. 함수 원형 및 쓰임의 차이
insert()
와 push_()
는 이미 생성된 요소를 삽입할 때 사용한다. insert
는 삽입될 유치를 직접 지정해줘야하고, 여러개의 요소를 삽입할 수 있다. push_()
는 시작과 끝이 존재하는 컨테이너에서 양쪽 끝에 새로운 원소를 단 한 개만 삽입하고자 할 때 사용한다. 어떻게 보면 insert의 특수화라고 생각할 수도 있다. 따라서, 어떤 vector v의 v.push_back(e)
는 v.insert(v.end(), e)
와 같다고 생각할 수 있다.
반면, emplace()
는 요소의 생성자를 직접 호출해서 메서드 내부에서 요소를 생성한다. 그래서 매개변수로도 요소 자체가 아니라, 생성시 필요한 것들을 가변인수를 통해서 매개변수로 받는다. 그래서 기존 int형으로는 차이가 두드러지지 않는다. Point라는 클래스를 예시로 들어 차이를 확인해보자.
#include<vector>
using namespace std;
class Point {
public:
int x, y;
Point() :x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
};
int main() {
vector<Point> v;
Point p1{ 10,10 };
Point p2{ 20,20 };
v.insert(v.end(), p1); // 기존에 존재하는 요소를 삽입
v.insert(v.end(), Point(1,1)); // 새로 요소를 생성하면서 삽입
v.insert(v.end(), {2,3}); // list-initializer를 이용한 생성 후 삽입
v.insert(v.end(), {p1, p2}); // 여러 개의 요소를 list-initialier로 묶어서 삽입
v.insert(v.end(), { Point(), Point(1,1)}); // 생성과 동시에 삽입 가능
v.insert(v.end(), { {4,5},{3,2} }); // 생성과 동시에 삽입 가능
v.insert(v.end(), 5, p1); // 동일한 요소롤 반복 추가 가능
//v.insert(v.end(), 3,3); // 알 수 없는 잘못된 삽입 연산
v.push_back(p1); // 기존에 존재하는 요소를 삽입
v.push_back(Point(1, 1)); // 새로 요소를 생성하면서 삽입
v.push_back({ 2,3 }); // list-initializer를 이용한 생성 후 삽입
//v.push_back({ p1, p2 }); // 여러 개의 요소를 삽입할 수 없다.
//v.push_back(4, 5); // 알 수 없는 잘못된 삽입 연산
v.emplace(v.end(), 1, 2); // Point(1,2)를 내부에서 생성함.
v.emplace(v.end()); // Point()를 내부에서 생성함.
v.emplace(v.end(), p1); // 복사 생성자를 호출
v.emplace(v.end(), Point(1, 1)); // 복사 생성자 호출
// v.emplace(v.end(), {p1,p2}); // 여러 개의 요소 생성 불가능.
}
그 외에도 insert
와 emplace
는 새로 생성된 요소(들의 시작점)의 이터레이터를 반환하는 것에 비해, push
는 void 형인 차이점도 있다.
2. 내부적으로는 어떻게 구현되어 있는가.
가끔 인터넷 블로그나 chatGPT에 물어보면, emplace는 이동 시멘틱을 사용하기 때문에 다른 삽입 연산보다 빠를 수 있다는 말을 한다. 그러나, 이것은 틀린 설명이다. insert와 push도 가능한 경우 이동 시멘틱을 사용하며, 실제 내부 구현은 emplace를 호출하는 식으로 되어 있다. (여러 요소를 추가할 수 있는 insert의 경우 다른 방법을 사용하지만, 이동 시멘틱을 쓸 수 있으면 쓰는 것은 동일하다.)
즉, 성능상의 차이 함수 스택이 하나 더 쌓이는 것 말고는 없다고 봐도 무방하다. 상황에 따라 쓰고 싶은 메서드를 적절히 사용하면 된다. 아래의 코드는 chatGPT를 이용해서 복사보다 이동이 빠른 클래스의 emplace와 push_back을 비교한 코드이다. 주석처리한 생성자 속 출력문의 주석을 해제하면, push_back도 이동 생성자를 이용하는 것을 알 수 있다. 성능 측정 결과는 두 메서드간의 차이가 거의 없음을 보여준다. (오히려, 메모리 할당의 문제로 늦게 메모리 할당을 수행하는 emplace가 더 느린 결과를 보여준다.)
#include <iostream>
#include <vector>
#include <chrono>
class LargeMemoryBlock {
public:
LargeMemoryBlock(size_t size)
: data(new int[size]), size(size) {
// Simulate expensive resource allocation
for (size_t i = 0; i < size; ++i) {
data[i] = i;
}
}
LargeMemoryBlock(const LargeMemoryBlock& other)
: data(new int[other.size]), size(other.size) {
//std::cout << "Copy constructor" << std::endl;
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
LargeMemoryBlock(LargeMemoryBlock&& other) noexcept
: data(other.data), size(other.size) {
//std::cout << "Move constructor" << std::endl;
other.data = nullptr;
other.size = 0;
}
~LargeMemoryBlock() {
delete[] data;
}
private:
int* data;
size_t size;
};
int main() {
const int numObjects = 10000;
const size_t blockSize = 10000; // 4 MB
const int N = 10;
std::vector<LargeMemoryBlock> vecCopy;
std::vector<LargeMemoryBlock> vecMove;
// Benchmark push_back() with LargeMemoryBlock
for (int t = 0; t < N; t++) {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numObjects; ++i) {
vecCopy.push_back(LargeMemoryBlock(blockSize));
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> pushBackCopyTime = end - start;
std::cout << "push_back() with LargeMemoryBlock time: " << pushBackCopyTime.count() << " seconds" << std::endl;
// Benchmark emplace_back() with LargeMemoryBlock
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < numObjects; ++i) {
vecMove.emplace_back(blockSize);
}
end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> emplaceBackMoveTime = end - start;
std::cout << "emplace_back() with LargeMemoryBlock time: " << emplaceBackMoveTime.count() << " seconds" << std::endl;
}
return 0;
}
push_back() with LargeMemoryBlock time: 1.03201 seconds
emplace_back() with LargeMemoryBlock time: 1.09236 seconds
push_back() with LargeMemoryBlock time: 0.957107 seconds
emplace_back() with LargeMemoryBlock time: 0.977603 seconds
push_back() with LargeMemoryBlock time: 0.934155 seconds
emplace_back() with LargeMemoryBlock time: 0.973151 seconds
push_back() with LargeMemoryBlock time: 0.943765 seconds
emplace_back() with LargeMemoryBlock time: 0.995767 seconds
push_back() with LargeMemoryBlock time: 0.932459 seconds
emplace_back() with LargeMemoryBlock time: 0.965338 seconds
push_back() with LargeMemoryBlock time: 0.968607 seconds
emplace_back() with LargeMemoryBlock time: 1.01215 seconds
push_back() with LargeMemoryBlock time: 0.966702 seconds
emplace_back() with LargeMemoryBlock time: 0.971681 seconds
push_back() with LargeMemoryBlock time: 0.923538 seconds
emplace_back() with LargeMemoryBlock time: 0.976951 seconds
push_back() with LargeMemoryBlock time: 0.978641 seconds
emplace_back() with LargeMemoryBlock time: 0.985579 seconds
push_back() with LargeMemoryBlock time: 0.950241 seconds
emplace_back() with LargeMemoryBlock time: 0.974465 seconds