C++ 반복자 | 'Iterator' 완벽 가이드
이 글의 핵심
C++ 반복자(iterator) 는 컨테이너 요소를 순회하는 객체입니다. 범위 기반 for와 vector에서 begin/end로 쓰이며, size_t·ptrdiff_t로 거리·인덱스를 다룰 때와 Composite 패턴으로 트리 순회를 통일할 때도 자주 씁니다.
반복자 기본
반복자(iterator) 는 컨테이너 요소를 순회하는 객체입니다. 범위 기반 for와 vector에서 begin/end로 사용되며, size_t·ptrdiff_t로 거리·인덱스를 다룰 때와 Composite 패턴으로 트리 순회를 통일할 때도 자주 씁니다.
반복자 개념도
graph LR
A[Container] --> B[begin]
A --> C[end]
B --> D[Iter]
D -->|++| E[Iter]
E -->|++| F[Iter]
F -->|++| C
D -->|*| G[Elem 1]
E -->|*| H[Elem 2]
F -->|*| I[Elem 3]
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4, 5};
// begin/end
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 역방향
for (auto it = v.rbegin(); it != v.rend(); ++it) {
cout << *it << " "; // 5 4 3 2 1
}
}
반복자 종류
반복자 계층 구조
graph TD
A[Input Iterator] --> C[Forward Iterator]
B[Output Iterator] --> C
C --> D[Bidirectional Iterator]
D --> E[Random Access Iterator]
E --> F[Contiguous Iterator C++20]
A -.->|읽기 전용| A1[istream_iterator]
B -.->|쓰기 전용| B1[ostream_iterator]
C -.->|단방향| C1[forward_list]
D -.->|양방향| D1[list, set, map]
E -.->|임의 접근| E1[vector, deque, array]
F -.->|연속 메모리| F1[vector, array, string]
반복자 기능 비교표
| 반복자 타입 | 읽기 | 쓰기 | 전진 | 후진 | 임의 접근 | 예시 컨테이너 |
|---|---|---|---|---|---|---|
| Input | ✅ | ❌ | ✅ | ❌ | ❌ | istream_iterator |
| Output | ❌ | ✅ | ✅ | ❌ | ❌ | ostream_iterator |
| Forward | ✅ | ✅ | ✅ | ❌ | ❌ | forward_list |
| Bidirectional | ✅ | ✅ | ✅ | ✅ | ❌ | list, set, map |
| Random Access | ✅ | ✅ | ✅ | ✅ | ✅ | vector, deque |
Input Iterator
// 읽기 전용, 한 번만 순회
istream_iterator<int> in(cin);
istream_iterator<int> eof;
vector<int> v(in, eof); // 입력 읽기
Output Iterator
// 쓰기 전용
ostream_iterator<int> out(cout, " ");
vector<int> v = {1, 2, 3};
copy(v.begin(), v.end(), out); // 1 2 3
Forward Iterator
C/C++ 예제 코드입니다.
// 읽기/쓰기, 여러 번 순회
forward_list<int> fl = {1, 2, 3};
for (auto it = fl.begin(); it != fl.end(); ++it) {
*it *= 2;
}
Bidirectional Iterator
C/C++ 예제 코드입니다.
// 양방향 이동
list<int> l = {1, 2, 3, 4, 5};
auto it = l.end();
--it; // 마지막 원소
cout << *it << endl; // 5
Random Access Iterator
C/C++ 예제 코드입니다.
// 임의 접근
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
it += 3; // 3칸 이동
cout << *it << endl; // 4
cout << v[2] << endl; // 3
카테고리별 연산·복잡도 요구사항(표준 개념과의 대응)
표준은 반복자를 연산 집합으로 정의합니다. 알고리즘이 RandomAccessIterator를 요구하면 +=, -, <, [] 등이 상수 시간(분摊)에 가깝게 동작해야 하며, BidirectionalIterator까지만 되면 advance가 선형 시간이 될 수 있습니다. Contiguous Iterator(C++20)는 to_address로 원시 포인터로 환원 가능한 임의 접근 반복자로, vector·array·string의 일반 반복자가 여기에 해당합니다.
| 카테고리 | 핵심 추가 연산 | 알고리즘에서의 의미 |
|---|---|---|
| Input | ==, !=, *(읽기), ++(한 번 소비) | 단일 패스, 동일 구간 재순회 보장 없음 |
| Output | *(쓰기), ++ | 쓰기 전용, 읽기와 섞으면 제약 많음 |
| Forward | Input+Output 성격의 다회 순회 | 동일한 순서로 재현 가능한 읽기 |
| Bidirectional | -- | 역방향 한 칸, prev가 정말 O(1) |
| Random Access | +=, -, [], < 등 | distance·advance가 상수 시간(분할 상각) |
| Contiguous(C++20) | 메모리 연속 | SIMD·저수준 API와 연동 |
C++20 Ranges에서는 위 분류가 std::input_iterator / forward_iterator / bidirectional_iterator / random_access_iterator / contiguous_iterator 등 concept으로 재정리되었으며, 커스텀 반복자는 연산자뿐 아니라 의미론(복사 횟수, 무효화)까지 맞춰야 합니다. 자세한 문법 요구사항은 cppreference의 LegacyIterator 계열 문서를 기준으로 삼는 것이 안전합니다.
실전 예시
예시 1: 커스텀 반복자
class Range {
private:
int current;
int end;
public:
class Iterator {
private:
int value;
public:
using iterator_category = forward_iterator_tag;
using value_type = int;
using difference_type = ptrdiff_t;
using pointer = int*;
using reference = int&;
Iterator(int v) : value(v) {}
int operator*() const { return value; }
Iterator& operator++() {
++value;
return *this;
}
bool operator!=(const Iterator& other) const {
return value != other.value;
}
};
Range(int start, int end) : current(start), end(end) {}
Iterator begin() { return Iterator(current); }
Iterator end() { return Iterator(this->end); }
};
int main() {
for (int x : Range(0, 10)) {
cout << x << " "; // 0 1 2 ... 9
}
}
예시 2: 필터 반복자
template<typename Iter, typename Pred>
class FilterIterator {
private:
Iter current;
Iter end;
Pred predicate;
void advance() {
while (current != end && !predicate(*current)) {
++current;
}
}
public:
FilterIterator(Iter begin, Iter end, Pred pred)
: current(begin), end(end), predicate(pred) {
advance();
}
auto operator*() const { return *current; }
FilterIterator& operator++() {
++current;
advance();
return *this;
}
bool operator!=(const FilterIterator& other) const {
return current != other.current;
}
};
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto isEven = [](int x) { return x % 2 == 0; };
FilterIterator begin(v.begin(), v.end(), isEven);
FilterIterator end(v.end(), v.end(), isEven);
for (auto it = begin; it != end; ++it) {
cout << *it << " "; // 2 4 6 8 10
}
}
예시 3: 변환 반복자
template<typename Iter, typename Func>
class TransformIterator {
private:
Iter current;
Func transform;
public:
TransformIterator(Iter it, Func f) : current(it), transform(f) {}
auto operator*() const {
return transform(*current);
}
TransformIterator& operator++() {
++current;
return *this;
}
bool operator!=(const TransformIterator& other) const {
return current != other.current;
}
};
int main() {
vector<int> v = {1, 2, 3, 4, 5};
auto square = [](int x) { return x * x; };
TransformIterator begin(v.begin(), square);
TransformIterator end(v.end(), square);
for (auto it = begin; it != end; ++it) {
cout << *it << " "; // 1 4 9 16 25
}
}
반복자 연산
반복자 연산 함수 비교
| 함수 | 시간 복잡도 | 지원 반복자 | 설명 |
|---|---|---|---|
| advance(it, n) | O(1) ~ O(N) | 모든 반복자 | n칸 이동 (제자리 수정) |
| distance(first, last) | O(1) ~ O(N) | Input 이상 | 두 반복자 간 거리 |
| next(it, n=1) | O(1) ~ O(N) | Forward 이상 | n칸 앞 반복자 반환 |
| prev(it, n=1) | O(1) ~ O(N) | Bidirectional 이상 | n칸 뒤 반복자 반환 |
| iter_swap(it1, it2) | O(1) | Forward 이상 | 두 반복자 값 교환 |
C/C++ 예제 코드입니다.
// 실행 예제
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
// 전진
advance(it, 3); // 3칸 이동
cout << *it << endl; // 4
// 거리
auto dist = distance(v.begin(), v.end());
cout << dist << endl; // 5
// 다음/이전
auto next_it = next(it);
auto prev_it = prev(it);
advance vs next 차이:
graph LR
A[it = v.begin] --> B{Which?}
B -->|advance it, 3| C[Modify it]
C --> D[it = begin+3]
B -->|next it, 3| E[Return new]
E --> F[it = begin]
E --> G[return = begin+3]
이터레이터 특성(iterator_traits)과 타입 정보
템플릿 알고리즘은 구체 반복자 타입을 몰라도 iterator_traits<Iter>로 값 타입·차이 타입·카테고리를 얻습니다. C++98 시절에는 iterator 클래스에 typedef 다섯 종(iterator_category, value_type, difference_type, pointer, reference)을 붙이는 패턴이 표준이었으며, 포인터는 iterator_traits<T*> 부분 특수화로 동일 정보를 제공합니다.
iterator_category와 C++20 iterator_concept
C++17까지는 random_access_iterator_tag 등 태그 타입이 카테고리의 전부였습니다. C++20부터 컨테이너 반복자는 iterator_traits::iterator_concept가 std::contiguous_iterator 같은 더 정밀한 개념을 가리킬 수 있습니다. 알고리즘 구현체(표준 라이브러리)는 우선 iterator_concept, 없으면 iterator_category로 태그 디스패치·제약을 걸어 최적 경로를 고릅니다. 태그 디스패치와 연결해 이해하면 컴파일 타임 분기의 실체가 보입니다.
#include <iterator>
#include <vector>
#include <iostream>
#include <typeinfo>
template <class Iter>
void describe() {
using Tr = std::iterator_traits<Iter>;
// C++20: 일부 구현에서 Tr::iterator_concept로 더 세분된 개념을 추가로 노출
std::cout << "iterator_category: "
<< typeid(typename Tr::iterator_category).name() << '\n';
}
int main() {
describe<std::vector<int>::iterator>();
}
pointer·reference와 프록시 반복자
일부 컨테이너(vector<bool> 등)는 프록시 참조를 쓰므로 reference가 정말 T&가 아닐 수 있습니다. 이 경우 auto로 역참조 결과를 받는 방식과, 연산자 -> 체인이 기대와 다를 수 있으니 범위 기반 for의 auto& 선택과 함께 읽는 것이 좋습니다. 연속 반복자가 아니면 std::to_address 사용도 제한적입니다.
std::iter_difference_t, std::iter_value_t (C++20)
C++20에서는 iterator_traits를 직접 꺼내기보다 std::iter_value_t<It>, std::iter_reference_t<It>, std::iter_difference_t<It> 같은 별칭 템플릿으로 가독성을 높입니다. Ranges 알고리즘 시그니처는 이 타입들을 기본으로 가정합니다.
#include <iterator>
#include <vector>
template <class It>
void use(It first, It last) {
std::iter_difference_t<It> n = std::distance(first, last);
(void)n;
std::iter_value_t<It> copy = *first;
(void)copy;
}
이터레이터 어댑터 패턴
어댑터는 기존 반복자·출력 대상을 감싸 다른 연산 모델을 제공합니다. 직접 FilterIterator를 작성하기 전에 표준 조합을 검토하는 것이 실무에서 안전합니다.
삽입 반복자(back_insert_iterator 등)
std::back_inserter, std::front_inserter, std::inserter는 컨테이너에 push_back 등을 호출하는 출력 반복자입니다. copy(src.begin(), src.end(), back_inserter(dest))는 미리 공간을 잡지 않고 확장 삽입할 때 유용합니다. 단, vector 재할당으로 인한 무효화를 염두에 두어야 합니다.
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> a{1, 2, 3}, b;
std::copy(a.begin(), a.end(), std::back_inserter(b));
}
reverse_iterator·move_iterator
reverse_iterator는 물리적 반복자를 한 칸 앞으로 옮겨 “역방향 ++” 의미를 구현합니다(base()와의 오프셋 관계에 주의). move_iterator는 역참조 시 std::move로 요소를 소비하는 출력에 가깝게 동작해, 이동만 허용하는 알고리즘에 씁니다.
counted_iterator + default_sentinel (C++20)
반복 횟수와 끝을 나타내는 센티널을 분리해, 길이만 알고 끝 반복자는 다른 타입인 패턴을 안전하게 표현합니다. Ranges와 함께 쓰면 끝 타입이 다른 범위(sentinel range)를 자연스럽게 다룹니다.
#include <iterator>
#include <algorithm>
#include <cstddef>
int main() {
int a[] = {1, 2, 3, 4, 5};
auto first = std::counted_iterator<int*>(a, 5);
std::for_each(first, std::default_sentinel, [](int x) { (void)x; });
}
뷰·파이프라인과의 역할 분담
C++20 std::ranges::views::filter, transform 등은 커스텀 반복자를 직접 만들지 않고도 지연 평가 파이프라인을 제공합니다. C++20 Ranges 기초와 함께 보면, “어댑터를 수작업으로 구현할지, 뷰로 조합할지” 판단이 쉬워집니다.
범위 기반 for의 디슈가링(desugaring)
range-for는 문법적 설탕(syntactic sugar)에 해당하지만, C++17에서 규칙이 정교해졌습니다. 핵심은 범위 표현식을 한 번 평가해 임시를 잡으며, begin/end를 ADL로 찾는다는 점입니다.
C++17 이후의 개략적 형태
대략 다음과 같은 구조입니다(실제 표준 문구와 begin/end 후보 집합은 더 길으며, 배열·브레이스 초기화 등 특례가 있습니다).
// for (for-range-declaration : for-range-initializer) 문
// 내부적으로는 개념적으로:
// auto&& __range = for-range-initializer;
// auto __begin = begin(__range); // ADL: std::begin + 멤버 begin
// auto __end = end(__range);
// for (; __begin != __end; ++__begin) {
// for-range-declaration = *__begin;
// ...
// }
중요한 결과: 커스텀 타입에 자유 함수 begin/end를 넣으면 표준 std::begin과 함께 ADL로 발견됩니다. 임시 벡터를 돌릴 때 __range 수명이 루프 전체에 걸쳐 유지되는 이유도 여기에서 나옵니다. 세부는 범위 기반 for 전용 글의 예시와 함께 보는 것을 권합니다.
begin ≠ 멤버만
일부 서드파티 버퍼는 멤버가 아닌 비멤버 begin만 제공합니다. C++17 규칙은 이런 타입도 range-for에 끼워 넣기 쉽게 설계되었습니다. 반대로, 잘못된 end 타입(센티널과 반복자 비교 불가 등)은 템플릿 에러로 드러나므로, 커스텀 범위는 같은 연산으로 비교 가능한 begin/end 쌍을 맞추는 것이 첫 번째 과제입니다.
프로덕션 이터레이터 패턴
end() 캐싱과 성능
매 루프마다 end()를 호출하는 것은 인라인·최적화되면 문제 없는 경우가 많지만, 디버그 빌드나 복잡한 컨테이너에서는 auto e = c.end()로 한 번만 잡아 두는 패턴이 읽기도 좋습니다. 범위 for는 이를 표준적으로 처리합니다.
센티널 범위와 알고리즘 선택
C++20 이후에는 begin과 end가 다른 타입인 범위가 일반적입니다(널 종료 문자열, istream 뷰 등). 이때 레거시 std::sort(first, last)처럼 동일 타입을 가정한 API와 충돌하지 않도록, std::ranges:: 알고리즘을 쓰는 편이 안전합니다.
예외·무효화·noexcept
저수준 반복자 루프는 예외 안전성과 반복자 무효화 규칙이 합쳐져 버그가 나기 쉽습니다. vector에서 erase 후 erase 반환 반복자를 사용하거나, 삽입으로 재할당이 일어날 수 있으면 인덱스 기반으로 바꾸는 등, 무효화를 먼저 정리한 뒤 반복자 설계를 하는 것이 좋습니다.
“직접 반복자 클래스” 대신
내부 라이브러리가 아니라면 Boost.Iterator의 iterator_facade류, 또는 인덱스 + 스팬, std::ranges 뷰로 요구사항을 줄이는 편이 유지보수 비용이 낮습니다. 꼭 필요할 때만 최소 연산자 집합으로 forward_iterator_tag 수준을 목표로 삼으며, 동치 관계(==)·복사 후 독립성을 문서화하세요.
자주 발생하는 문제
문제 1: 무효화된 반복자
// ❌ 반복자 무효화
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) {
v.erase(it); // it 무효화!
// ++it; // 위험!
}
}
// ✅ erase 반환값 사용
for (auto it = v.begin(); it != v.end();) {
if (*it == 3) {
it = v.erase(it); // 다음 반복자 반환
} else {
++it;
}
}
문제 2: end() 역참조
// ❌ end() 역참조
vector<int> v = {1, 2, 3};
auto it = v.end();
// cout << *it << endl; // UB
// ✅ end() 체크
if (it != v.end()) {
cout << *it << endl;
}
문제 3: 반복자 타입 불일치
C/C++ 예제 코드입니다.
// ❌ 타입 불일치
vector<int> v;
list<int> l;
// auto it = v.begin();
// it = l.begin(); // 에러
// ✅ 올바른 타입
auto vit = v.begin();
auto lit = l.begin();
FAQ
Q1: 반복자는 언제 사용하나요?
A:
- 컨테이너 순회
- STL 알고리즘
- 범위 지정
Q2: 포인터 vs 반복자?
A: 반복자가 더 추상적이고 안전합니다.
Q3: 반복자 무효화는?
A:
- vector: 삽입/삭제 시
- list: 삭제된 원소만
- map: 삭제된 원소만
Q4: 커스텀 반복자는?
A: 5가지 typedef와 연산자 구현 필요.
Q5: 반복자 성능은?
A: 인라인화로 포인터와 비슷.
Q6: 반복자 학습 리소스는?
A:
- “Effective STL” (Scott Meyers)
- cppreference.com
- “The C++ Standard Library”
관련 글: 범위 기반 for, vector 완벽 가이드, size_t·ptrdiff_t, Composite 패턴.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 범위 기반 for | “Range-based for” 가이드
- C++ STL vector | “배열보다 편한” 벡터 완벽 정리 [실전 예제]
- C++ size_t & ptrdiff_t | “크기 타입” 가이드
- C++ Composite 패턴 완벽 가이드 | 트리 구조를 동일 인터페이스로 다루기
관련 글
- C++ 반복자 무효화 에러
- C++ 반복자 기초 완벽 가이드 | iterator 카테고리·begin/end·역방향 반복자·실전 패턴
- C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional
- C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
- C++ Tag Dispatch 완벽 가이드 | 컴파일 타임 함수 선택과 STL 최적화
이 글에서 다루는 키워드 (관련 검색어)
C++, iterator, 반복자, STL, range 등으로 검색하시면 이 글이 도움이 됩니다.