C++/Concepts
std::vector reallocation과 noexcept move constructor
C++에서 move constructor를 작성할 때 noexcept를 붙여야 하는지 고민되는 순간이 있습니다.
단순히 “예외를 던지지 않으니까 붙인다” 정도로만 이해하면 std::vector 같은 container에서 중요한 성능 차이를 놓칠 수 있습니다.
std::vector는 capacity가 부족해져 reallocation이 발생하면 기존 element를 새 memory buffer로 옮겨야 합니다.
이때 move constructor가 noexcept인지 여부는 기존 element를 move할지 copy할지 결정하는 중요한 힌트가 됩니다.
std::vector reallocation에서 noexcept move constructor는 “이 객체는 안전하게 move해도 된다”는 신호가 됩니다.

1. 들어가며
std::move를 배웠다면 move가 copy보다 항상 빠르다고 생각하기 쉽습니다.
하지만 standard container는 성능만 보지 않습니다.
Container는 element를 옮기는 중 예외가 발생했을 때 기존 상태를 얼마나 안전하게 유지할 수 있는지도 고려해야 합니다.
특히 std::vector는 연속된 memory를 사용하기 때문에 capacity가 부족해지면 새 buffer를 만들고 기존 element를 새 buffer로 옮깁니다.
이 과정에서 move가 예외를 던질 수 있다면, container 입장에서는 copy가 더 안전한 선택일 수 있습니다.
| 구분 | 의미 | std::vector reallocation에서의 영향 |
| copy constructor | 기존 객체를 보존한 채 새 객체를 만듦 | move가 위험하다고 판단되면 선택될 수 있음 |
| move constructor | 기존 객체의 resource를 새 객체로 이전 | 빠를 수 있지만 예외 가능성이 있으면 container가 조심함 |
| noexcept move constructor | move 중 예외를 던지지 않는다고 선언 | 기존 element 재배치에서 move가 선택되기 쉬움 |
| reallocation | capacity 부족으로 새 buffer를 할당하고 element를 옮기는 과정 | 많은 element에서 copy/move 차이가 누적됨 |
이 글에서는 C++17 기준으로 noexcept move constructor가 왜 중요한지, std::vector reallocation에서 어떤 차이를 만드는지 예제 코드로 확인해보겠습니다.
2. 왜 중요한가?
실무에서 class가 string, vector, unique_ptr, file handle, socket 같은 resource를 들고 있다면 move constructor를 직접 작성하거나 compiler-generated move에 의존하게 됩니다.
이때 move가 실제로 빠르게 사용되려면 container가 그 move를 믿고 사용할 수 있어야 합니다.
- std::vector는 reallocation 때 기존 element를 새 buffer로 옮깁니다.
- move constructor가 noexcept이면 기존 element를 move해도 rollback 위험이 작습니다.
- move constructor가 noexcept가 아니고 copy가 가능하면 copy가 선택될 수 있습니다.
- element 수가 많고 copy 비용이 크면 reallocation 비용 차이가 커집니다.
- 성능뿐 아니라 exception safety를 코드로 표현한다는 점에서도 중요합니다.
여기서 중요한 점은 noexcept가 단순한 최적화 장식이 아니라는 것입니다.
noexcept는 library에게 “이 move는 실패하지 않는다”는 contract를 제공하고, container는 그 contract를 바탕으로 더 공격적인 move 전략을 선택할 수 있습니다.
3. 핵심 개념
std::vector는 element를 연속된 memory에 저장합니다.
capacity가 부족한 상태에서 새 element가 들어오면 더 큰 memory buffer를 만들고, 기존 element를 새 buffer로 옮긴 뒤 기존 buffer를 정리합니다.
이때 기존 element를 move하다가 예외가 발생하면 vector는 중간 상태를 복구해야 합니다.
Copy는 원본 element를 그대로 둔 채 새 객체를 만들기 때문에 rollback 관점에서 더 다루기 쉬운 경우가 있습니다.
| 조건 | container가 보기 쉬운 선택 | 이유 |
| move constructor가 noexcept | move | move 중 예외가 없다고 선언되어 rollback 부담이 작음 |
| move constructor가 noexcept가 아님 | copy | copy가 가능하면 기존 element를 보존하는 쪽이 안전할 수 있음 |
| copy constructor가 없음 | move | copy 선택지가 없으므로 move를 사용할 수밖에 없음 |
| trivial type 또는 작은 type | 차이가 작을 수 있음 | copy/move 비용보다 allocation 비용이 더 클 수 있음 |
실제 standard library 구현은 이런 판단을 위해 std::move_if_noexcept 계열의 전략을 사용합니다.
즉, move가 noexcept이면 move하고, 그렇지 않고 copy가 가능하면 copy를 선택할 수 있습니다.
그래서 move constructor를 직접 작성했다면 noexcept를 붙일 수 있는지 반드시 검토해야 합니다.
4. 예제 코드
아래 예제는 move constructor에 noexcept가 없는 타입과 있는 타입을 비교합니다.
std::vector에 element 하나를 넣고 capacity를 1로 고정한 뒤, 두 번째 element를 넣어 reallocation을 강제로 발생시킵니다.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct RiskyMove {
static int copies;
static int moves;
std::string value;
explicit RiskyMove(std::string text)
: value(std::move(text))
{
}
RiskyMove(const RiskyMove& other)
: value(other.value)
{
++copies;
}
RiskyMove(RiskyMove&& other)
: value(std::move(other.value))
{
++moves;
}
};
int RiskyMove::copies = 0;
int RiskyMove::moves = 0;
struct SafeMove {
static int copies;
static int moves;
std::string value;
explicit SafeMove(std::string text)
: value(std::move(text))
{
}
SafeMove(const SafeMove& other)
: value(other.value)
{
++copies;
}
SafeMove(SafeMove&& other) noexcept
: value(std::move(other.value))
{
++moves;
}
};
int SafeMove::copies = 0;
int SafeMove::moves = 0;
template <typename T>
void trigger_reallocation()
{
std::vector<T> items;
items.reserve(1);
items.emplace_back("first");
items.emplace_back("second");
}
int main()
{
trigger_reallocation<RiskyMove>();
std::cout << "RiskyMove copies: " << RiskyMove::copies
<< ", moves: " << RiskyMove::moves << '\n';
trigger_reallocation<SafeMove>();
std::cout << "SafeMove copies: " << SafeMove::copies
<< ", moves: " << SafeMove::moves << '\n';
}
실행 결과
RiskyMove copies: 1, moves: 0
SafeMove copies: 0, moves: 1
RiskyMove는 move constructor가 있지만 noexcept가 아닙니다.
그래서 reallocation 중 기존 element를 옮길 때 copy가 선택되었습니다.
SafeMove는 move constructor가 noexcept이므로 기존 element가 copy되지 않고 move되었습니다.
5. 실행 결과에서 봐야 할 것
예제의 핵심은 새로 들어오는 second element가 아니라 기존에 있던 first element입니다.
items.reserve(1) 이후 items.emplace_back("first")를 호출하면 capacity가 1인 vector에 element 하나가 들어갑니다.
다음 items.emplace_back("second")에서 capacity가 부족해져 reallocation이 발생하고, 기존 first element를 새 buffer로 옮겨야 합니다.
상태 변화
reserve(1)
capacity 1 확보
emplace_back("first")
기존 buffer에 first 생성
emplace_back("second")
capacity 부족
새 buffer 할당
기존 first element를 새 buffer로 재배치
RiskyMove
move constructor가 noexcept가 아님
copy constructor가 있으므로 copy 선택
SafeMove
move constructor가 noexcept
기존 element를 move
결과가 보여주는 것은 std::move 문법 자체가 아니라 container의 선택입니다.
move constructor가 존재해도 noexcept가 아니면 vector reallocation에서 copy가 선택될 수 있습니다.
6. 그림으로 이해하기

그림에서 중요한 분기점은 move constructor가 noexcept인지 여부입니다.
noexcept가 없다면 container는 move 중 예외 가능성을 고려해야 하고, copy가 가능하면 copy 경로를 선택할 수 있습니다.
noexcept가 있다면 기존 element를 move해도 실패하지 않는다는 contract가 생기므로 reallocation 비용을 줄일 수 있습니다.
7. 자주 하는 오해
- move constructor가 있으면 std::vector는 항상 move한다.
move constructor가 있어도 noexcept가 아니고 copy가 가능하면 reallocation에서 copy가 선택될 수 있습니다. - noexcept는 성능과 관계없는 예외 명세일 뿐이다.
noexcept는 library가 안전한 move를 선택할 수 있게 만드는 중요한 정보입니다. - std::move를 쓰면 무조건 move가 발생한다.
std::move는 cast입니다. 실제로 어떤 constructor가 호출되는지는 overload resolution과 container의 내부 전략에 따라 달라집니다. - move가 copy보다 항상 빠르다.
작은 type이나 trivial type에서는 차이가 거의 없을 수 있습니다. 하지만 resource를 가진 큰 object에서는 차이가 커질 수 있습니다. - noexcept는 아무 move constructor에나 붙이면 된다.
정말 예외를 던지지 않을 때만 붙여야 합니다. noexcept 함수에서 예외가 밖으로 나가면 std::terminate가 호출됩니다.
noexcept는 compiler와 standard library에게 보내는 약속입니다.
약속할 수 없는 경우에는 붙이면 안 되지만, 약속할 수 있는 move constructor라면 붙이지 않는 것도 비용이 될 수 있습니다.
8. 실무에서는 어떻게 볼까?
실무에서는 custom type을 container에 많이 넣는지부터 봐야 합니다.
std::vector<T>에 T가 많이 들어가고, T가 string, vector, unique_ptr, buffer 같은 resource를 갖고 있다면 move constructor의 noexcept 여부를 확인할 가치가 큽니다.
특히 성능 문제가 있는 code path에서 vector growth가 자주 발생한다면 copy/move count를 측정해보는 것이 좋습니다.
| 코드 리뷰 질문 | 확인할 내용 | 권장 방향 |
| move constructor를 직접 작성했는가? | 내부 member들의 move가 예외를 던질 수 있는지 확인 | 가능하면 noexcept 명시 |
| std::vector에 많이 들어가는 type인가? | reallocation이 자주 발생하는지 확인 | reserve와 noexcept move를 함께 검토 |
| copy 비용이 큰가? | string, buffer, nested container, handle 보유 여부 확인 | move 경로가 실제로 사용되는지 측정 |
| member type의 move가 noexcept인가? | 직접 작성하지 않은 member들의 noexcept 상태 확인 | defaulted move constructor의 noexcept 추론 활용 |
| noexcept를 거짓으로 선언했는가? | 예외 가능성이 있는데 noexcept를 붙였는지 확인 | 잘못된 noexcept는 std::terminate 위험 |
좋은 기본 전략은 직접 move constructor를 작성하지 않아도 되는 구조를 먼저 만드는 것입니다.
Compiler-generated move constructor가 member들의 noexcept 상태를 바탕으로 적절히 noexcept를 추론할 수 있기 때문입니다.
직접 작성해야 한다면 함수 body에서 예외가 나갈 수 없는지 확인하고 noexcept를 명시하는 편이 좋습니다.
9. 정리
std::vector는 reallocation 때 기존 element를 새 buffer로 옮겨야 합니다.
move constructor가 noexcept이면 vector가 기존 element를 move하기 쉬워집니다.
move constructor가 noexcept가 아니고 copy가 가능하면 copy가 선택될 수 있습니다.
noexcept는 성능 최적화 힌트이면서 exception safety contract입니다.
Container에 많이 들어가는 custom type이라면 move constructor의 noexcept 여부를 코드 리뷰에서 확인해야 합니다.
'C++ > Concepts' 카테고리의 다른 글
| C++ Copy Elision과 RVO/NRVO 제대로 이해하기 (1) | 2026.07.30 |
|---|---|
| C++ lock ownership 제대로 이해하기 (1) | 2026.07.25 |
| if constexpr는 일반 if와 무엇이 다를까? (0) | 2026.07.20 |
| C++ Callback 코드에서 Lambda Capture를 안전하게 쓰는 법 (0) | 2026.07.16 |
| std::variant active alternative와 std::visit 제대로 이해하기 (0) | 2026.07.13 |
'C++/Concepts'의 다른글
- 현재글std::vector reallocation과 noexcept move constructor