C++ Copy Algorithms: std::copy, copy_if, copy_n

Key takeaways

std::copy and its variants let you transfer ranges between containers with clear intent and no boilerplate. This guide covers the full family — copy, copy_if, copy_n, copy_backward, and move — with working examples and the overlap gotcha.

Why Use STL Copy Algorithms?

Hand-written copy loops work, but STL algorithms express intent more clearly and let the compiler optimize better (trivially copyable types often compile to memmove):

// Hand loop — verbose, easy to get range wrong
for (size_t i = 0; i < src.size(); ++i) {
    dst[i] = src[i];
}

// STL — clear intent, range-safe
std::copy(src.begin(), src.end(), dst.begin());

// With growth — no manual resize needed
std::copy(src.begin(), src.end(), std::back_inserter(dst));

The copy algorithm family in <algorithm>:

AlgorithmCopiesSelection
std::copyFull range [first, last)All elements
std::copy_ifFull rangeElements where predicate is true
std::copy_nFirst n elementsCount-based
std::copy_backwardFull range (right-to-left)All elements
std::moveFull range (moves)All elements, sources become moved-from

Every algorithm in this family shares the same underlying contract, and understanding that contract up front saves you from re-deriving it separately for each function: they all operate purely on iterator ranges, they perform no bounds checking whatsoever, and none of them know anything about the container backing those iterators. std::copy cannot tell whether dst.begin() points into a std::vector<int> with room for five more elements or a std::vector<int> with room for zero — it just dereferences and assigns, last - first times, and trusts the caller to have made room. This is the same “algorithms and containers are separate” design that shows up across all of <algorithm> (heap operations, sorting, searching), and it is precisely why back_inserter exists: it is an output iterator that turns each assignment into a push_back() call instead of an in-place write, which is how you get “safe,” growing destinations without the algorithm itself needing to know anything about resizing.

The second contract worth internalizing before you touch any of these functions is the overlap rule, because it is the single most common source of silent, hard-to-debug corruption in code that uses this family. std::copy is only well-defined when the destination’s first iterator does not fall inside the source range [first, last). That sounds abstract, but it has a very concrete consequence: shifting elements left within the same container (destination starts before or at the source) is safe, while shifting elements right within the same container (destination starts inside the source range) is undefined behavior — silently wrong output, not a crash, which is what makes it dangerous. std::copy_backward exists specifically to handle that second case correctly, and the section below walks through exactly why.


std::copy

The most common: copy every element from source to destination.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};

    // Destination pre-sized
    std::vector<int> dst1(src.size());
    std::copy(src.begin(), src.end(), dst1.begin());
    // dst1: {1, 2, 3, 4, 5}

    // Destination grows automatically
    std::vector<int> dst2;
    std::copy(src.begin(), src.end(), std::back_inserter(dst2));
    // dst2: {1, 2, 3, 4, 5}

    // Copy to array
    int arr[5];
    std::copy(src.begin(), src.end(), arr);

    // Copy to output stream
    std::copy(src.begin(), src.end(),
        std::ostream_iterator<int>(std::cout, " "));
    // prints: 1 2 3 4 5
}

Return value: iterator to one past the last element written in the destination. Useful if you need to know where copying ended.

Why it exists and how it works: std::copy is a thin, generic wrapper over “assign *result++ = *first++ until first == last.” That is genuinely all it does — no allocation, no bounds checking, no type conversion beyond whatever operator= the destination’s value_type supports. The value of reaching for it instead of a raw for loop is not runtime performance in the common case (a hand-rolled loop over ints compiles to the same instructions) but expressiveness and safety of composition: std::copy(src.begin(), src.end(), std::ostream_iterator<int>(std::cout, " ")) reads as “copy this range to standard output” without a loop body to audit, and the same call works unmodified whether the destination is a raw array, a std::vector, a std::list, or a stream — because the algorithm is templated purely on iterator category, not container type.

When to reach for it: any time you are transferring a full range into a destination you already know is (or will become, via back_inserter) big enough — flattening one container into another, populating a buffer from a generator range, or writing a range straight to an output stream for logging or serialization.

Pitfalls: the return value is easy to ignore but useful when you copy into the middle of a larger destination and need to know where the copied data ends (for example, chaining a second std::copy call right after the first, writing into the iterator that the first call returned). The more dangerous pitfall is destination sizing — see the “Common Mistakes” section below for the exact undefined-behavior case of a too-small destination.


std::copy_if

Copy only elements that satisfy a predicate:

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> evens;

    // Copy only even numbers
    std::copy_if(nums.begin(), nums.end(),
        std::back_inserter(evens),
        [](int n) { return n % 2 == 0; });
    // evens: {2, 4, 6, 8, 10}

    // Copy strings longer than 3 characters
    std::vector<std::string> words = {"hi", "hello", "hey", "greetings"};
    std::vector<std::string> long_words;

    std::copy_if(words.begin(), words.end(),
        std::back_inserter(long_words),
        [](const std::string& s) { return s.length() > 3; });
    // long_words: {"hello", "greetings"}
}

copy_if is the copy version of the filter operation. The destination must either be pre-sized (worst case: same size as source) or use back_inserter.

Why it exists and how it works: copy_if is functionally equivalent to writing your own loop with an if guard around the assignment — if (pred(*first)) *result++ = *first; for every element in [first, last) — but it removes the temptation to accidentally increment the output iterator on elements that were skipped, which is the classic hand-rolled-loop bug this algorithm eliminates. There is no copy_if_n or in-place variant: it always walks the full source range because it has to inspect every element’s predicate result to decide whether to copy it, so unlike std::copy there is no shortcut based on distance alone.

When to reach for it: filtering while copying — building a “matches” or “survivors” list without a separate remove_if/erase pass, extracting a subset for a report, or splitting a range into two destinations with complementary predicates (call it twice, once with the predicate and once with its negation).

Pitfalls: because you cannot know in advance how many elements will pass the predicate, pre-sizing the destination to nums.size() and then not trimming it afterward leaves default-constructed trailing elements if you assigned through indices instead of back_inserter — this is why back_inserter is almost always the right output iterator choice here rather than a pre-sized resize(). There is also no in-place overload: you cannot copy_if a container into itself the way remove_if conceptually does; if you need to compact a range in place, look at std::remove_if plus erase, not copy_if.


std::copy_n

Copy a fixed number of elements:

#include <algorithm>
#include <vector>
#include <list>

int main() {
    std::vector<int> src = {10, 20, 30, 40, 50};

    // Copy first 3 elements
    std::vector<int> dst(3);
    std::copy_n(src.begin(), 3, dst.begin());
    // dst: {10, 20, 30}

    // From a stream or generator (forward iterator)
    std::list<int> data = {1, 2, 3, 4, 5};
    std::vector<int> first3;
    std::copy_n(data.begin(), 3, std::back_inserter(first3));
    // first3: {1, 2, 3}
}

Warning: if n exceeds the number of readable elements, behavior is undefined. Validate before calling on streams or untrusted sizes.

Why it exists and how it works: copy_n is copy’s counterpart for when you know a count rather than an end iterator — it takes (first, n, result) instead of (first, last, result), which matters for input iterators (like a stream iterator reading from std::cin) where you may not have — or want to compute — a last at all. Internally it is effectively a loop that decrements a counter instead of comparing against last, so its complexity is exactly n assignments regardless of the source container’s actual size.

When to reach for it: reading a known-size prefix from an input stream, taking the first k elements of a std::list or other non-random-access container without paying for an advance() call to compute an end iterator, or implementing pagination/batching logic where the batch size is the natural loop bound.

Pitfalls: copy_n trusts you completely about n — there is no way for it to detect that the source only has 3 readable elements if you pass n = 10; it will simply keep dereferencing and incrementing past the end, which is undefined behavior (typically manifesting as a crash on a std::vector, or reading garbage/hanging on a stream iterator). Always validate n <= std::distance(first, last) when the count comes from external input rather than a literal you control.


std::copy_backward — Overlapping Ranges

This is the most confusing variant, but it solves a real problem: shifting elements right within the same buffer.

Why std::copy fails for rightward shifts:

src: [1][2][3][4][5]
dst starts at position 2 (shift right by 2)

copy goes left-to-right:
step 1: pos[2] = pos[0] → [1][2][1][4][5]  // dest overwrites src[2]
step 2: pos[3] = pos[1] → [1][2][1][2][5]  // dest overwrites src[3]
step 3: pos[4] = pos[2] → [1][2][1][2][1]  // reads OVERWRITTEN data — wrong!

copy_backward goes right-to-left, reading source before destination overwrites it:

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // Shift elements right by 2 positions (insert gap at beginning)
    // copy_backward(first, last, dest_last) — dest_last points one past destination end
    std::copy_backward(v.begin(), v.begin() + 3, v.begin() + 5);
    // Before: 1 2 3 4 5
    // After:  1 2 1 2 3  (first two positions now available for new data)

    // Example: insert at front by shifting
    std::vector<int> buf = {1, 2, 3, 4, 5};
    buf.push_back(0);  // make room
    std::copy_backward(buf.begin(), buf.begin() + 5, buf.end());
    buf[0] = 99;  // insert
    // buf: {99, 1, 2, 3, 4, 5}
}

Rule: when source and destination overlap and destination is to the right of source, use copy_backward. When destination is to the left (shifting left), copy is safe.

To be precise about the boundary condition (the “right/left” framing above is a good intuition, but the standard states it in terms of iterators, not just direction): std::copy(first, last, d_first) is undefined if d_first falls inside [first, last). std::copy_backward(first, last, d_last) is undefined if d_last falls inside (first, last]. The two functions have complementary blind spots by design — each is safe for exactly the overlap case the other one is not — which is why the pair together, not either one alone, covers every possible in-place shift:

flowchart TD
    A["Source range [first, last)<br/>and destination overlap?"] -->|No overlap| B["Either std::copy or<br/>std::copy_backward is safe"]
    A -->|Yes, overlap| C{"Where does the<br/>destination start?"}
    C -->|"d_first is BEFORE first<br/>(shifting left)"| D["std::copy is safe<br/>left-to-right reads ahead of writes"]
    C -->|"d_first falls INSIDE [first, last)<br/>(shifting right)"| E["std::copy is UB —<br/>use std::copy_backward instead"]
    E --> F["std::copy_backward reads<br/>right-to-left, writes stay<br/>behind the read position"]

Concretely: shrinking a gap by moving elements toward the front of a buffer (erase-style compaction) is a leftward shift and is safe with std::copy. Opening a gap by moving elements toward the back of a buffer (insert-style, making room at the front) is a rightward shift and needs std::copy_backward. This exact pair of operations — shift left to erase, shift right to insert — is what a std::vector’s own erase() and insert() do internally, so understanding this rule also explains why those member functions are O(n): they perform exactly this kind of in-place shift under the hood.


std::move (Algorithm)

std::move the algorithm (not the cast) transfers elements using move semantics, leaving sources in a valid-but-unspecified state:

#include <algorithm>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> src = {"hello", "world", "foo"};
    std::vector<std::string> dst;

    // Move strings — no copying of string data
    std::move(src.begin(), src.end(), std::back_inserter(dst));

    // src strings are now moved-from (valid but unspecified)
    // Safe to reassign or let them be destroyed
    for (auto& s : src) {
        s = "cleared";  // fine — reassigning moved-from
    }

    // dst has the original values
    // dst: {"hello", "world", "foo"}
}

Use move (algorithm) over copy when:

  • The source container will be discarded or reset after the operation
  • Elements are large (strings, vectors, unique_ptrs) and copying is expensive
  • Elements are move-only (unique_ptr, file handles)

Why it exists and how it works: std::move (the algorithm in <algorithm>) is essentially std::copy with one difference: it dereferences through std::move() (the cast in <utility>) before assigning, so *result++ = std::move(*first++) instead of a plain copy-assignment. For a std::vector<std::string>, this is the difference between deep-copying every character buffer and simply transferring ownership of each string’s internal pointer — a difference that can turn an O(total character count) operation into an O(element count) one for large strings. The two names colliding — std::move the cast and std::move the algorithm — is a frequent source of confusion for developers new to the standard library; they share a name because they share a purpose (opt into move semantics) but operate at different granularities: one on a single value, one on a whole range.

When to reach for it: draining a buffer you are about to discard or clear() anyway, transferring ownership of move-only types like std::unique_ptr between containers (where std::copy would not even compile, since unique_ptr’s copy constructor is deleted), or any bulk transfer of expensive-to-copy types where the source’s post-move state does not matter to your program.

Pitfalls: the biggest one is reading a moved-from element afterward and expecting its old value — the standard only guarantees “valid but unspecified state” for the standard library’s own types, meaning the object is safe to destroy or reassign but its value must be treated as garbage until reassigned. A second, subtler pitfall: std::move the algorithm requires the destination to actually accept move-assignment or move-construction (via the output iterator); using it on a destination whose value_type has no usable move constructor silently falls back to a copy, so it costs nothing to write but buys nothing either — always check that the type you are “moving” actually benefits from move semantics.


Destination Space: back_inserter and Other Output Iterators

The output iterator controls where elements go:

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <set>

std::vector<int> src = {1, 2, 3, 4, 5};

// Append to end of vector/string/deque
std::vector<int> dst1;
std::copy(src.begin(), src.end(), std::back_inserter(dst1));

// Prepend to front of list/deque (not vector — no push_front)
std::list<int> dst2;
std::copy(src.begin(), src.end(), std::front_inserter(dst2));
// dst2: {5, 4, 3, 2, 1} — reversed, because each element is prepended

// Insert at arbitrary position
std::vector<int> dst3 = {10, 20, 30};
std::copy(src.begin(), src.end(), std::inserter(dst3, dst3.begin() + 1));
// dst3: {10, 1, 2, 3, 4, 5, 20, 30}

// Write to output stream
std::copy(src.begin(), src.end(),
    std::ostream_iterator<int>(std::cout, ", "));
// prints: 1, 2, 3, 4, 5,

This is worth internalizing as a general technique, not just a copy-specific trick: every algorithm in the copy family is agnostic to which output iterator you hand it, so the same std::copy call behaves completely differently depending on whether the third argument is dst.begin() (overwrite in place, destination must already be big enough), back_inserter(dst) (append, growing as needed), front_inserter(dst) (prepend, which is why the result comes out reversed — each new element gets inserted before the previous one), or inserter(dst, pos) (insert at an arbitrary position, shifting the rest of the container). None of this logic lives in std::copy itself; it all lives in the iterator’s operator= and operator++, which is the STL’s “algorithms are dumb, iterators are smart” design taken to its logical conclusion.


When std::copy Becomes memmove

For trivially copyable types (int, float, plain structs), the standard library implementation may optimize std::copy to a memmove call — equivalent to manually calling memcpy but safe for overlapping ranges.

Never use memcpy on non-trivial types like std::string, std::vector, or any class with a non-trivial copy constructor — it skips constructors and causes double-free or memory leaks.

// Safe — compiler knows int is trivially copyable
std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b(5);
std::copy(a.begin(), a.end(), b.begin());  // may compile to memmove

// Never do this for non-trivial types
std::vector<std::string> strs = {"hello", "world"};
std::vector<std::string> dst(2);
// memcpy(&dst[0], &strs[0], 2 * sizeof(std::string));  // WRONG — UB
std::copy(strs.begin(), strs.end(), dst.begin());  // correct

Every major standard library (libstdc++, libc++, MSVC STL) special-cases std::copy, std::copy_backward, std::fill, and a handful of other algorithms for the specific combination of contiguous-iterator ranges plus a TriviallyCopyable value_type. When that combination holds, the generic element-by-element loop is replaced entirely with a single call into the platform’s memmove (not memcpy — deliberately the overlap-safe variant, since the algorithm cannot statically know whether the caller’s ranges overlap), which is typically implemented with SIMD-width or even wider vectorized moves and far outperforms a scalar loop for anything beyond a handful of elements. This optimization is exactly why the guidance “prefer std::copy over a hand-written loop” is not just about readability — for trivial types, a hand loop compiled with typical optimization settings usually gets auto-vectorized too, but std::copy gets the fast path unconditionally and portably across compilers and optimization levels, without you needing to check the generated assembly.

The moment a type stops being trivially copyable — it has a user-defined copy constructor, a virtual function table, or owns a resource through a raw pointer with custom cleanup — that fast path disappears and std::copy falls back to calling the type’s copy constructor or copy-assignment operator once per element, exactly like the naive loop would. This is also precisely why memcpy-ing a std::string or std::vector is never correct: those types are not trivially copyable (their copy constructor has to allocate a new buffer and deep-copy contents), so a raw byte copy duplicates the internal pointer without duplicating what it points to — both the original and the “copy” now believe they own the same heap allocation, and whichever one is destroyed first frees memory the other still references, producing a double-free or use-after-free later. std::copy sidesteps this entirely by always going through the proper constructor or assignment operator for non-trivial types, which is why the guidance in this guide is consistently “use std::copy, never memcpy, for anything except trivial types you have explicitly verified.”


C++20 Ranges: std::ranges::copy and Friends

C++20 added range-constrained counterparts to this entire family under <algorithm> in the std::ranges namespace — std::ranges::copy, std::ranges::copy_if, std::ranges::copy_n, std::ranges::copy_backward, and std::ranges::move. They solve the same problems as their classic counterparts, with three concrete improvements that matter in real code:

#include <algorithm>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;

    // Classic: pass first/last separately
    std::copy(src.begin(), src.end(), std::back_inserter(dst));

    // Ranges: pass the whole range, no begin()/end() pair to keep in sync
    std::ranges::copy(src, std::back_inserter(dst));

    // Composable with views — copy only even numbers, no copy_if needed
    std::vector<int> evens;
    std::ranges::copy(
        src | std::views::filter([](int n) { return n % 2 == 0; }),
        std::back_inserter(evens));
    // evens: {2, 4}

    // Structured return value instead of a bare iterator
    auto [in_end, out_end] = std::ranges::copy(src, dst.begin());
}

Fewer iterator-pairing mistakes: std::ranges::copy(src, dst) takes the source as a single range argument instead of a (first, last) pair, which removes an entire category of bug where a developer accidentally passes a.begin() together with b.end() from two different containers — a mistake the compiler cannot catch with classic iterators but that is structurally impossible once the range is a single argument.

Composability with views: because std::views::filter, std::views::transform, and friends are themselves ranges, you can pipe a transformation directly into std::ranges::copy instead of writing a separate copy_if call with a predicate — the filter view and copy_if are not identical (the view is lazy and reusable, the loop inside copy_if is eager and one-shot), but for a single filter-then-copy operation they produce the same result, and the pipeline reads closer to the intent.

Compile-time dangling protection: range algorithms use the borrowed_range concept to detect when the source range is a temporary whose iterators would outlive it — for example, std::ranges::copy(get_vector(), out) where get_vector() returns a temporary std::vector by value. Because std::vector does not model borrowed_range, passing a temporary like this returns std::ranges::dangling in the result’s in field, which cannot be dereferenced, converting what would silently be a dangling-iterator use-after-free in classic code into a compile error if you try to use the returned iterator. This protection does not extend to the overlap rules discussed earlier, though — std::ranges::copy has the exact same d_first-inside-[first, last) undefined behavior as classic std::copy, because that is a property of the copy operation itself, not of iterator lifetime.

In practice: reach for the std::ranges versions in new C++20-or-later code for the ergonomics and the dangling-iterator safety net, but do not expect them to change the overlap semantics covered in the copy_backward section — that rule is unchanged and just as easy to violate with the ranges version.


Undersized Destinations and Other Copy Bugs

1. Destination too small:

std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(3);  // only room for 3
std::copy(src.begin(), src.end(), dst.begin());  // UB: writes past end
// Fix: resize dst first, or use back_inserter

2. Using copy for rightward overlap:

std::vector<int> v = {1, 2, 3, 4, 5};
std::copy(v.begin(), v.begin() + 3, v.begin() + 2);  // UB: overlapping, shifts right
std::copy_backward(v.begin(), v.begin() + 3, v.begin() + 5);  // correct

3. Reading moved-from elements:

std::move(src.begin(), src.end(), back_inserter(dst));
std::cout << src[0];  // UB or garbage — moved-from string
src.clear();  // safe — destroy or reassign

4. Assuming any overlap is automatically handled by “the right function”:

std::vector<int> v = {1, 2, 3, 4, 5};
// Overlapping copy INTO a different container is fine — no shared storage, no UB
std::vector<int> other(5);
std::copy(v.begin(), v.end(), other.begin());  // always safe: different containers

// But self-copy in the SAME container must respect the direction rule above,
// and neither copy nor copy_backward magically detects the wrong direction —
// calling the wrong one compiles cleanly and just corrupts data at runtime.

The compiler does not check overlap direction for you — both std::copy and std::copy_backward compile identically whether you picked the right one or not, and the corrupted output from picking wrong can look plausible enough (partially correct values) that it slips past a casual glance at test output. When debugging a suspicious in-place shift, print the buffer before and after, or write a targeted unit test with known input, rather than trusting a visual read of the results.


Choosing Among the Copy Algorithms

  • std::copy — copy all elements; use back_inserter to avoid pre-sizing the destination; undefined behavior if the destination’s start iterator falls inside the source range
  • std::copy_if — filter while copying with a predicate (the “filter” of the copy family); always walks the entire source range to evaluate the predicate
  • std::copy_n — copy exactly n elements; validate n doesn’t exceed available elements, since there is no way for the algorithm to detect an out-of-range count itself
  • std::copy_backward — use when source and destination overlap with the destination to the right; safe exactly where std::copy is not, and vice versa
  • std::move (algorithm) — transfer ownership instead of copying; sources become valid-but-unspecified; only pays off for types with a real move constructor
  • Never use memcpy on non-trivial types — use std::copy and let the compiler optimize trivially copyable ranges to memmove automatically
  • std::ranges::copy and friends (C++20) trade the (first, last) pair for a single range argument, compose with views, and catch dangling temporaries at compile time — but keep the same overlap-direction rules as the classic algorithms

Frequently Asked Questions

How do I avoid writing past the end of the destination?

Use back_inserter(dest) as the output iterator — it calls push_back() for each element, growing the container automatically. Alternatively, resize() the destination before copying. Writing past the end of a pre-sized array or vector is undefined behavior, and it typically will not crash immediately, which is what makes it dangerous in production code.

When should I use copy_backward instead of copy?

Use copy_backward when source and destination overlap and the destination’s end iterator falls inside (first, last] — in practice, when you are shifting elements toward higher indices within the same buffer. std::copy goes left-to-right and would overwrite elements before they are read; std::copy_backward goes right-to-left and avoids exactly that failure mode. See the mermaid diagram above for the decision path.

What happens to elements after std::move (the algorithm)?

Source elements are left in a “valid but unspecified” state for standard library types — still destructible and assignable, but their values must not be read until reassigned. Treat a moved-from std::string or std::vector the same way you would treat uninitialized data: safe to overwrite, unsafe to inspect.

Is there already a Korean-language walkthrough of this same topic?

Yes — 복사 알고리즘 (Copy Algorithms) covers the same functions in Korean with a slightly different set of examples, aimed at readers of this site’s Korean C++ series rather than as a translation of this post.