C++ vector reserve vs resize: When to Use Which
Key takeaways
reserve allocates room without creating elements; resize creates or destroys elements. Mixing them up causes out-of-range writes, doubled sizes, and quadratic reallocation.
Introduction
reserve and resize both “make a vector bigger”, which is why they get confused. They change two different numbers:
size(): how many elements exist. Valid indices are0tosize() - 1.capacity(): how many elements fit in the current allocation before the vector must reallocate.
reserve(n) changes only capacity. resize(n) changes size (and grows capacity if needed). Everything else in this article follows from that.
The output below is from GCC 10 with libstdc++. The standard guarantees only “at least n”, so exact capacities can differ on other implementations.
std::vector<int> v;
v.reserve(100); // size 0, capacity 100
v.resize(100); // size 100, capacity 100
v.resize(10); // size 10, capacity 100 (elements destroyed, memory kept)
v.reserve(5); // size 10, capacity 100 (reserve never shrinks)
v.shrink_to_fit();// size 10, capacity 10 (non-binding request, honored here)
What each call actually does
reserve(n)
If n > capacity(), the vector allocates a new buffer that holds at least n elements, moves (or copies) the existing elements into it, and frees the old buffer. No new elements are created. If n <= capacity(), nothing happens. If n > max_size(), it throws std::length_error (libstdc++‘s message is just vector::reserve).
Because reserve may reallocate, it invalidates all iterators, pointers and references into the vector, exactly like a reallocating push_back. After it, push_back is guaranteed not to reallocate until size() reaches the reserved capacity. Pointers taken after the reserve stay valid until then.
resize(n) and resize(n, value)
If n > size(), the vector appends n - size() new elements: value-initialized (0 for int, default constructor for classes) or copies of value. If n < size(), it destroys the elements past n. Capacity grows if needed but never shrinks.
Two consequences are easy to miss:
resize(n)requiresTto be default-constructible. For a type without a default constructor,resize(n)fails to compile, and you must useresize(n, value)orreserve+emplace_back.- Value-initialization is real work.
resize(1'000'000)on avector<int>writes a million zeros, and for a class type it runs a million constructors, even if you overwrite every element right away.
The bugs this confusion causes
Writing by index after reserve
std::vector<int> v;
v.reserve(5);
v[0] = 42; // undefined behavior: size() is 0
v.at(0) = 42; // throws std::out_of_range
The operator[] write lands inside memory the vector owns, so it usually does not crash. It is still broken. size() stays 0, a later push_back overwrites your value, and range-for loops skip it. The first time I ran into this, the code “worked” for weeks because every write happened to be read back through the same wrong index. Building with -D_GLIBCXX_ASSERTIONS makes libstdc++ abort on the bad operator[], which is how this kind of bug usually gets caught.
resize followed by push_back
std::vector<int> x;
x.resize(3);
for (int i = 0; i < 3; ++i) x.push_back(i);
// x.size() == 6, x == {0, 0, 0, 0, 1, 2}
resize already created three zeros, and push_back appends after them. This one is common when someone “optimizes” a push_back loop by adding resize where reserve was intended. The output looks roughly right, and only the extra leading zeros hint at the bug.
reserve inside the loop
for (int i = 0; i < n; ++i) {
v.reserve(v.size() + 1); // looks careful, is quadratic
v.push_back(i);
}
A plain push_back loop is amortized O(1) per element because the vector grows geometrically: libstdc++ and libc++ double the capacity, and MSVC grows it by 1.5x. Filling one million ints with plain push_back reallocated 21 times in my libstdc++ test. reserve(size() + 1) asks for exactly one more slot. In the same test with 1,000 elements, it reallocated all 1,000 times, and each reallocation moves every existing element. Reserve once, with the final (or estimated) size, before the loop.
Performance: what reserve saves and what resize costs
Without reserve, filling a vector of n elements causes about log2(n) reallocations. Each moves all current elements, so the total work is still O(n). The constant factor comes from extra allocations, extra moves, and, for large vectors, touching roughly twice the memory. reserve removes all of that when you know the size up front.
resize + index assignment avoids reallocations too, but it pays for initializing each element before overwriting it. For int that is a cheap memset-like pass. For types with non-trivial constructors, it can matter more. reserve + push_back avoids the double write but adds a capacity check on every push. Which one is faster depends on the element type and the compiler, so measure it for your case instead of trusting a generic benchmark table.
When the elements’ types have a move constructor that can throw and is not marked noexcept, reallocation copies instead of moving to preserve the strong exception guarantee. That makes each reallocation much more expensive, and reserve saves correspondingly more. Marking move constructors noexcept matters for vectors of your own types either way.
Choosing
| Situation | Use |
|---|---|
Appending with push_back/emplace_back, size known or estimated | reserve(n) once, before the loop |
Need n elements to exist now, filled by index | resize(n) |
Need n copies of a value | resize(n, value) or the constructor vector<T>(n, value) |
| Element type has no default constructor | reserve + emplace_back |
| Holding pointers into the vector while appending | reserve first, take pointers after |
| Truncating | resize(smaller), then shrink_to_fit() if memory matters |
// Unknown count, rough estimate available: reserve is only a hint
std::vector<std::string> lines;
lines.reserve(1000);
std::string line;
while (std::getline(file, line)) lines.push_back(std::move(line));
// Exact size, filled by index (e.g. from a C API)
std::vector<char> buf;
buf.resize(len);
ssize_t got = read(fd, buf.data(), buf.size());
if (got < 0) { /* handle errno */ }
buf.resize(static_cast<std::size_t>(got)); // trim to what was actually read
// 2D grid: the constructor is clearer than resize loops
std::vector<std::vector<int>> grid(rows, std::vector<int>(cols, 0));
The read example shows a legitimate reason to prefer resize over reserve: a C API writes into data(), and those bytes only count as elements if size() covers them. Writing through data() into reserved-but-unsized memory is the same bug as v[i] after reserve.
Note the signed result. POSIX read returns ssize_t, and -1 on error. Storing that in a std::size_t turns the error into a huge positive number, and buf.resize(got) then throws std::length_error or std::bad_alloc far from the real cause. Check the sign before resizing.
The reserve(1000) for lines is only a guess, and that is fine. If the file has fewer lines, some memory goes unused until the vector is destroyed; if it has more, the vector simply resumes normal geometric growth from 1000. A wrong estimate never breaks correctness, which is what makes reserve safe to use liberally. The one thing to avoid is an estimate that is wildly too large, such as reserving based on a count read from untrusted input: reserve(n) with an attacker-controlled n is an easy way to make a program allocate gigabytes or throw std::bad_alloc before it reads a single element.
Reusing capacity across iterations
clear() destroys all elements but, like a shrinking resize, keeps the allocation. That makes a vector declared outside a loop and cleared at the top of each iteration an effective buffer: after the first few iterations it has grown to the largest size needed and never allocates again.
std::vector<Token> tokens;
for (const auto& line : lines) {
tokens.clear(); // size 0, capacity kept
tokenize(line, tokens); // push_back into existing capacity
process(tokens);
}
The same property is a memory trap in long-running programs. A vector that once held a million elements keeps that capacity after clear() or resize(0), so a cache or queue that briefly spiked stays large. When memory matters, call shrink_to_fit() after the spike, or use the older swap idiom, std::vector<T>().swap(v);, which is guaranteed to release the buffer because the temporary takes it and is destroyed.
For vector<char> or vector<unsigned char> buffers that a C API fills completely, the zero-fill done by resize is wasted work, but it is usually small compared to the I/O itself. If profiling shows otherwise, std::unique_ptr<char[]>(new char[n]) gives uninitialized storage, and for strings C++23 adds std::string::resize_and_overwrite, which skips the initialization and lets you set the final length in one step. There is no standard equivalent for std::vector.
Summary
reserve is a promise about memory: “I’m about to add this many”. resize is a change to contents: “there are now exactly this many”. Use reserve once before appending, resize when elements must exist for indexing or for a C API to fill. Never index into reserved space, and never reserve inside the loop that fills the vector.