C++ std::copy·copy_if·copy_backward: 목적지 크기, 겹치는 범위, back_inserter
이 글의 핵심
std::copy는 목적지 공간을 확보해 주지 않기 때문에 빈 벡터에 그대로 복사하면 정의되지 않은 동작이 됩니다. 겹치는 범위에서 copy_backward가 필요한 이유, std::copy와 memcpy의 관계, reserve로 재할당을 줄이는 방법과 C++17 병렬 복사까지 다뤄 상황에 맞는 복사 알고리즘을 고를 수 있게 합니다.
복사 알고리즘이란?
복사 알고리즘은 STL에서 제공하는 범위 기반 복사 유틸리티입니다. 원본 범위의 요소를 목적지로 복사하거나, 조건에 맞는 요소만 선택적으로 복사할 수 있습니다. 실무에서는 벡터 간 데이터 이동, 필터링된 결과 수집, 로그 데이터 복사 등에 자주 사용됩니다.
왜 필요한가?
문제: 수동으로 루프를 돌려 복사하면 코드가 길어지고 실수하기 쉽습니다.
// 수동 복사 (번거로움)
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]);
}
해결: std::copy 같은 알고리즘으로 한 줄로 처리합니다.
// STL 복사 (간결함)
std::copy(src.begin(), src.end(), std::back_inserter(dst));
복사 알고리즘 종류
범위 복사에는 여러 변형이 있습니다:
- copy: 전체 범위 복사
- copy_if: 조건 만족 요소만 복사
- copy_n: 처음 N개만 복사
- move: 이동 (복사 회피)
- remove_copy: 특정 값 제외하고 복사
#include <algorithm>
#include <vector>
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(5);
// 전체 범위 복사
std::copy(src.begin(), src.end(), dst.begin());
핵심: 목적지 범위는 충분한 크기를 가져야 합니다. 크기가 부족하면 버퍼 오버플로우가 발생합니다.
copy vs copy_if vs copy_n
세 알고리즘은 모두 “한 방향으로 요소를 옮긴다”는 점은 같지만, 입력 범위를 어떻게 정의하느냐가 다릅니다.
| 알고리즘 | 입력 범위 | 선택 기준 | 목적지 크기 |
|---|---|---|---|
copy | [first, last) 전체 | 없음 (전부 복사) | last - first 이상 또는 back_inserter |
copy_if | [first, last) | predicate가 참인 것만 | 최대 last - first (실제는 조건에 따라 적음) |
copy_n | first부터 n개 | 없음 (개수만) | n 이상 (또는 back_inserter로 n번 대입) |
- copy: 가장 기본입니다. “여기서 여기까지 그대로” 복제할 때 씁니다.
- copy_if: 필터링 복사입니다. 원본은 그대로 두며, 조건에 맞는 요소만 새 시퀀스로 모을 때
remove_if로 지우기 전에 “보기만” 하고 싶을 때 유용합니다. - copy_n: 끝 반복자가 없을 때 씁니다. 스트림 이터레이터, 생성기, “앞에서부터 정확히 n개만” 같은 패턴과 잘 맞습니다. 이때 유효한 입력이 n개 미만이면 UB이므로, 스트림이나 불확실한 소스면 먼저 길이를 확인하거나
istream_iterator와 조합을 신중히 설계해야 합니다.
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> a, b, c;
std::copy(src.begin(), src.end(), std::back_inserter(a));
std::copy_if(src.begin(), src.end(), std::back_inserter(b),
[](int x) { return x % 2 == 0; });
std::copy_n(src.begin(), 3, std::back_inserter(c));
// a: 1..5, b: 짝수만, c: 1,2,3
copy_backward: 겹치는 범위에서의 안전한 복사
std::copy는 일반적으로 앞에서 뒤로 대입합니다. 원본과 목적지가 같은 버퍼 안에서 겹치고, 블록이 오른쪽(높은 주소) 으로 이동해야 하면, 아직 읽지 않은 원본이 덮어써져 UB가 될 수 있습니다. 이때 쓰는 것이 copy_backward입니다.
- 시그니처:
copy_backward(first, last, d_last)—d_last는 목적지 구간의 끝 다음을 가리킵니다(역방향 복사의 “끝 기준점”). - 언제: 같은
vector안에서 부분 구간을 오른쪽으로 밀거나, 겹치는 슬라이드가 오른쪽일 때.
std::vector<int> v = {1, 2, 3, 4, 5};
// v[0..2)를 한 칸 오른쪽으로: {1,1,2,3,5} 형태가 되려면 끝에서부터 복사
std::copy_backward(v.begin(), v.begin() + 3, v.begin() + 4);
// v: 1,1,2,3,5
move_backward는 같은 이유로 이동 시 겹침을 처리합니다. 왼쪽으로 당기는 경우에는 보통 copy/move가 안전합니다.
반복자 무효화 주의
복사 알고리즘 자체는 “값을 대입한다”는 점에서 단순하지만, 목적지 컨테이너의 반복자는 상황에 따라 무효화됩니다.
vector에insert/push_back/emplace로 끝에 붙이기: 재할당이 일어나면 모든 반복자·참조가 무효일 수 있습니다.reserve로 용량을 맞춰 두면 재할당을 줄일 수 있습니다.- back_inserter: 내부적으로
push_back을 반복 호출하므로, 미리reserve하지 않은 큰 복사에서는 반복자를 잡아 둔 채로 루프에 쓰면 위험합니다. deque의 중간 삽입 등은 컨테이너별 무효화 규칙을 확인해야 합니다.
실무에서는 “복사 중에 다른 스레드가 같은 vector를 쓰는가?” 같은 동시성도 반복자 무효화와 겹치면 데이터 레이스나 이중 해제로 이어질 수 있으므로, 공유 컨테이너에는 뮤텍스·불변 스냅샷 등 별도 설계가 필요합니다.
실전 예제: 벡터 복사와 필터링
동일 타입 벡터 간 복제
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst;
dst.reserve(src.size());
std::copy(src.begin(), src.end(), std::back_inserter(dst));
크기를 이미 알면 dst.resize(src.size()); std::copy(..., dst.begin()); 한 번이 push_back 오버헤드 없이 깔끔합니다.
조건에 맞는 요소만 새 벡터로
std::vector<int> scores = {55, 72, 40, 90, 61};
std::vector<int> pass;
std::copy_if(scores.begin(), scores.end(), std::back_inserter(pass),
[](int s) { return s >= 60; });
// pass: 72, 90, 61
원본 scores는 유지되므로, 같은 데이터로 “불합격만 보기” 같은 다른 copy_if도 이어서 만들 수 있습니다.
성능: std::copy와 memcpy의 관계
- 튜비얼리 복사 가능(trivially copyable)한 타입(예:
int,double,char배열)에 대해, 구현은 종종memmove/memcpy에 가까운 최적화를 할 수 있습니다. 그래도 표준 보장은 “복사 대입” 횟수이지, 반드시memcpy라는 뜻은 아닙니다. std::string,std::vector, 사용자 정의 클래스처럼 비트 단위 복사가 안 되는 타입에는memcpy를 쓰면 UB입니다. 반드시std::copy나 컨테이너의 복사/이동 생성자를 사용하세요.- 속도만 보려면: 연속 메모리의 POD/트리비얼 타입 대량 복사는
std::memcpy/std::memmove가 측정상 유리한 경우가 많지만, 유지보수와 이식성을 위해 먼저std::copy로 충분한지 프로파일링한 뒤, 병목이 증명된 핫패스에서만 저수준 최적화를 고려하는 편이 안전합니다. - C++20
std::ranges::copy등 범위 버전은 의도는 같으며, 반복자 실수를 줄이는 데 도움이 됩니다.
copy: 기본 복사
std::copy는 범위 [first, last)의 모든 요소를 목적지로 복사합니다. 반환값은 복사된 마지막 요소의 다음 위치를 가리키는 반복자입니다.
#include <algorithm>
#include <vector>
#include <iostream>
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst;
// back_inserter 사용 (자동으로 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
}
동작 원리: back_inserter는 출력 반복자로, 대입 연산자가 호출될 때마다 push_back()을 실행합니다. 따라서 목적지 컨테이너의 크기를 미리 확보하지 않아도 자동으로 확장됩니다.
성능: 복사 생성자가 N번 호출됩니다. 큰 객체(예: std::string, 사용자 정의 클래스)는 복사 비용이 크므로, 이동이 가능하면 std::move를 고려하세요.
실전 예시
예시 1: copy_if - 조건부 복사
std::copy_if는 조건(predicate)을 만족하는 요소만 복사합니다. 필터링된 데이터를 수집할 때 유용합니다.
#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;
// 짝수만 복사
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
}
}
동작 원리: 각 요소에 대해 predicate(람다 함수)를 호출하며, true를 반환하는 요소만 목적지에 복사합니다. 원본은 변경되지 않습니다.
실무 활용:
- 로그에서 에러 레벨만 필터링
- 사용자 목록에서 활성 사용자만 추출
- 센서 데이터에서 임계값 초과 샘플만 수집
// 실무 예시: 에러 로그만 수집
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"; });
예시 2: copy_n - N개만 복사
std::copy_n은 처음 N개 요소만 복사합니다. 범위의 끝을 지정하지 않고 개수만 지정하므로, 스트림이나 무한 시퀀스에서 일부만 가져올 때 유용합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst;
// 처음 3개만 복사
std::copy_n(src.begin(), 3, std::back_inserter(dst));
for (int x : dst) {
std::cout << x << " "; // 1 2 3
}
}
동작 원리: 시작 반복자에서 N번 전진하며 각 요소를 복사합니다. N이 범위를 초과하면 정의되지 않은 동작(UB)이므로, N이 유효한지 확인해야 합니다.
실무 활용:
- 파일에서 처음 100줄만 읽기
- 네트워크 패킷에서 헤더 크기만큼만 복사
- 대용량 데이터에서 샘플링
// 실무 예시: 파일 헤더만 읽기
std::ifstream file("data.bin", std::ios::binary);
std::vector<char> header(512);
std::copy_n(std::istreambuf_iterator<char>(file), 512, header.begin());
예시 3: move - 이동으로 성능 향상
std::move 알고리즘은 요소를 복사 대신 이동합니다. 큰 객체(예: 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;
// 이동 (복사 아님)
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는 유효하지만 지정되지 않은 상태 (보통 빈 문자열)
for (const auto& s : src) {
std::cout << "'" << s << "' "; // '' '' ''
}
}
동작 원리: 각 요소에 대해 이동 생성자를 호출합니다. std::string의 경우, 내부 버퍼 포인터만 이동하고 원본은 빈 상태가 됩니다. 복사는 O(N × 문자열 길이)이지만, 이동은 O(N × 포인터 크기)로 훨씬 빠릅니다.
주의: 이동 후 원본 컨테이너의 요소는 유효하지만 지정되지 않은 상태(valid but unspecified state) 입니다. 재할당하거나 clear() 후 재사용하세요.
성능 비교:
// 복사: 문자열마다 새 버퍼를 힙에 할당하고 내용을 복사 (10,000번 할당)
std::copy(src.begin(), src.end(), std::back_inserter(dst));
// 이동: string 객체(보통 24~32바이트)만 옮기고 버퍼 소유권을 넘김 (할당 없음)
std::move(src.begin(), src.end(), std::back_inserter(dst));
차이의 대부분은 바이트 수가 아니라 힙 할당 횟수에서 나옵니다. 문자열이 길수록, 개수가 많을수록 복사 쪽의 할당·memcpy 비용이 커져 격차가 벌어집니다. 반대로 SSO(Small String Optimization) 범위에 들어가는 짧은 문자열은 원래 힙 할당이 없어서 복사와 이동의 차이가 거의 없습니다.
실무 활용:
- 임시 벡터를 다른 컨테이너로 이동
- 파싱 결과를 캐시로 이동
- 큐에서 작업 항목을 워커로 이동
예시 4: remove_copy - 특정 값 제외하고 복사
std::remove_copy는 특정 값을 제외하고 나머지만 복사합니다. 원본을 수정하지 않고 필터링된 결과를 얻을 때 유용합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 2, 4, 2, 5};
std::vector<int> dst;
// 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는 변경되지 않음
std::cout << "원본: ";
for (int x : src) {
std::cout << x << " "; // 1 2 3 2 4 2 5
}
}
동작 원리: 각 요소를 비교하여 지정된 값과 다른 요소만 목적지에 복사합니다. std::remove와 달리 원본을 수정하지 않습니다.
실무 활용:
- 설정 파일에서 주석 라인 제외
- 데이터에서 NULL 값 제거
- 로그에서 특정 패턴 제외
// 실무 예시: 빈 문자열 제외하고 복사
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 변형: 조건으로 제거할 요소를 지정할 수도 있습니다.
// 음수 제외하고 복사
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}
복사 알고리즘 전체 목록
STL은 다양한 복사 알고리즘을 제공합니다. 각각의 특징과 사용 시기를 이해하면 적절한 도구를 선택할 수 있습니다.
// 기본 복사
std::copy(begin, end, out) // 전체 범위 복사
std::copy_if(begin, end, out, pred) // 조건 만족 요소만
std::copy_n(begin, n, out) // 처음 N개만
// 역방향 복사 (겹치는 범위에서 안전)
std::copy_backward(begin, end, out) // 끝에서부터 복사
// 이동 (복사 회피)
std::move(begin, end, out) // 이동 생성자 사용
std::move_backward(begin, end, out) // 역방향 이동
// 조건부 복사
std::remove_copy(begin, end, out, value) // 특정 값 제외
std::remove_copy_if(begin, end, out, pred) // 조건 만족 요소 제외
알고리즘 선택 가이드
| 상황 | 알고리즘 | 이유 |
|---|---|---|
| 전체 복사 | copy | 가장 기본적이고 명확함 |
| 필터링 | copy_if | 조건으로 선택 |
| 일부만 | copy_n | 개수 지정 |
| 큰 객체 | move | 복사 비용 절감 |
| 겹치는 범위 | copy_backward | 오른쪽으로 이동 시 안전 |
| 특정 값 제외 | remove_copy | 원본 유지하며 필터링 |
자주 발생하는 문제
문제 1: 목적지 크기 부족
증상: 목적지 컨테이너의 크기가 부족하면 버퍼 오버플로우가 발생합니다.
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(3); // 크기 3
// ❌ 크기 부족 - 정의되지 않은 동작!
// std::copy(src.begin(), src.end(), dst.begin()); // 버퍼 오버플로우
// ✅ 해결 1: back_inserter 사용 (자동 확장)
std::vector<int> dst2;
std::copy(src.begin(), src.end(), std::back_inserter(dst2));
// ✅ 해결 2: 미리 resize
dst.resize(src.size());
std::copy(src.begin(), src.end(), dst.begin());
// ✅ 해결 3: reserve + back_inserter (메모리 재할당 최소화)
std::vector<int> dst3;
dst3.reserve(src.size()); // 메모리 미리 확보
std::copy(src.begin(), src.end(), std::back_inserter(dst3));
왜 이런 일이?: std::copy는 목적지 반복자에 대입만 합니다. dst.begin()은 기존 요소를 가리키므로, 크기가 부족하면 범위를 벗어난 메모리에 쓰게 됩니다.
성능 고려: back_inserter는 편리하지만 push_back()을 반복 호출하므로, 크기를 알면 reserve()로 미리 메모리를 확보하는 것이 재할당을 줄여 더 빠릅니다.
문제 2: 겹치는 범위
증상: 원본과 목적지 범위가 겹치면 정의되지 않은 동작(UB) 이 발생할 수 있습니다.
std::vector<int> v = {1, 2, 3, 4, 5};
// ❌ 겹치는 범위 - 오른쪽으로 복사 시 데이터 손실
// std::copy(v.begin(), v.begin() + 3, v.begin() + 1); // UB!
// 의도: {1, 1, 2, 3, 5}
// 실제: {1, 1, 1, 1, 5} (복사 중 덮어써짐)
// ✅ 해결 1: copy_backward (오른쪽으로 이동 시)
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}
// ✅ 해결 2: 왼쪽으로는 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}
// ✅ 해결 3: memmove (POD 타입만, C 스타일)
// memmove(v.data() + 1, v.data(), 3 * sizeof(int));
왜 이런 일이?: std::copy는 왼쪽에서 오른쪽으로 순차 복사합니다. 오른쪽으로 이동할 때 아직 복사하지 않은 원본 데이터를 먼저 덮어쓰면, 이후 복사할 데이터가 손상됩니다.
copy_backward 동작: 끝에서부터 역순으로 복사하므로, 오른쪽으로 이동 시 원본 데이터가 덮어써지기 전에 복사됩니다.
실무 팁:
- 오른쪽 이동:
copy_backward사용 - 왼쪽 이동:
copy사용 가능 - 확실하지 않으면: 임시 컨테이너에 복사 후 다시 대입
문제 3: move 후 원본 상태
증상: std::move 알고리즘 후 원본 요소는 유효하지만 지정되지 않은 상태(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는 유효하지만 지정되지 않은 상태
// ❌ src[0] 사용 - 빈 문자열일 수도, 아닐 수도 있음 (UB는 아니지만 위험)
std::cout << src[0].size() << '\n'; // 0일 가능성 높지만 보장 안 됨
// ✅ 안전한 사용법
src.clear(); // 명시적으로 비우기
src = {"new", "data"}; // 재할당
왜 이런 상태가?: 이동 후 원본 객체는 소멸자를 안전하게 호출할 수 있는 상태여야 하지만, 값은 보장되지 않습니다. std::string의 경우 보통 빈 문자열이 되지만, 표준은 이를 보장하지 않습니다.
실무 권장사항:
- 이동 후 원본 사용 금지: 이동 후에는 원본을 읽지 마세요.
- 재사용 전 초기화:
clear(),reset(), 또는 재할당으로 명확한 상태로 만드세요. - 임시 객체 이동: 어차피 버릴 객체만 이동하세요.
// ✅ 좋은 패턴: 임시 벡터 이동
std::vector<Data> parse_file(const std::string& path) {
std::vector<Data> temp;
// ... 파싱 ...
return temp; // 자동 이동 (RVO/NRVO)
}
auto data = parse_file("input.txt"); // 이동되며, temp는 더 이상 접근 불가
문제 4: 복사 vs 이동 성능
증상: 큰 객체를 copy로 복사하면 성능이 크게 저하됩니다.
std::vector<std::string> src(10000, "very long string with lots of data...");
std::vector<std::string> dst;
dst.reserve(src.size());
// copy: 복사 생성자 10,000번 호출
auto start = std::chrono::steady_clock::now();
std::copy(src.begin(), src.end(), std::back_inserter(dst));
auto copy_time = std::chrono::steady_clock::now() - start;
dst.clear();
// move: 이동 생성자 10,000번 호출 (포인터만 이동)
start = std::chrono::steady_clock::now();
std::move(src.begin(), src.end(), std::back_inserter(dst));
auto move_time = std::chrono::steady_clock::now() - start;
// 긴 문자열이면 move_time이 copy_time보다 훨씬 작게 나옵니다 (배율은 문자열 길이·할당자에 따라 다름)
성능 비교:
| 타입 | copy 비용 | move 비용 | 차이가 나는 이유 |
|---|---|---|---|
int | 값 복사 | 값 복사 | 이동도 복사와 같음 |
std::string (SSO 범위의 짧은 문자열) | 내부 버퍼 복사 | 내부 버퍼 복사 | 힙 할당이 없어 차이 거의 없음 |
std::string (긴 문자열) | 힙 할당 + 내용 복사 | 포인터·크기만 이동 | 길이에 비례해 격차 증가 |
std::vector<int> | 힙 할당 + 원소 전체 복사 | 포인터 3개 이동 | 원소 수에 비례해 격차 증가 |
| 사용자 정의 클래스 | 구현에 따라 | 이동 생성자 구현에 따라 | noexcept 이동 생성자가 없으면 복사로 대체되기도 함 |
실무 가이드:
- 작은 타입 (
int,double,char):copy사용 (이동 이점 없음) - 문자열/컨테이너: 원본이 필요 없으면
move사용 - 사용자 정의 타입: 이동 생성자가 있으면
move고려 - const 객체: 이동 불가,
copy만 가능
// 큰 객체는 move 권장
std::vector<HugeObject> src = /* ... */;
std::vector<HugeObject> dst;
dst.reserve(src.size());
if (/* 원본이 더 이상 필요 없으면 */) {
std::move(src.begin(), src.end(), std::back_inserter(dst));
} else {
std::copy(src.begin(), src.end(), std::back_inserter(dst));
}
출력 반복자 (Output Iterator)
복사 알고리즘의 목적지는 출력 반복자입니다. STL은 다양한 출력 반복자를 제공하여 복사 동작을 유연하게 만듭니다.
#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));
// dst1: {1, 2, 3}
// 2. inserter: insert 호출 (지정된 위치에 삽입)
std::vector<int> dst2 = {10, 20};
std::copy(src.begin(), src.end(), std::inserter(dst2, dst2.begin()));
// dst2: {1, 2, 3, 10, 20}
// 3. front_inserter: push_front 호출 (list, deque만)
std::list<int> dst3;
std::copy(src.begin(), src.end(), std::front_inserter(dst3));
// dst3: {3, 2, 1} (역순)
// 4. ostream_iterator: 스트림에 출력
std::copy(src.begin(), src.end(),
std::ostream_iterator<int>(std::cout, " "));
// 출력: 1 2 3
출력 반복자 선택 가이드
| 반복자 | 호출 메서드 | 지원 컨테이너 | 사용 시기 |
|---|---|---|---|
back_inserter | push_back() | vector, deque, list, string | 끝에 추가 (가장 흔함) |
front_inserter | push_front() | list, deque | 앞에 추가 (역순) |
inserter | insert() | 모든 컨테이너 | 중간 위치에 삽입 |
ostream_iterator | operator<< | 스트림 | 파일/콘솔 출력 |
성능 고려:
back_inserter: vector는 재할당 가능,reserve()권장front_inserter: list는 O(1), vector는 지원 안 함inserter: 중간 삽입은 vector에서 O(N), list에서 O(1)
실무 예시:
// 파일로 복사
std::ofstream file("output.txt");
std::copy(data.begin(), data.end(),
std::ostream_iterator<int>(file, "\n"));
// set에 복사 (중복 자동 제거)
std::vector<int> v = {1, 2, 2, 3, 3, 3};
std::set<int> s;
std::copy(v.begin(), v.end(), std::inserter(s, s.end()));
// s: {1, 2, 3}
성능 최적화 팁
reserve로 재할당 최소화
std::vector<int> src(10000);
std::vector<int> dst;
// ❌ 느림: 여러 번 재할당
std::copy(src.begin(), src.end(), std::back_inserter(dst));
// ✅ 빠름: 한 번에 메모리 확보
dst.reserve(src.size());
std::copy(src.begin(), src.end(), std::back_inserter(dst));
병렬 복사 (C++17)
#include <execution>
// 순차 복사
std::copy(src.begin(), src.end(), dst.begin());
// 병렬 복사 (멀티코어 활용)
std::copy(std::execution::par, src.begin(), src.end(), dst.begin());
memcpy로 최적화 (POD 타입만)
// POD 타입은 memcpy가 더 빠를 수 있음
std::vector<int> src(1000000);
std::vector<int> dst(src.size());
// std::copy 대신
std::memcpy(dst.data(), src.data(), src.size() * sizeof(int));
주의: memcpy는 POD 타입(Plain Old Data)에만 안전합니다. std::string, std::vector 같은 타입은 절대 사용하지 마세요.
FAQ
Q1: copy는 언제 사용하나요?
A: 전체 범위를 복사할 때 사용합니다. 가장 기본적이고 명확한 복사 방법입니다.
Q2: copy_if는 언제 필요한가요?
A: 조건을 만족하는 요소만 선택적으로 복사할 때 사용합니다. 필터링이 필요한 모든 상황에 적합합니다.
Q3: move는 언제 사용해야 하나요?
A: 큰 객체(문자열, 컨테이너)를 복사하고 원본이 더 이상 필요 없을 때 사용합니다. 복사 비용을 크게 줄일 수 있습니다.
Q4: remove_copy와 remove의 차이는?
A:
- remove_copy: 원본 유지, 결과를 새 컨테이너에 복사
- remove: 원본 수정, erase-remove idiom과 함께 사용
Q5: 겹치는 범위는 어떻게 처리하나요?
A: 오른쪽으로 이동 시 copy_backward, 왼쪽으로는 copy 사용. 확실하지 않으면 임시 컨테이너를 거치세요.
Q6: 복사 알고리즘 학습 리소스는?
A:
- “Effective STL” by Scott Meyers
- “C++ Primer” by Lippman
- cppreference.com - std::copy
관련 글: STL 알고리즘 가이드, 반복자 가이드, 범위 기반 for.
STL 복사 알고리즘으로 범위를 안전하고 효율적으로 복사하며, 큰 객체는 move로 성능을 높이세요.
같이 보면 좋은 글
- C++ replace·replace_if·replace_copy: 값 치환과 transform 중 무엇을 쓸까
- C++ reverse·rotate·reverse_copy: 범위 뒤집기와 회전 알고리즘 사용법
- C++ fill·generate·iota로 범위 채우기: 테스트 데이터 생성 예제
- C++ Algorithm Count
- C++ STL 알고리즘 기초: sort·find·transform·accumulate 실전 활용
- C++ Algorithm Heap
- C++ Algorithm MinMax
실전 팁 (C++)
- 컴파일러 경고를 최대로 켜고(
-Wall -Wextra등 팀 합의), Sanitizer(ASan/UBSan)로 미정의 동작을 조기에 잡습니다. - 최적화는 프로파일 결과를 본 뒤에 합니다.
- STL
<algorithm>사용 시 반복자 무효화·비교자 일관성을 함께 검토합니다.