The shortest way to join two vectors in C++ is a.insert(a.end(), b.begin(), b.end());, which appends everything in b onto the end of a in one call. It is a single line, it works in every standard since C++98, and because insert knows the size of the range up front it grows the destination exactly once. Four other approaches exist and each wins in a specific situation.
a.insert(a.end(), b.begin(), b.end()) to append in place. To build a new vector, call reserve(a.size() + b.size()) first and then copy both ranges: in my benchmark, skipping the reserve made the operation about two and a half times slower. For vectors of expensive objects you no longer need, use std::move instead of copying.Every sample below was compiled and run with GCC 13.3 using g++ -std=c++20 -Wall -Wextra, and the benchmark numbers come from an actual run with -O2 on the same machine. Timings will differ on your hardware, but the ordering between the approaches holds.
Method 1: insert, the default choice
#include <iostream>
#include <vector>
int main() {
std::vector<int> a{1, 2, 3};
std::vector<int> b{4, 5, 6};
a.insert(a.end(), b.begin(), b.end());
for (int v : a) {
std::cout << v << ' ';
}
std::cout << "\nsize = " << a.size() << '\n';
return 0;
}# 1 2 3 4 5 6
# size = 6Because both iterators are random access, the library can compute the distance in constant time and perform exactly one reallocation. This is why insert beats a loop of push_back calls, which reallocates several times as the vector grows.
a.insert(a.end(), a.begin(), a.end()) to duplicate a vector onto itself. The reallocation invalidates the source iterators mid operation and the result is undefined behavior. Copy to a temporary first, or use a.reserve(a.size() * 2) before the insert so no reallocation can happen.Method 2: copy with back_inserter
When the destination is a third vector rather than one of the inputs, std::copy with a back_inserter reads well and composes with the rest of the algorithms library.
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> a{1, 2, 3};
std::vector<int> b{4, 5, 6};
std::vector<int> joined;
joined.reserve(a.size() + b.size());
std::copy(a.begin(), a.end(), std::back_inserter(joined));
std::copy(b.begin(), b.end(), std::back_inserter(joined));
for (int v : joined) {
std::cout << v << ' ';
}
std::cout << '\n';
return 0;
}The reserve call is not optional if you care about speed. A back_inserter calls push_back for every element, so without it the vector reallocates and copies repeatedly as it grows. The benchmark below quantifies exactly how much that costs.
Method 3: ranges in C++20 and C++23
The ranges algorithms take the container directly instead of a pair of iterators, which removes a whole class of typo where you pass a.begin() and b.end().
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> a{1, 2, 3};
std::vector<int> b{4, 5, 6};
std::vector<int> joined;
joined.reserve(a.size() + b.size());
std::ranges::copy(a, std::back_inserter(joined));
std::ranges::copy(b, std::back_inserter(joined));
for (int v : joined) {
std::cout << v << ' ';
}
std::cout << '\n';
// A lazy view over both, no allocation at all.
std::vector<std::vector<int>> both{a, b};
for (int v : both | std::views::join) {
std::cout << v << ' ';
}
std::cout << '\n';
return 0;
}# 1 2 3 4 5 6
# 1 2 3 4 5 6The second half is worth pausing on. std::views::join produces a lazy view: nothing is copied and no memory is allocated for a combined sequence. If all you need is to iterate over the two vectors as though they were one, that is strictly cheaper than building a real vector. You only pay when you materialize.
Method 4: append_range in C++23
C++23 added member functions that take a range directly. Guard the call so the code still builds on a C++20 toolchain.
#include <iostream>
#include <vector>
int main() {
std::vector<int> a{1, 2, 3};
std::vector<int> b{4, 5, 6};
// append_range is C++23; guard it so C++20 builds still work.
#if __cpp_lib_containers_ranges >= 202202L
a.append_range(b);
std::cout << "used append_range\n";
#else
a.insert(a.end(), b.begin(), b.end());
std::cout << "fell back to insert\n";
#endif
for (int v : a) {
std::cout << v << ' ';
}
std::cout << '\n';
return 0;
}On the GCC 13.3 build I tested, this printed fell back to insert under both -std=c++20 and -std=c++23, because that release of libstdc++ does not yet define the feature test macro. That is exactly why you test the macro instead of the standard version: the language mode and the library implementation move on different schedules. Newer GCC and Clang releases do provide it.
Method 5: move for expensive elements
When the elements are strings, vectors, or anything else with a heap allocation, copying them is wasted work if the source is about to be thrown away.
#include <iostream>
#include <vector>
#include <string>
#include <iterator>
#include <algorithm>
int main() {
std::vector<std::string> a{"alpha", "beta"};
std::vector<std::string> b{"gamma", "delta"};
a.reserve(a.size() + b.size());
std::move(b.begin(), b.end(), std::back_inserter(a));
b.clear();
for (const auto& s : a) {
std::cout << s << ' ';
}
std::cout << "\nb.size() = " << b.size() << '\n';
return 0;
}# alpha beta gamma delta
# b.size() = 0std::move, the elements of b are in a valid but unspecified state. b still has its original size and you can safely destroy it or assign new values, but you must not read what is in it. The b.clear() above makes that explicit so a later reader cannot get it wrong. Note also that std::move here is the algorithm from <algorithm> taking three arguments, not the one argument cast from <utility>.Benchmark: what reserve is actually worth
I joined two vectors of five million int values into a fresh vector, taking the best of seven runs for each approach, compiled with -O2. Every variant produced the same ten million element result.
| Approach | Best of 7 | Relative | Reallocations |
|---|---|---|---|
ranges::copy with reserve | 33.5 ms | 1.00x | 1 |
insert twice with reserve | 35.5 ms | 1.06x | 1 |
copy plus back_inserter with reserve | 38.8 ms | 1.16x | 1 |
copy plus back_inserter, no reserve | 92.1 ms | 2.75x | About 24 |
The interesting result is not the small spread among the first three. It is that the only variant without reserve is nearly three times slower than the rest. Growth is geometric, so the vector reallocates on the order of two dozen times and copies every element it already holds on each of those. Choose whichever of the first three reads best in your codebase, but do not skip the reserve.
Joining more than two vectors
The same principle scales. Sum the sizes first, reserve once, then append each piece. This is the shape to reach for whenever you are flattening results from several workers or several files.
#include <iostream>
#include <vector>
std::vector<int> join_all(const std::vector<std::vector<int>>& parts) {
size_t total = 0;
for (const auto& p : parts) {
total += p.size();
}
std::vector<int> out;
out.reserve(total);
for (const auto& p : parts) {
out.insert(out.end(), p.begin(), p.end());
}
return out;
}
int main() {
std::vector<std::vector<int>> parts{{1, 2}, {3, 4, 5}, {}, {6}};
std::vector<int> all = join_all(parts);
std::cout << "size = " << all.size() << ", capacity = " << all.capacity() << '\n';
for (int v : all) {
std::cout << v << ' ';
}
std::cout << '\n';
return 0;
}# size = 6, capacity = 6
# 1 2 3 4 5 6Capacity comes out equal to size, which confirms the reserve was exact and nothing reallocated. The empty vector in the middle costs nothing. Returning by value is free here because the compiler elides the copy.
Which method to use
| Situation | Use | Standard |
|---|---|---|
| Append b onto a | a.insert(a.end(), b.begin(), b.end()) | C++98 |
| Build a third vector | reserve then two copies | C++98 |
| Only need to iterate | std::views::join | C++20 |
| Source is disposable | std::move algorithm | C++11 |
| Modern toolchain, terse code | a.append_range(b) | C++23 |
Troubleshooting
The program crashes or produces garbage after the join. You held an iterator, pointer or reference into the destination across the insert. Any operation that grows a vector past its capacity invalidates all of them. Take indices instead of iterators when you need a position to survive an insert.
error: no matching function for call to ‘insert’. The element types differ. Vectors of int and long will not join directly. Use std::transform with an explicit conversion, or make the types match.
Joining is unexpectedly slow in a loop. You are joining inside a loop that runs many times, so each pass copies the whole accumulated result. Collect the pieces first and join once at the end, sizing the destination with a single reserve.
The source vector still has its old contents after std::move. That is allowed. Moved from objects are in a valid but unspecified state, and for small strings the short string optimization often means nothing visibly changed. Never rely on either outcome. Call clear() if you want a defined result.
append_range does not compile. Your standard library predates C++23 container range support, which is separate from the -std flag you passed. Check the __cpp_lib_containers_ranges feature test macro as shown above and fall back to insert.
Frequently asked questions
Is insert faster than push_back in a loop?
Yes, meaningfully. Both iterators are random access, so insert computes the total size once and reallocates once. A loop of push_back calls reallocates repeatedly as the vector grows, copying everything already stored each time.
How do I join two vectors without modifying either one?
Create a third vector, call reserve(a.size() + b.size()) on it, then copy both ranges in. Alternatively, if you only need to read the elements in sequence, iterate a std::views::join view and allocate nothing at all.
Can I join vectors of different types?
Not directly. The destination’s element type must be constructible from the source’s. Use std::transform with a lambda that performs the conversion, and be explicit about it so a narrowing conversion cannot slip through unnoticed.
Does reserve actually matter for small vectors?
Not measurably for a handful of elements. It starts to matter in the thousands and becomes the dominant cost in the millions, where my benchmark showed a factor of nearly three. Since the call costs one line, adding it by habit is cheaper than deciding each time.
What happens to capacity after joining?
The destination’s capacity grows to at least the combined size, and it does not shrink when you later remove elements. If the vector was temporarily huge and you want the memory back, use the shrink to fit idiom: std::vector<int>(a).swap(a), or call a.shrink_to_fit(), which is a non binding request.
The bottom line
Use insert to append and a reserved third vector to combine. Those two cover almost everything, they work on every compiler you are likely to meet, and they are within a few percent of the fastest option in the benchmark above.
The measurement worth carrying away is that the choice of algorithm barely matters and the presence of reserve matters a lot. If you learn one habit from this, make it sizing the destination before you fill it. For related C++ topics, see our guides on storing text file data in a vector, declaring float variables and using threads in C++. The vector insert reference documents the exact complexity guarantees quoted here.
