C++ fill·generate·iota로 범위 채우기: 테스트 데이터 생성 예제
이 글의 핵심
생성 알고리즘은 단순해 보이지만 크기를 지정하지 않은 벡터에 generate를 호출하거나 람다 캡처 방식을 잘못 고르면 기대와 다른 결과가 나옵니다. rand 대신 <random> 엔진을 쓰는 이유, fill·generate·iota의 선택 기준, 성능상 주의할 점을 짚어 반복적인 초기화 코드를 줄일 수 있게 합니다.
들어가며
STL 생성 알고리즘은 컨테이너를 값으로 채우거나 함수로 생성하는 기능을 제공합니다. fill, generate, iota 등을 활용하면 초기화 코드를 간결하고 효율적으로 작성할 수 있습니다.
왜 반복문 대신 알고리즘을 쓰는가
for (size_t i = 0; i < v.size(); ++i) v[i] = 42; 같은 반복문도 같은 일을 합니다. 그럼에도 std::fill을 쓰는 이유는 의도가 이름에 드러나기 때문입니다. 반복문은 본문을 읽어야 “채우는 코드”인지 알 수 있지만, fill은 한 단어로 설명이 끝납니다. 인덱스 경계를 잘못 쓰는 실수(<=)가 끼어들 여지도 없고, vector든 deque든 배열이든 반복자만 있으면 같은 코드가 동작합니다. 성능은 대부분 같거나 더 낫습니다. 구현이 char 같은 바이트 타입이나 0으로 채우는 경우를 memset으로 최적화하는 경우가 많습니다.
이 글에서 다루는 알고리즘은 모두 이미 존재하는 범위에 값을 쓰는 것이지 원소를 추가하는 것이 아니라는 점을 먼저 기억해 두면, 뒤에 나오는 “빈 벡터에 아무것도 안 들어가는” 문제를 자연스럽게 피할 수 있습니다.
std::fill과 fill_n으로 고정 값 채우기
범위 전체를 한 값으로
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10);
// 모두 42로 채우기
std::fill(v.begin(), v.end(), 42);
for (int x : v) {
std::cout << x << " "; // 42 42 42 42 42 42 42 42 42 42
}
std::cout << std::endl;
return 0;
}
앞에서 N개만 채우는 fill_n
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10, 0); // 모두 0으로 초기화
// 처음 5개만 1로
std::fill_n(v.begin(), 5, 1);
for (int x : v) {
std::cout << x << " "; // 1 1 1 1 1 0 0 0 0 0
}
std::cout << std::endl;
return 0;
}
함수 시그니처:
template<class ForwardIt, class T>
void fill(ForwardIt first, ForwardIt last, const T& value);
template<class OutputIt, class Size, class T>
OutputIt fill_n(OutputIt first, Size count, const T& value);
시그니처에서 두 가지를 읽어 낼 수 있습니다. 첫째, fill은 ForwardIt를 요구하지만 fill_n은 OutputIt만 있으면 됩니다. 그래서 fill_n은 std::back_inserter나 std::ostream_iterator처럼 “쓰기만 가능한” 반복자에도 쓸 수 있습니다. std::fill_n(std::back_inserter(v), 5, 1);은 빈 벡터에 1을 다섯 개 추가합니다. 둘째, fill_n은 마지막으로 쓴 다음 위치를 반환하므로, 앞부분을 채운 뒤 이어서 다른 값을 채우는 코드를 이어 쓸 수 있습니다.
value가 const T&로 전달되고 모든 원소에 복사된다는 점도 기억할 만합니다. std::vector<std::shared_ptr<Foo>> v(10); std::fill(v.begin(), v.end(), std::make_shared<Foo>());는 서로 다른 Foo 10개가 아니라 같은 Foo 하나를 가리키는 포인터 10개를 만듭니다. 원소마다 새 객체가 필요하다면 다음 절의 generate를 써야 합니다. 그리고 벡터를 만들면서 채우는 경우라면 std::vector<int> v(10, 42);처럼 생성자로 한 번에 하는 것이 fill보다 간단하고, 0으로 한 번 초기화한 뒤 다시 쓰는 비용도 없습니다.
std::generate와 generate_n으로 함수 결과 채우기
람다 생성기로 채우기
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10);
int counter = 0;
// 함수로 생성
std::generate(v.begin(), v.end(), [&counter]() {
return counter++;
});
for (int x : v) {
std::cout << x << " "; // 0 1 2 3 4 5 6 7 8 9
}
std::cout << std::endl;
return 0;
}
generate는 인자 없는 함수(생성기)를 원소마다 한 번씩 호출하고 그 반환값을 대입합니다. fill이 값 하나를 복사하는 것과 달리, 호출할 때마다 다른 값을 만들 수 있으므로 카운터, 난수, 새 객체 생성에 적합합니다.
표준은 generate가 원소를 앞에서부터 순서대로 처리한다고 규정하므로, 위 코드는 항상 0 1 2 ... 9를 출력합니다. 이 보장은 순차 버전에만 해당합니다. C++17의 병렬 버전 std::generate(std::execution::par, ...)에 이런 상태 있는 람다를 넘기면 여러 스레드가 counter를 동시에 증가시켜 데이터 경쟁(미정의 동작)이 됩니다. 순서가 뒤섞이는 정도가 아니라 같은 값이 여러 번 나오거나 값이 건너뛰어질 수 있습니다. 병렬로 순차 번호가 필요하다면 iota를 쓰거나 인덱스로 값을 계산하는 transform이 맞습니다.
N개만 생성하는 generate_n
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v;
// back_inserter로 자동 확장
std::generate_n(std::back_inserter(v), 5, []() {
return rand() % 100;
});
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
함수 시그니처:
template<class ForwardIt, class Generator>
void generate(ForwardIt first, ForwardIt last, Generator g);
template<class OutputIt, class Size, class Generator>
OutputIt generate_n(OutputIt first, Size count, Generator g);
generate_n과 back_inserter의 조합은 “개수만 알고 컨테이너는 비어 있는” 상황에서 가장 자주 쓰입니다. back_inserter는 대입될 때마다 push_back을 호출하는 반복자라서, 벡터 크기를 미리 정하지 않아도 원소가 추가됩니다. 원소가 많다면 v.reserve(n)을 먼저 호출해 재할당을 줄이는 것이 좋습니다.
위 예제의 rand()는 설명을 위해 남겨 둔 것이고, 실제로는 뒤의 “rand() 대신 <random>을 써야 합니다. rand()는 <cstdlib>에 선언되어 있으며 srand로 시드를 주지 않으면 매번 같은 수열이 나옵니다.
시그니처에서 눈여겨볼 부분은 생성기 g가 값으로 전달된다는 점입니다. 알고리즘 안에서 쓰이는 것은 복사본이므로, 함수 객체를 넘기면 원본 객체의 상태는 바뀌지 않습니다. 예를 들어 Counter c; std::generate(v.begin(), v.end(), c);를 두 번 호출하면 두 번 모두 0부터 시작합니다. 이어서 번호를 매기고 싶다면 std::ref(c)로 감싸 참조를 넘기거나, 람다에서 외부 변수를 참조로 캡처해야 합니다. 이 규칙은 뒤의 “캡처 방식” 문제와도 연결됩니다.
std::iota로 연속 값 만들기
0부터 1씩 증가하는 값
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v(10);
// 1부터 순차 생성
std::iota(v.begin(), v.end(), 1);
for (int x : v) {
std::cout << x << " "; // 1 2 3 4 5 6 7 8 9 10
}
std::cout << std::endl;
return 0;
}
iota는 첫 원소에 시작값을 넣고, 이후 원소마다 ++value를 한 결과를 넣습니다. 이름은 APL 언어의 ι(iota) 연산자에서 왔습니다. <algorithm>이 아니라 <numeric>에 있다는 점 때문에 error: 'iota' is not a member of 'std' 에러를 만나는 경우가 많은데, 헤더만 추가하면 해결됩니다.
가장 흔한 실무 용도는 인덱스 배열 만들기입니다. 예를 들어 원본을 건드리지 않고 정렬 순서를 알고 싶을 때 std::vector<size_t> idx(v.size()); std::iota(idx.begin(), idx.end(), 0);로 0부터 n-1까지 채운 뒤, std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) { return v[a] < v[b]; });로 인덱스를 정렬합니다. 셔플된 순서가 필요하면 iota 후 std::shuffle을 쓰면 중복 없는 무작위 순열이 됩니다. 증가폭이 1이 아닌 수열(0, 5, 10, …)은 iota로 만들 수 없으므로 generate에 상태를 두어야 합니다.
char·double 등 다른 타입에 쓰기
#include <numeric>
#include <vector>
#include <iostream>
int main() {
// double로 순차 생성
std::vector<double> v(5);
std::iota(v.begin(), v.end(), 1.5);
// 1.5, 2.5, 3.5, 4.5, 5.5
// char로 순차 생성
std::vector<char> chars(5);
std::iota(chars.begin(), chars.end(), 'A');
// 'A', 'B', 'C', 'D', 'E'
for (double d : v) {
std::cout << d << " ";
}
std::cout << std::endl;
for (char c : chars) {
std::cout << c << " ";
}
std::cout << std::endl;
return 0;
}
iota는 ++가 정의된 타입이면 무엇이든 동작합니다. double에서는 ++가 1.0을 더하므로 1.5, 2.5, …가 나옵니다. 다만 0.1씩 증가하는 부동소수점 수열처럼 1이 아닌 간격이 필요하면 iota로는 안 되고, generate로 누적하면 오차가 쌓이므로 start + i * step처럼 인덱스로부터 매번 계산하는 편이 정확합니다.
char의 경우 ‘A’부터 26개를 넘게 만들면 알파벳이 아닌 문자(’[’, ’\’ 등)로 넘어가고, char가 부호 있는 타입인 환경에서 127을 넘으면 오버플로가 됩니다. 반복자를 역참조한 타입과 시작값의 타입이 다를 때는 시작값의 타입(T)으로 증가시킨 뒤 원소 타입으로 변환해 대입한다는 점도 알아 두면, std::iota(v.begin(), v.end(), 0)을 size_t 벡터에 써도 문제가 없는 이유를 이해할 수 있습니다. C++20의 std::views::iota(1, 11)은 컨테이너 없이 지연 평가되는 수열을 만들어 주므로 범위 기반 for에서 바로 쓸 수 있습니다.
fill·generate·iota 비교
| 알고리즘 | 헤더 | 용도 | 시간 복잡도 |
|---|---|---|---|
fill | <algorithm> | 고정 값으로 채우기 | O(N) |
fill_n | <algorithm> | N개를 고정 값으로 | O(N) |
generate | <algorithm> | 함수로 생성 | O(N) |
generate_n | <algorithm> | N개를 함수로 생성 | O(N) |
iota | <numeric> | 순차 증가 값 생성 | O(N) |
함수 시그니처:
// fill
template<class ForwardIt, class T>
void fill(ForwardIt first, ForwardIt last, const T& value);
// generate
template<class ForwardIt, class Generator>
void generate(ForwardIt first, ForwardIt last, Generator g);
// iota
template<class ForwardIt, class T>
void iota(ForwardIt first, ForwardIt last, T value);
빈 컨테이너·값 캡처·rand()에서 생기는 문제
크기 0인 vector에 fill을 호출하면 아무 일도 없다
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v; // 크기 0
// ❌ 크기가 0이면 아무 일도 안 함
std::fill(v.begin(), v.end(), 42);
std::cout << v.size() << std::endl; // 0
// ✅ 크기 지정
v.resize(10);
std::fill(v.begin(), v.end(), 42);
std::cout << v.size() << std::endl; // 10
// ✅ 또는 생성자에서 크기 지정
std::vector<int> v2(10);
std::fill(v2.begin(), v2.end(), 42);
return 0;
}
해결책: 컨테이너 크기를 미리 지정하거나 back_inserter를 사용하세요.
이 문제의 위험한 변형은 reserve와 resize를 혼동하는 경우입니다. v.reserve(10); std::fill(v.begin(), v.end(), 42);는 용량만 늘리고 크기는 0이므로 여전히 아무것도 채우지 않습니다. 더 나쁜 것은 std::fill_n(v.begin(), 10, 42);처럼 개수를 직접 지정하는 경우입니다. reserve만 하고 fill_n을 호출하면 크기가 0인 벡터의 할당된 메모리에 값을 써 버립니다. 메모리는 할당되어 있으니 크래시가 나지 않고, v.size()는 여전히 0이라 값이 사라진 것처럼 보입니다. 미정의 동작이지만 증상이 조용해서 찾기 어렵습니다. 디버그 빌드의 반복자 검사(-D_GLIBCXX_DEBUG, MSVC 디버그 모드)를 켜면 이런 범위 밖 쓰기를 즉시 잡아 줍니다.
값 캡처한 카운터가 원본을 바꾸지 않는다
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
int counter = 0;
std::vector<int> v(10);
// ❌ 값 캡처 (복사본 수정)
std::generate(v.begin(), v.end(), [=]() mutable {
return counter++; // 복사본만 증가
});
std::cout << "counter: " << counter << std::endl; // 0 (변경 안 됨)
// ✅ 참조 캡처
counter = 0;
std::generate(v.begin(), v.end(), [&counter]() {
return counter++; // 원본 증가
});
std::cout << "counter: " << counter << std::endl; // 10
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
해결책: 상태를 유지하려면 참조 캡처([&])를 사용하세요.
첫 번째 버전도 v에는 0부터 9까지 정상적으로 채워진다는 점이 헷갈리는 부분입니다. mutable 람다 안의 복사본 counter가 증가하므로 생성되는 값 자체는 맞고, 바깥의 counter만 0으로 남습니다. 채운 값만 필요하다면 이것으로 충분하고, 오히려 외부 변수를 건드리지 않아 더 안전합니다. 문제가 되는 것은 “몇 개를 생성했는지”나 “다음 번호가 무엇인지”를 바깥에서 이어서 써야 할 때입니다. 즉 값 캡처 + mutable은 틀린 코드가 아니라 상태가 알고리즘 안에만 머문다는 다른 의미를 가진 코드입니다.
참조 캡처를 쓸 때는 반대로 수명을 조심해야 합니다. 생성기 람다를 즉시 generate에 넘기는 경우는 안전하지만, 람다를 변수에 저장해 두었다가 캡처한 지역 변수가 사라진 뒤에 호출하면 댕글링 참조가 됩니다. C++14 초기화 캡처 [i = 0]() mutable은 상태를 람다 안에 소유시키는 방법으로, 외부 변수 없이 카운터를 만들 때 가장 깔끔합니다.
rand() 대신 엔진 쓰기
#include <algorithm>
#include <vector>
#include <random>
#include <iostream>
int main() {
std::vector<int> v(10);
// ❌ rand() (C 스타일, 품질 낮음)
std::generate(v.begin(), v.end(), []() {
return rand() % 100;
});
// ✅ C++11 random (품질 높음)
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<> dist{0, 99};
std::generate(v.begin(), v.end(), [&]() {
return dist(gen);
});
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
해결책: C++11 <random> 라이브러리를 사용하세요.
rand()의 문제는 세 가지입니다. 첫째, RAND_MAX가 구현마다 다르고 MSVC에서는 32767밖에 되지 않아 큰 범위의 난수를 만들 수 없습니다. 둘째, % 100은 RAND_MAX + 1이 100의 배수가 아니면 작은 수가 더 자주 나오는 편향을 만듭니다. 셋째, 전역 상태를 쓰므로 스레드 안전성이 보장되지 않고, 라이브러리 코드가 몰래 rand()를 호출하면 재현성이 깨집니다.
제가 <random>으로 옮길 때 가장 흔하게 본 실수는 함수 안에서 엔진을 매번 새로 만드는 것입니다. int roll() { std::mt19937 gen{std::random_device{}()}; ... }처럼 쓰면 호출할 때마다 random_device를 읽고 5KB짜리 엔진 상태를 초기화하므로 느리고, random_device가 결정적 구현인 일부 플랫폼(과거 MinGW 버전)에서는 매번 같은 값이 나옵니다. 엔진은 한 번 만들어 멤버나 static thread_local로 재사용하고, 분포 객체만 필요한 곳에서 만드는 것이 일반적인 패턴입니다.
fill과 generate의 실제 성능 차이
#include <algorithm>
#include <vector>
#include <chrono>
#include <iostream>
int main() {
std::vector<int> v(1000000);
// fill: 최적화됨 (빠름)
auto start1 = std::chrono::high_resolution_clock::now();
std::fill(v.begin(), v.end(), 0);
auto end1 = std::chrono::high_resolution_clock::now();
// generate: 함수 호출 오버헤드 (느림)
auto start2 = std::chrono::high_resolution_clock::now();
std::generate(v.begin(), v.end(), []() { return 0; });
auto end2 = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1).count();
auto duration2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2).count();
std::cout << "fill: " << duration1 << " μs" << std::endl;
std::cout << "generate: " << duration2 << " μs" << std::endl;
return 0;
}
해결책: 고정 값은 fill을, 동적 생성은 generate를 사용하세요.
이 측정 코드는 주석처럼 “generate가 느리다”는 결론을 항상 보여 주지는 않습니다. -O2 이상으로 최적화하면 []() { return 0; } 람다는 인라인되어 사라지고, 두 루프 모두 같은 기계어(또는 memset)가 되는 경우가 많습니다. 반대로 최적화 없이(-O0) 측정하면 함수 호출 비용이 그대로 남아 generate가 훨씬 느리게 나오는데, 이것은 실제 배포 빌드와 무관한 숫자입니다. 또 첫 번째 루프가 페이지 폴트와 캐시 워밍을 떠안기 때문에 순서만 바꿔도 결과가 달라집니다.
실제 차이가 생기는 곳은 생성기가 실제로 일을 할 때입니다. 난수 엔진 호출이나 문자열 생성은 원소당 수십~수백 나노초가 들 수 있으므로, 같은 값이면 fill이나 생성자 초기화를 쓰는 것이 확실히 이득입니다. 성능을 비교하려면 최적화 빌드에서 여러 번 반복 측정하고, 결과를 사용해 컴파일러가 루프를 지우지 못하게 해야 합니다. Google Benchmark 같은 도구를 쓰면 이런 함정을 대부분 피할 수 있습니다.
난수·함수 객체·구조체·ID 생성 예제
mt19937로 난수 채우기
#include <algorithm>
#include <vector>
#include <random>
#include <iostream>
int main() {
std::vector<int> v(10);
// 난수 생성기 설정
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<> dist{1, 100};
// 난수로 채우기
std::generate(v.begin(), v.end(), [&]() {
return dist(gen);
});
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
<random>은 역할을 셋으로 나눕니다. random_device는 운영체제가 제공하는 (가능하면) 진짜 난수로 시드를 한 번 만들고, mt19937 엔진은 그 시드에서 빠르게 의사 난수 비트를 만들며, uniform_int_distribution은 그 비트를 원하는 범위 [1, 100]로 편향 없이 변환합니다. 분포의 범위가 양 끝을 모두 포함한다는 점이 rand() % 100(0~99)과 다르므로 옮길 때 주의해야 합니다.
람다가 [&]로 gen과 dist를 참조 캡처한 것이 중요합니다. 값으로 캡처([=]() mutable)하면 람다 안에 엔진의 복사본이 생기고, generate에 넘어가면서 람다가 또 복사되므로 원본 gen의 상태는 전혀 진행되지 않습니다. 그러면 같은 generate를 두 번 호출했을 때 같은 난수 수열이 반복되는 버그가 됩니다. 또 std::mt19937 객체는 약 5KB로 크고 생성 비용도 있으므로, 함수가 호출될 때마다 새로 만들기보다 한 번 만들어 재사용하는 것이 좋습니다. 테스트에서 재현 가능한 결과가 필요하다면 random_device 대신 std::mt19937 gen{42};처럼 고정 시드를 쓰면 됩니다. 이 주제는 C++ 난수 생성 가이드에서 더 자세히 다룹니다.
상태를 가진 함수 객체 생성기
#include <algorithm>
#include <vector>
#include <iostream>
class Counter {
int count;
int step;
public:
Counter(int start = 0, int step = 1)
: count(start), step(step) {}
int operator()() {
int result = count;
count += step;
return result;
}
};
int main() {
std::vector<int> v(10);
// 0, 2, 4, 6, ...
std::generate(v.begin(), v.end(), Counter{0, 2});
for (int x : v) {
std::cout << x << " "; // 0 2 4 6 8 10 12 14 16 18
}
std::cout << std::endl;
return 0;
}
함수 객체는 상태를 멤버 변수로 들고 다니는 생성기입니다. 람다로도 [c = 0, step = 2]() mutable { int r = c; c += step; return r; }처럼 같은 일을 할 수 있지만, 생성기 로직이 여러 곳에서 재사용되거나 생성자 매개변수로 동작을 바꿔야 한다면 클래스로 이름을 붙이는 편이 읽기 쉽습니다.
Counter{0, 2}는 임시 객체로 전달되고, generate 안에서 그 복사본의 count가 증가합니다. 알고리즘이 끝나면 복사본은 사라지므로 호출한 쪽에서는 최종 상태를 알 수 없습니다. std::for_each는 함수 객체를 반환해 주지만 generate는 void를 반환합니다. 마지막 카운터 값이 필요하다면 Counter c{0, 2}; std::generate(v.begin(), v.end(), std::ref(c));처럼 std::ref로 넘겨야 합니다. operator()가 const가 아닌 것도 의도적입니다. 상태를 바꾸는 호출 연산자이기 때문입니다.
구조체 배열 초기화
#include <algorithm>
#include <vector>
#include <iostream>
struct Point {
int x, y;
};
int main() {
std::vector<Point> points(5);
int id = 0;
std::generate(points.begin(), points.end(), [&id]() {
Point p{id, id * 10};
++id; // return 앞에서 증가시켜야 실행됨
return p;
});
for (const auto& p : points) {
std::cout << "(" << p.x << ", " << p.y << ") "; // (0, 0) (1, 10) (2, 20) (3, 30) (4, 40)
}
std::cout << std::endl;
return 0;
}
이 예제의 이전 버전은 return Point{id, id * 10}; 다음 줄에 id++;를 두었는데, return 뒤의 코드는 실행되지 않으므로 모든 점이 (0, 0)이 되는 버그가 있었습니다. 컴파일러는 이런 도달 불가능한 코드를 에러로 막지 않고, 경고도 기본 옵션에서는 나오지 않는 경우가 많습니다(Clang의 -Wunreachable-code로 확인 가능). 생성기 람다에서 “값을 만들고 상태를 갱신하는” 두 단계는 위처럼 지역 변수에 결과를 담고, 상태를 바꾸고, 반환하는 순서로 쓰는 것이 가장 안전합니다. return Point{id, id++ * 10};처럼 한 줄로 줄이는 것도 피해야 합니다. 중괄호 초기화는 왼쪽부터 평가되는 것이 보장되지만, 읽는 사람이 평가 순서를 따져야 하는 코드는 실수를 부릅니다.
순차 ID 생성기
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
class IDGenerator {
std::string prefix;
int counter;
public:
IDGenerator(const std::string& prefix)
: prefix(prefix), counter(1) {}
std::string operator()() {
return prefix + std::to_string(counter++);
}
};
int main() {
std::vector<std::string> ids(5);
std::generate(ids.begin(), ids.end(), IDGenerator{"USER_"});
for (const auto& id : ids) {
std::cout << id << std::endl;
}
// USER_1
// USER_2
// USER_3
// USER_4
// USER_5
return 0;
}
std::vector<std::string> ids(5);는 빈 문자열 다섯 개를 먼저 만들고, generate가 그 위에 새 문자열을 대입합니다. 문자열처럼 생성 비용이 있는 타입이라면 빈 객체를 만들었다 덮어쓰는 것이 낭비이므로, ids.reserve(5); std::generate_n(std::back_inserter(ids), 5, IDGenerator{"USER_"});처럼 처음부터 추가하는 방식이 더 효율적입니다. 기본 생성자가 없는 타입이라면 vector<T> v(n) 자체가 컴파일되지 않으므로 이 방식만 가능합니다.
이 생성기는 객체마다 카운터를 따로 가지므로, 프로그램 전체에서 고유한 ID가 필요한 상황에는 맞지 않습니다. 여러 곳에서 IDGenerator{"USER_"}를 만들면 각각 USER_1부터 다시 시작해 ID가 겹칩니다. 전역적으로 고유해야 한다면 카운터를 하나의 공유 객체(멀티스레드라면 std::atomic<int>)로 두거나 UUID를 써야 합니다.
단위 테스트용 데이터 생성기 만들기
#include <algorithm>
#include <numeric>
#include <vector>
#include <random>
#include <iostream>
#include <string>
class TestDataGenerator {
std::mt19937 gen;
public:
TestDataGenerator() : gen(std::random_device{}()) {}
// 난수 배열 생성
std::vector<int> randomInts(size_t count, int min, int max) {
std::vector<int> result(count);
std::uniform_int_distribution<> dist{min, max};
std::generate(result.begin(), result.end(), [&]() {
return dist(gen);
});
return result;
}
// 순차 배열 생성
std::vector<int> sequence(size_t count, int start = 0) {
std::vector<int> result(count);
std::iota(result.begin(), result.end(), start);
return result;
}
// 고정 값 배열 생성
std::vector<int> constant(size_t count, int value) {
std::vector<int> result(count);
std::fill(result.begin(), result.end(), value);
return result;
}
// 패턴 배열 생성
std::vector<int> pattern(size_t count, const std::vector<int>& pattern) {
std::vector<int> result(count);
size_t patternSize = pattern.size();
std::generate(result.begin(), result.end(), [&, i = 0]() mutable {
return pattern[i++ % patternSize];
});
return result;
}
// 사용자 정의 생성
template<typename Generator>
std::vector<int> custom(size_t count, Generator gen) {
std::vector<int> result(count);
std::generate(result.begin(), result.end(), gen);
return result;
}
};
int main() {
TestDataGenerator tdg;
// 난수 10개 (1~100)
auto random = tdg.randomInts(10, 1, 100);
std::cout << "난수: ";
for (int x : random) std::cout << x << " ";
std::cout << std::endl;
// 순차 10개 (0부터)
auto seq = tdg.sequence(10);
std::cout << "순차: ";
for (int x : seq) std::cout << x << " ";
std::cout << std::endl;
// 고정 값 10개
auto constant = tdg.constant(10, 42);
std::cout << "고정: ";
for (int x : constant) std::cout << x << " ";
std::cout << std::endl;
// 패턴 반복 (1, 2, 3, 1, 2, 3, ...)
auto patt = tdg.pattern(10, {1, 2, 3});
std::cout << "패턴: ";
for (int x : patt) std::cout << x << " ";
std::cout << std::endl;
// 사용자 정의 (제곱수)
auto custom = tdg.custom(5, [i = 0]() mutable {
int sq = i * i; // i * i++ 는 같은 식에서 i를 읽고 수정하므로 미정의 동작
++i;
return sq; // 0 1 4 9 16
});
std::cout << "제곱: ";
for (int x : custom) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
TestDataGenerator가 엔진 gen을 멤버로 들고 있는 것은 앞에서 말한 “엔진은 한 번 만들어 재사용” 원칙을 따른 것입니다. randomInts를 여러 번 호출해도 수열이 이어지며, 생성자에서 random_device 대신 고정 시드를 받도록 바꾸면 실패한 테스트를 같은 데이터로 재현할 수 있습니다. 테스트 데이터 생성기라면 시드를 로그로 남겨 두는 것이 특히 유용합니다.
pattern의 람다는 [&, i = 0]로 pattern과 patternSize는 참조로, 인덱스 i는 람다 소유로 캡처합니다. patternSize가 0이면 % 0으로 0으로 나누기(미정의 동작)가 되므로, 빈 패턴이 들어올 수 있다면 먼저 확인해야 합니다. 또 i의 타입이 int로 추론되어 size_t인 patternSize와 섞이는데, 여기서는 값이 작아 문제가 없지만 size_t i = 0으로 맞추는 편이 경고를 피합니다.
custom에 넘기는 람다의 원래 코드는 return i * i++;였는데, 한 식 안에서 i를 읽으면서 동시에 수정하는 것은 곱셈 피연산자 사이에 평가 순서가 정해져 있지 않아 미정의 동작입니다. 컴파일러에 따라 0 1 4 9 16이 나오기도 하고 0 2 6 12 20이 나오기도 하며, GCC는 -Wsequence-point(-Wall에 포함) 경고로 알려 줍니다. 위처럼 계산과 증가를 별도 문장으로 나누면 해결됩니다.
생성 알고리즘 요약
- fill: 고정 값으로 채우기 (빠름)
- fill_n: N개를 고정 값으로
- generate: 함수로 생성 (유연함)
- generate_n: N개를 함수로 생성
- iota: 순차 증가 값 생성 (
<numeric>)
상황별 선택표
| 상황 | 권장 알고리즘 | 이유 |
|---|---|---|
| 고정 값 | fill | 최적화, 빠름 |
| 순차 값 | iota | 간결함 |
| 난수 | generate + <random> | 품질 높음 |
| 복잡한 로직 | generate + 람다 | 유연함 |
| 상태 유지 | generate + 참조 캡처 | 상태 공유 |
성능과 병렬화 메모
- 고정 값은
fill사용 (최적화됨) generate는 함수 호출 오버헤드 있음- 대량 데이터는 병렬 알고리즘 고려 (
std::execution::par). 단, 상태를 가진 생성기를 병렬generate에 넘기면 데이터 경쟁이 되므로 병렬화는fill이나 상태 없는 연산에만 적용
이어서 볼 STL 알고리즘
같이 보면 좋은 글
- C++ Algorithm Copy
- C++ reverse·rotate·reverse_copy
- C++ count·count_if와 all_of·any_of·none_of로 조건 집계하기
- C++ remove·remove_if가 원소를 지우지 않는 이유: erase-remove와 C++20 erase_if
자주 묻는 질문 (FAQ)
Q. 빈 vector에 std::fill이나 std::generate를 호출했는데 아무 값도 들어가지 않는 이유는 무엇인가요?
A. fill과 generate는 이미 존재하는 [first, last) 범위의 원소에 값을 쓰는 알고리즘이라 컨테이너 크기를 늘려 주지 않습니다. 빈 vector는 begin()과 end()가 같으므로 아무 일도 하지 않고 끝납니다. 미리 resize로 크기를 정하거나, std::generate_n에 std::back_inserter(v)를 넘겨 원소를 추가하면서 값을 채우는 방법을 써야 합니다.