C++ Algorithm Copy | "Copy Algorithm" Guide

C++ Algorithm Copy | "Copy Algorithm" Guide

이 글의 핵심

std::copy, copy_if, copy_n, copy_backward—range copies, filters, and iterator pitfalls overlapping source and destination in C++.

What is the Copy Algorithm?

The copy algorithm is a range-based copy utility provided by STL. It allows you to copy elements from a source range to a destination or selectively copy elements that meet specific conditions. In practice, it is often used for data transfer between vectors, collecting filtered results, and copying log data.

Why is it needed?

Problem: Manually copying with loops makes the code verbose and prone to errors.

// Manual copy (tedious)
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst;
for (size_t i = 0; i < src.size(); ++i) {
    dst.push_back(src[i]);
}

Solution: Use algorithms like std::copy to handle it in one line.

// STL copy (concise)
std::copy(src.begin(), src.end(), std::back_inserter(dst));

Types of Copy Algorithms

There are several variations of range-based copy algorithms:

  • copy: Copies the entire range
  • copy_if: Copies only elements that satisfy a condition
  • copy_n: Copies the first N elements
  • move: Moves elements (avoiding copying)
  • remove_copy: Copies elements excluding a specific value
#include <algorithm>
#include <vector>

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

// Copy entire range
std::copy(src.begin(), src.end(), dst.begin());

Key Point: The destination range must have sufficient size. If the size is insufficient, buffer overflow will occur.


copy: Basic Copy

std::copy copies all elements in the range [first, last) to the destination. The return value is an iterator pointing to the position after the last copied element.

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

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

// Use back_inserter (automatically calls push_back)
std::copy(src.begin(), src.end(), std::back_inserter(dst));

// dst: {1, 2, 3, 4, 5}
for (int x : dst) {
    std::cout << x << " ";  // 1 2 3 4 5
}

How it works: back_inserter is an output iterator that calls push_back() whenever an assignment operation is performed. This means you don’t need to preallocate the size of the destination container; it will automatically expand.

Performance: The copy constructor is called N times. For large objects (e.g., std::string, user-defined classes), copying can be expensive, so consider using std::move if moving is possible.

Practical Examples

Example 1: copy_if - Conditional Copy

std::copy_if copies only elements that satisfy a condition (predicate). It is useful for collecting filtered data.

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

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> dst;
    
    // Copy only even numbers
    std::copy_if(src.begin(), src.end(), std::back_inserter(dst),
        [](int x) { return x % 2 == 0; });
    
    for (int x : dst) {
        std::cout << x << " ";  // 2 4 6 8 10
    }
}

How it works: The predicate (lambda function) is called for each element, and only elements returning true are copied to the destination. The original source remains unchanged.

Real-world applications:

  • Filtering error-level logs
  • Extracting active users from a user list
  • Collecting sensor data that exceeds a threshold
// Real-world example: Collect error logs only
struct LogEntry {
    std::string level;
    std::string message;
};

std::vector<LogEntry> logs = { /* ... */ };
std::vector<LogEntry> errors;

std::copy_if(logs.begin(), logs.end(), std::back_inserter(errors),
    [](const LogEntry& e) { return e.level == "ERROR"; });

Example 2: copy_n - Copy First N Elements

std::copy_n copies only the first N elements. Instead of specifying the range’s end, you specify the number of elements to copy, making it useful for streams or infinite sequences.

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

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;
    
    // Copy first 3 elements
    std::copy_n(src.begin(), 3, std::back_inserter(dst));
    
    for (int x : dst) {
        std::cout << x << " ";  // 1 2 3
    }
}

How it works: The algorithm advances the starting iterator N times, copying each element. If N exceeds the range, undefined behavior (UB) occurs, so ensure N is valid.

Real-world applications:

  • Reading the first 100 lines of a file
  • Copying header size from network packets
  • Sampling from large datasets
// Real-world example: Read file header only
std::ifstream file("data.bin", std::ios::binary);
std::vector<char> header(512);
std::copy_n(std::istreambuf_iterator<char>(file), 512, header.begin());

Example 3: move - Improve Performance with Move

The std::move algorithm moves elements instead of copying. This significantly reduces the cost of handling large objects (e.g., std::string, std::vector).

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

int main() {
    std::vector<std::string> src = {"hello", "world", "C++"};
    std::vector<std::string> dst;
    
    // Move (no copying)
    std::move(src.begin(), src.end(), std::back_inserter(dst));
    
    for (const auto& s : dst) {
        std::cout << s << " ";  // hello world C++
    }
    std::cout << "\n";
    
    // src is valid but in an unspecified state (usually empty strings)
    for (const auto& s : src) {
        std::cout << "'" << s << "' ";  // '' '' ''
    }
}

How it works: The move constructor is called for each element. For std::string, the internal buffer pointer is moved, leaving the original in an empty state. Copying is O(N × string length), but moving is O(N × pointer size), making it much faster.

Caution: After moving, the original container’s elements are valid but in an unspecified state. Reassign or clear() before reusing.

Performance Comparison:

// Copy: 10,000 strings × average 100 bytes = ~1MB copied
std::copy(src.begin(), src.end(), std::back_inserter(dst));

// Move: 10,000 pointers × 8 bytes = 80KB moved (about 12x faster)
std::move(src.begin(), src.end(), std::back_inserter(dst));

Real-world applications:

  • Moving temporary vectors to another container
  • Transferring parsing results to a cache
  • Moving tasks from a queue to a worker

Example 4: remove_copy - Copy Excluding Specific Values

std::remove_copy copies all elements except a specific value. It is useful for filtering results without modifying the original source.

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

int main() {
    std::vector<int> src = {1, 2, 3, 2, 4, 2, 5};
    std::vector<int> dst;
    
    // Copy excluding 2
    std::remove_copy(src.begin(), src.end(), std::back_inserter(dst), 2);
    
    for (int x : dst) {
        std::cout << x << " ";  // 1 3 4 5
    }
    std::cout << "\n";
    
    // src remains unchanged
    std::cout << "Original: ";
    for (int x : src) {
        std::cout << x << " ";  // 1 2 3 2 4 2 5
    }
}

How it works: Each element is compared to the specified value, and elements that differ are copied to the destination. Unlike std::remove, this does not modify the original source.

Real-world applications:

  • Excluding comment lines from configuration files
  • Removing NULL values from data
  • Filtering logs to exclude specific patterns
// Real-world example: Copy non-empty strings
std::vector<std::string> lines = {"hello", "", "world", "", "C++"};
std::vector<std::string> non_empty;

std::remove_copy(lines.begin(), lines.end(), 
                 std::back_inserter(non_empty), "");
// non_empty: {"hello", "world", "C++"}

remove_copy_if variation: You can also specify a condition to exclude elements.

// Copy excluding negative numbers
std::vector<int> numbers = {-1, 2, -3, 4, -5};
std::vector<int> positives;

std::remove_copy_if(numbers.begin(), numbers.end(), 
                    std::back_inserter(positives),
                    [](int x) { return x < 0; });
// positives: {2, 4}

Full List of Copy Algorithms

STL provides a variety of copy algorithms. Understanding their characteristics and when to use them will help you choose the right tool.

// Basic copy
std::copy(begin, end, out)           // Copy entire range
std::copy_if(begin, end, out, pred)  // Copy elements satisfying condition
std::copy_n(begin, n, out)           // Copy first N elements

// Backward copy (safe for overlapping ranges)
std::copy_backward(begin, end, out)  // Copy from end to start

// Move (avoid copying)
std::move(begin, end, out)           // Use move constructor
std::move_backward(begin, end, out)  // Move from end to start

// Conditional copy
std::remove_copy(begin, end, out, value)     // Copy excluding specific value
std::remove_copy_if(begin, end, out, pred)   // Copy excluding elements satisfying condition

Algorithm Selection Guide

SituationAlgorithmReason
Copy entire rangecopyMost basic and straightforward
Filteringcopy_ifSelect elements based on condition
Copy a subsetcopy_nSpecify number of elements
Large objectsmoveReduce copy overhead
Overlapping rangescopy_backwardSafe for rightward movement
Exclude specific valuesremove_copyFilter while preserving the original

Common Pitfalls

Pitfall 1: Destination too small

Symptom: If the destination container is not large enough, you get undefined behavior (writes past the end).

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

// ❌ Too small — undefined behavior!
// std::copy(src.begin(), src.end(), dst.begin());  // buffer overrun

// ✅ Fix 1: back_inserter (grows automatically)
std::vector<int> dst2;
std::copy(src.begin(), src.end(), std::back_inserter(dst2));

// ✅ Fix 2: resize first
dst.resize(src.size());
std::copy(src.begin(), src.end(), dst.begin());

// ✅ Fix 3: reserve + back_inserter (fewer reallocations)
std::vector<int> dst3;
dst3.reserve(src.size());
std::copy(src.begin(), src.end(), std::back_inserter(dst3));

Why: std::copy only assigns through the destination iterator. dst.begin() points at existing slots; if there are not enough, you write out of bounds.

Performance: back_inserter is convenient but calls push_back repeatedly. When you know the final size, reserve() first avoids extra reallocations.

Pitfall 2: Overlapping ranges

Symptom: If source and destination overlap, undefined behavior is possible when the copy direction clobbers data you still need to read.

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

// ❌ Overlap shifting right — data loss
// std::copy(v.begin(), v.begin() + 3, v.begin() + 1);  // UB!

// ✅ Fix 1: copy_backward when shifting right
std::vector<int> v2 = {1, 2, 3, 4, 5};
std::copy_backward(v2.begin(), v2.begin() + 3, v2.begin() + 4);
// v2: {1, 1, 2, 3, 5}

// ✅ Fix 2: Shifting left is often safe with copy
std::vector<int> v3 = {1, 2, 3, 4, 5};
std::copy(v3.begin() + 2, v3.end(), v3.begin());
// v3: {3, 4, 5, 4, 5}

// ✅ Fix 3: If unsure, copy through a temporary container

Why: std::copy proceeds from left to right. Shifting right over an overlapping range can overwrite unread source elements.

Rule of thumb: Rightward overlap → copy_backward; leftwardcopy is usually fine; if in doubt, use a temp buffer.

Pitfall 3: Source state after std::move

Symptom: After the std::move algorithm, source elements are valid but unspecified.

std::vector<std::string> src = {"hello", "world"};
std::vector<std::string> dst;

std::move(src.begin(), src.end(), std::back_inserter(dst));

// src elements are valid for destruction but not for reading meaningfully
// ✅ Clear or reassign before reuse
src.clear();
src = {"new", "data"};

Why: Moved-from objects must be destructible, but their values are not specified. Do not read from them; clear() or assign fresh values.

Pitfall 4: copy vs move on large types

Symptom: Using copy on many large strings or vectors is slow.

// copy: N copy constructions
std::copy(src.begin(), src.end(), std::back_inserter(dst));

// move: N move constructions (often pointer swaps)
std::move(src.begin(), src.end(), std::back_inserter(dst));
TypePrefer
int, double, trivial typescopy (move offers little)
std::string, std::vector, heavy UDTsmove if source is disposable
const elementscopy only

Output iterators

Copy algorithms write through an output iterator. Helpers from <iterator> adapt containers and streams.

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

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

// 1. back_inserter — push_back
std::vector<int> dst1;
std::copy(src.begin(), src.end(), std::back_inserter(dst1));

// 2. inserter — insert at position
std::vector<int> dst2 = {10, 20};
std::copy(src.begin(), src.end(), std::inserter(dst2, dst2.begin()));

// 3. front_inserter — push_front (list, deque)
std::list<int> dst3;
std::copy(src.begin(), src.end(), std::front_inserter(dst3));
// order reversed

// 4. ostream_iterator — write to stream
std::copy(src.begin(), src.end(),
          std::ostream_iterator<int>(std::cout, " "));
AdapterEffectTypical containers
back_inserterpush_backvector, deque, list, string
front_inserterpush_frontlist, deque
inserterinsertany
ostream_iteratoroperator<<streams

Performance: For vector + back_inserter, call reserve(n) when you know n upfront.

Performance tips

1. reserve to avoid reallocations

std::vector<int> dst;
dst.reserve(src.size());
std::copy(src.begin(), src.end(), std::back_inserter(dst));

2. Parallel copy (C++17)

#include <execution>
std::copy(std::execution::par, src.begin(), src.end(), dst.begin());

Requires destination sized for direct assignment (not back_inserter).

3. memcpy for trivially copyable POD (advanced)

For contiguous trivial types, memcpy between raw buffers can beat generic copy. Never use this for non-trivial types (std::string, std::vector, etc.).

std::vector<int> src(1'000'000), dst(src.size());
std::memcpy(dst.data(), src.data(), src.size() * sizeof(int));

Frequently Asked Questions (FAQ)

Q1: When should I use copy?

A: Use it to copy an entire range. It is the most basic and straightforward copy method.

Q2: When is copy_if needed?

A: Use it to selectively copy elements that satisfy a condition. It is suitable for any situation requiring filtering.

Q3: When should I use move?

A: Use it when handling large objects (strings, containers) and the source is no longer needed. It significantly reduces the cost of copying.

Q4: What is the difference between remove_copy and remove?

A:

  • remove_copy: Preserves the original source and copies the filtered results to a new container.
  • remove: Modifies the original source and is typically used with the erase-remove idiom.

Q5: How do I handle overlapping ranges?

A: Use copy_backward for rightward movement and copy for leftward movement. If unsure, use a temporary container.

Q6: Where can I learn more about copy algorithms?

A:

Related Posts: STL Algorithm Guide, Iterator Guide, Range-based for Loop.

Summary: Use STL copy algorithms to safely and efficiently copy ranges, and leverage move for performance gains with large objects.


Here are other posts related to this topic:

  • C++ STL algorithms — English series hub
  • C++ Algorithm Replace | “Replace Algorithm” Guide
  • C++ Algorithm Reverse | “Reverse Algorithm” Guide
  • C++ Algorithm Generate | “Generate Algorithm” Guide

Practical Tips

Debugging Tips

  • Check compiler warnings first when issues arise.
  • Reproduce the problem with a simple test case.

Performance Tips

  • Avoid premature optimization without profiling.
  • Set measurable benchmarks before optimizing.

Code Review Tips

  • Pre-check common issues before submitting for review.
  • Follow your team’s coding conventions.

Keywords Covered in This Post (Related Search Terms)

C++, algorithm, copy, move, STL, and more. Search using these terms to find helpful resources related to this post.