1. 이론
1.1. push_back
push_back
함수는 C++98 시절부터 존재했던 전통적인 요소 추가 방식으로 이미 생성된 객체를 인자로 받아 벡터의 끝에 복사 또는 이동하여 추가한다.
push_back(const T& val)
(lvalue 참조): 인자로 전달된 객체(val
)의 복사본을 생성하여 벡터의 끝에 추가한다. 복사 생성자가 호출된다.push_back(T&& val)
(rvalue 참조, C++11부터): 인자로 전달된 임시 객체(val
)를 벡터의 끝으로 이동시킨다. 이동 생성자가 호출되며, 복사보다 훨씬 효율적이다.
내부 구조 및 구현 방식
- Capacity 확인:
push_back
이 호출되면, 벡터는 먼저 내부 버퍼에 새로운 요소를 추가할 공간이 있는지 확인한다. (size() < capacity()
). - 재할당 (Reallocation): 만약 공간이 없다면 (
size() == capacity()
), 벡터는 더 큰 메모리 블록을 새로 할당한다. 기존의 모든 요소를 새 메모리 블록으로 이동(또는 이동이 불가능하면 복사)시킨 후, 이전 메모리 블록을 해제한다. 이 과정은 비용이 많이 들 수 있다. - 요소 생성:
- lvalue가 인자로 들어온 경우: 벡터의 끝
(data() + size())
위치에 복사 생성자를 호출하여 객체를 생성한다. - rvalue가 인자로 들어온 경우: 이동 생성자를 호출하여 객체를 생성한다.
- lvalue가 인자로 들어온 경우: 벡터의 끝
- 크기 증가: 벡터의
size
를 1 증가시킨다.
1.2. emplace_back
emplace_back
함수는 C++11에서 도입되었으며, 객체를 벡터 내에서 직접 생성하는 더 효율적인 방법을 제공한다.
template<class... Args> void emplace_back(Args&&... args)
: 객체를 생성하는 데 필요한 생성자 인자들을 직접 전달받는다.
emplace_back
은 perfect forwarding
을 사용하여 이 인자들을 벡터 내부의 새 요소가 위치할 메모리 공간으로 전달하고, 해당 위치에서 직접 생성자를 호출한다. 이를 "in-place construction" (제자리 생성)이라고 한다.
내부 구조 및 구현 방식
- Capacity 확인:
push_back
과 동일하게 공간을 확인한다. - 재할당 (Reallocation):
push_back
과 동일하게 공간이 없으면 재할당을 수행한다. - 요소 생성 (In-place): 벡터의 끝
(data() + size())
위치에placement new
를 사용하여 전달된 인자들(args...
)로 생성자를 직접 호출한다. 이 과정에서 임시 객체가 생성되지 않는다. - 크기 증가: 벡터의
size
를 1 증가시킨다.
1.3. push_back
vs emplace_back
비교
특징 | push_back |
emplace_back |
---|---|---|
인자 | 객체 (lvalue 또는 rvalue) | 객체의 생성자 인자들 |
핵심 동작 | 객체를 복사 또는 이동 | 객체를 제자리에서 생성 (in-place construction) |
임시 객체 | rvalue를 받을 때 임시 객체가 생성될 수 있음 | 임시 객체를 생성하지 않음 |
성능 | 복사/이동 비용 발생 | 생성자 호출 비용만 발생. 일반적으로 더 효율적 |
도입 시기 | C++98 | C++11 |
성능 비교 심층 분석
push_back(lvalue)
:T obj; v.push_back(obj);
obj
생성 (생성자 호출)- 벡터 내부에
obj
의 복사본 생성 (복사 생성자 호출)
push_back(rvalue)
:v.push_back(T());
- 임시 객체
T()
생성 (생성자 호출) - 벡터 내부로 임시 객체 이동 (이동 생성자 호출)
- 임시 객체
emplace_back
:v.emplace_back();
- 벡터 내부에서 바로 객체 생성 (생성자 호출)
결론적으로, emplace_back
은 불필요한 복사 및 이동 생성자 호출을 완전히 제거하므로 대부분의 경우 push_back
보다 빠르다. 특히 객체의 생성, 복사, 이동 비용이 큰 경우 (예: 긴 문자열, 다른 컨테이너를 멤버로 갖는 경우) 그 차이는 더욱 명확하다.
언제 무엇을 사용해야 하는가?
emplace_back
:- 새로운 객체를 생성하여 벡터에 추가할 때.
- 객체 생성에 필요한 인자들을 이미 알고 있을 때.
- 성능이 중요한 코드, 특히 반복문 안에서 요소를 추가할 때.
push_back
:- 이미 존재하는 객체(lvalue)를 벡터에 추가해야 할 때.
explicit
생성자를 가진 타입의 경우,emplace_back
에서 중괄호 초기화{}
를 사용하면 컴파일 에러가 발생할 수 있다. 이 때push_back
을 사용하면 더 명확할 수 있다. (예:v.push_back(MyType{1, 2});
)- 가독성이 더 중요하고 성능 차이가 미미할 때.
2. 동작 계층 구조
- 사용자 영역 (User Space):
my_vector.emplace_back("orc", 100);
코드가 실행.
- C++ 표준 라이브러리 (STL -
vector
구현):emplace_back
멤버 함수가 호출됩니다.- 내부적으로
size()
와capacity()
를 비교하여 재할당이 필요한지 검사. - 재할당이 없는 경우:
- 벡터가 관리하는 메모리 버퍼의 끝
(data() + size())
주소를 계산. std::allocator_traits::construct
를 호출하고, 이 함수는 내부적으로placement new
를 사용하여 해당 주소에 객체의 생성자를 호출.new(address) T("orc", 100);
와 유사하게 동작.
- 벡터가 관리하는 메모리 버퍼의 끝
- 재할당이 있는 경우:
std::allocator
를 통해 현재capacity
보다 큰 (보통 1.5배 또는 2배) 새로운 메모리 블록을 요청.- 기존 요소들을 새 메모리 블록으로 이동. (이동 생성자 호출)
- 이전 메모리 블록을 해제.
- 새 메모리 블록의 끝에
placement new
로 새 객체를 생성.
- 메모리 할당자 (
std::allocator
):- 재할당 시,
std::allocator::allocate
함수가 호출. - 이 함수는
::operator new
(전역new
연산자)를 호출하여 운영체제에 힙 메모리를 요청.
- 재할당 시,
- C 런타임 라이브러리 (CRT):
::operator new
는 내부적으로malloc()
과 같은 저수준 메모리 할당 함수를 호출.malloc
은 자체적인 힙 관리 알고리즘을 사용하여 적절한 크기의 메모리 블록을 찾거나, 없다면 운영체제에 추가 메모리를 요청.
- 운영체제 커널 (OS Kernel):
malloc
이 추가 메모리를 요청하면, 커널의 시스템 콜(예:brk
또는mmap
)이 호출.- 커널은 프로세스의 가상 주소 공간에 새로운 페이지를 할당하고, 이를 물리적 RAM에 매핑.
- 이 메모리 주소를 C 런타임 라이브러리에 반환하고, 최종적으로 사용자 코드의
vector
가 이 메모리를 사용.
3. 요약
Q: vector
의 push_back
과 emplace_back
의 차이점은 무엇인가요?
A: push_back
은 이미 생성된 객체를 인자로 받아 벡터의 끝에 복사하거나 이동하여 추가합니다. 반면, emplace_back
은 객체를 생성하는 데 필요한 인자들을 직접 전달받아 벡터의 끝 메모리 공간에서 객체를 직접 생성(in-place construction)합니다. 이 방식 덕분에 emplace_back
은 불필요한 임시 객체의 생성과 그에 따른 복사/이동 과정을 생략할 수 있어 일반적으로 더 효율적입니다.
Q: 어떤 경우에 emplace_back
을 사용하는 것이 좋은가요?
A: 성능이 중요하고, 벡터에 추가할 새로운 객체의 생성자 인자들을 알고 있을 때 emplace_back
을 사용하는 것이 가장 좋습니다. 특히 객체의 복사나 이동 비용이 큰 경우, 예를 들어 복잡한 문자열이나 다른 컨테이너를 멤버 변수로 가지는 객체를 추가할 때 성능 향상 효과가 극대화됩니다.
Q: push_back
에 임시 객체(rvalue)를 전달하는 것과 emplace_back
은 완전히 동일한가요?
A: 아닙니다. push_back
에 임시 객체를 전달하면, 먼저 임시 객체가 생성된 후 벡터 내부로 '이동'됩니다. 즉, 생성자 1번, 이동 생성자 1번이 호출됩니다. 하지만 emplace_back
은 벡터 내부에서 바로 생성자를 호출하여 객체를 만들기 때문에, 이동 과정 자체가 생략됩니다. 따라서 임시 객체 생성 및 이동이라는 오버헤드가 없어 emplace_back
이 더 효율적입니다.
4. 실습 코드
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
// 게임 몬스터를 표현하는 클래스
class Monster {
public:
// 생성자: 몬스터의 이름과 체력을 받아 초기화합니다.
Monster(const std::string& name, int hp) : name_(name), hp_(hp) {
std::cout << " [생성] " << name_ << " (체력: " << hp_ << ") 몬스터 생성됨" << std::endl;
}
// 복사 생성자
Monster(const Monster& other) : name_(other.name_), hp_(other.hp_) {
std::cout << " [복사] " << name_ << " 몬스터 복사됨" << std::endl;
}
// 이동 생성자
Monster(Monster&& other) noexcept : name_(std::move(other.name_)), hp_(other.hp_) {
std::cout << " [이동] " << name_ << " 몬스터 이동됨" << std::endl;
other.hp_ = 0; // 이동 후 원본 객체는 비워진 상태로 만듭니다.
}
// 소멸자
~Monster() {
// 소멸자 호출이 너무 많아 보여서 주석 처리합니다.
// std::cout << " [소멸] " << name_ << " 몬스터 소멸됨" << std::endl;
}
void PrintInfo() const {
std::cout << " 몬스터: " << name_ << ", 체력: " << hp_ << std::endl;
}
private:
std::string name_;
int hp_;
};
int main() {
// 몬스터 부대를 관리할 벡터
std::vector<Monster> monster_squad;
std::cout << "벡터의 초기 capacity: " << monster_squad.capacity() << std::endl;
std::cout << "\n--- push_back (lvalue) 테스트 ---" << std::endl;
Monster goblin("고블린", 100); // 1. 몬스터 객체를 미리 생성
monster_squad.push_back(goblin); // 2. 생성된 객체를 push_back으로 추가
// Q1: 위 코드 블록(1, 2)이 실행될 때, Monster 클래스의 생성자, 복사 생성자, 이동 생성자
// 중 어떤 것들이 호출될까요? 그리고 그 이유는 무엇인가요?
// 제출:
std::cout << "\n--- push_back (rvalue) 테스트 ---" << std::endl;
monster_squad.push_back(Monster("오크", 200)); // 3. 임시 객체를 생성하여 push_back으로 추가
// Q2: 위 코드(3)가 실행될 때, Monster 클래스의 생성자, 복사 생성자, 이동 생성자 중 어떤 것들
// 이 호출될까요? push_back(lvalue) 방식과 비교했을 때 어떤 차이가 있나요?
// 제출:
std::cout << "\n--- emplace_back 테스트 ---" << std::endl;
monster_squad.emplace_back("트롤", 500); // 4. 생성자 인자를 직접 전달하여 emplace_back으로 추가
// Q3: 위 코드(4)가 실행될 때, Monster 클래스의 생성자, 복사 생성자, 이동 생성자 중 어떤 것들이
// 호출될까요? push_back(rvalue) 방식과 비교했을 때 어떤 점이 더 효율적인가요?
// 제출:
std::cout << "\n--- 벡터 capacity 변경 시 동작 확인 ---" << std::endl;
std::cout << "현재 몬스터 수: " << monster_squad.size() << ", 현재 capacity: " << monster_squad.capacity() << std::endl;
std::cout << "새로운 몬스터를 추가하여 capacity 확장을 유도합니다." << std::endl;
monster_squad.emplace_back("드래곤", 1000);
// Q4: 위 코드가 실행될 때, 벡터의 capacity가 변경되면서 기존에 있던 몬스터 객체들("고블린", "오크", "트롤")
// 에는 어떤 일이 발생할까요? Monster 클래스의 어떤 생성자가 호출될지 예상해보세요.
// 제출:
std::cout << "\n--- 최종 몬스터 부대 정보 ---" << std::endl;
for (const auto& monster : monster_squad) {
monster.PrintInfo();
}
return 0;
}
'C++' 카테고리의 다른 글
std::sort와 std::list의 sort (0) | 2025.09.18 |
---|---|
vector의 size와 capacity (0) | 2025.09.16 |
std::vector vs std::map 비교 (0) | 2025.09.12 |
std::vector (0) | 2025.09.12 |
std::map (1) | 2025.09.11 |