C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional
이 글의 핵심
C++ 커스텀 반복자 완벽 가이드에 대한 실전 가이드입니다. Forward·Bidirectional 등을 예제와 함께 상세히 설명합니다.
들어가며: “우리 컨테이너에 std::sort를 쓸 수 없어요”
문제 시나리오
도메인 특화 컨테이너를 만들었는데, for (auto x : myContainer)나 std::sort(v.begin(), v.end())에 그대로 넣을 수 없어 답답했던 경험이 있으신가요? 표준 vector, list는 begin()/end()가 반복자를 반환하므로 STL 알고리즘과 range-based for에 바로 쓸 수 있지만, 우리가 만든 타입은 컴파일 에러가 납니다.
// 우리가 만든 링 버퍼
class RingBuffer {
std::vector<int> data_;
size_t head_ = 0;
public:
void push(int x) { /* ... */ }
int front() const { return data_[head_]; }
};
int main() {
RingBuffer buf;
buf.push(1); buf.push(2); buf.push(3);
// ❌ 컴파일 에러: begin/end가 없음
// for (auto x : buf) { ... }
// std::sort(buf.begin(), buf.end());
}
실제 프로덕션에서 겪는 문제들:
- 링 버퍼:
RingBuffer를 순회해 최근 N개 로그만 처리하고 싶은데begin/end가 없어 수동 인덱스 접근만 가능 - 슬라이스:
vector의 일부 구간만std::sort에 넘기고 싶은데subrange없이 복사해야 함 - 연결 리스트: 자체
LinkedList를std::find_if로 검색하고 싶은데 반복자 없음 - 파일 스트림:
istream을 라인 단위로ranges::for_each에 넘기고 싶은데input_iterator가 없음 - 네트워크 패킷: 수신 버퍼를 청크 단위로 순회해 파싱하고 싶은데 반복자 인터페이스 부재
- 데이터베이스 커서: 쿼리 결과를 한 행씩
std::accumulate에 넘기고 싶은데 반복자 미지원
원인: RingBuffer에 begin()/end()가 없고, 반복자를 정의하지 않았기 때문입니다. 해결: 커스텀 반복자를 구현해 begin()/end()가 반환하도록 하면 됩니다.
flowchart LR
subgraph before["Before: 반복자 없음"]
B1[RingBuffer] --> B2[begin/end 없음]
B2 --> B3[for-range ❌]
B2 --> B4["std sort ❌"]
end
subgraph after["After: 커스텀 반복자"]
A1[RingBuffer] --> A2[반복자 구현]
A2 --> A3[for-range ✅]
A2 --> A4["std sort ✅"]
end
반복자 계층 구조:
| 반복자 종류 | 연산 | 예시 컨테이너 |
|---|---|---|
| output_iterator | ++, *it = x (쓰기 전용) | ostream_iterator, back_inserter |
| input_iterator | ++, *, == | istream, LineIterator |
| forward_iterator | input + 다중 패스 | singly-linked list |
| bidirectional_iterator | forward + -- | list, map |
| random_access_iterator | bidirectional + +, -, [], < | vector, array, SliceIterator |
이 글을 읽으면:
- Forward·Bidirectional·Random Access 반복자를 완전히 구현할 수 있습니다.
iterator_traits와iterator_category를 올바르게 설정할 수 있습니다.- 흔한 실수와 해결법을 알 수 있습니다.
- 프로덕션에서 사용하는 패턴을 익힐 수 있습니다.
목차
- 반복자 요구 사항
- Input Iterator 완전 구현
- Forward Iterator 완전 구현
- Bidirectional Iterator 완전 구현
- Random Access Iterator 완전 구현
- 자주 발생하는 에러와 해결법
- 모범 사례 (Best Practices)
- 프로덕션 패턴
- 구현 체크리스트
1. 반복자 요구 사항
최소 조건 (input_iterator)
반복자가 되려면 다음 연산이 필요합니다:
operator*— 현재 요소 참조operator++— prefix/postfix 증가operator==— 비교 (같은 컨테이너 내 반복자끼리)
// input_iterator 최소 요구 사항
class MyIterator {
public:
T& operator*() const { return *ptr_; }
MyIterator& operator++() { ++ptr_; return *this; }
MyIterator operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
friend bool operator==(const MyIterator& a, const MyIterator& b) {
return a.ptr_ == b.ptr_;
}
};
iterator_traits 특수화
STL 알고리즘은 std::iterator_traits<It>를 사용해 반복자 정보를 가져옵니다. C++20 이전에는 반드시 특수화해야 합니다.
#include <iterator>
template <typename T>
struct std::iterator_traits<MyIterator<T>> {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
using pointer = T*;
using reference = T&;
};
C++20에서: std::input_iterator 개념을 만족하려면 value_type, difference_type, iterator_category(또는 iterator_concept)가 필요합니다. iterator_traits는 자동 추론되지만, 명시적으로 정의하는 것이 안전합니다.
iterator_traits 상세: 각 타입별 역할
| 타입 별칭 | 용도 | 예시 |
|---|---|---|
value_type | *it가 반환하는 값의 타입 | int, std::string |
difference_type | 두 반복자 간 거리 (it1 - it2) | std::ptrdiff_t |
iterator_category | 반복자 종류 (알고리즘 선택용) | forward_iterator_tag |
pointer | 요소 포인터 타입 | T* |
reference | 요소 참조 타입 | T& |
STL 알고리즘은 iterator_category로 어떤 연산이 가능한지 판단합니다. std::sort는 random_access_iterator_tag만 허용하고, std::find는 input_iterator_tag 이상이면 됩니다.
output_iterator: *it = value로 쓰기만 가능. std::copy의 출력 대상, std::back_inserter 등. 읽기(*it)는 보장되지 않습니다.
// iterator_traits가 없으면 std::distance, std::advance 등에서 컴파일 에러
template <typename It>
void my_algorithm(It first, It last) {
// iterator_traits<It>::difference_type 필요
auto n = std::distance(first, last);
// iterator_traits<It>::iterator_category 필요 (최적화 분기)
std::advance(first, n / 2);
}
2. Input Iterator 완전 구현
문제 시나리오: 파일·스트림을 STL 알고리즘에
파일을 라인 단위로 읽어 std::find나 std::count_if에 넘기고 싶은데, std::istream에는 반복자가 없습니다. std::istream_iterator는 공백 단위라 라인 단위 순회가 불가능합니다. Input iterator는 한 번만 순회 가능하며, 다중 패스가 보장되지 않습니다 (파일 포인터가 앞으로만 이동).
flowchart LR
subgraph input["Input Iterator"]
I1[istream] --> I2[한 번만 읽기]
I2 --> I3[operator*]
I3 --> I4[operator++]
end
완전한 Input Iterator 예제: 라인 단위 istream 반복자
#include <iterator>
#include <algorithm>
#include <iostream>
#include <sstream>
#include <string>
class LineIterator {
std::istream* stream_ = nullptr;
std::string line_;
bool at_end_ = true;
public:
using value_type = std::string;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
using pointer = const std::string*;
using reference = const std::string&;
LineIterator() = default;
explicit LineIterator(std::istream& is) : stream_(&is) { advance(); }
const std::string& operator*() const { return line_; }
LineIterator& operator++() { advance(); return *this; }
LineIterator operator++(int) { auto tmp = *this; ++*this; return tmp; }
friend bool operator==(const LineIterator& a, const LineIterator& b) {
return a.at_end_ == b.at_end_ && (a.at_end_ || a.stream_ == b.stream_);
}
friend bool operator!=(const LineIterator& a, const LineIterator& b) {
return !(a == b);
}
private:
void advance() {
at_end_ = !(stream_ && std::getline(*stream_, line_));
}
};
template <> struct std::iterator_traits<LineIterator> {
using value_type = std::string;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
using pointer = const std::string*;
using reference = const std::string&;
};
class LineRange {
std::istream* stream_ = nullptr;
public:
explicit LineRange(std::istream& is) : stream_(&is) {}
LineIterator begin() const { return LineIterator(*stream_); }
LineIterator end() const { return LineIterator(); }
};
int main() {
std::istringstream iss("apple\nbanana\ncherry\ndate\n");
LineRange lines(iss);
auto it = std::find(lines.begin(), lines.end(), "cherry");
if (it != lines.end()) std::cout << "Found: " << *it << "\n";
}
핵심 포인트:
- Input iterator는 다중 패스 불가: 한 번
++하면 이전 값을 다시 읽을 수 없음 iterator_category = std::input_iterator_tag로std::find,std::copy등 사용 가능operator*는const참조 반환 (읽기 전용)
실행 결과:
Found: cherry
반복자 카테고리 비교표
| 카테고리 | 다중 패스 | 역방향 | 임의 접근 | 사용 가능 알고리즘 |
|---|---|---|---|---|
| output_iterator | ❌ | ❌ | ❌ | copy (출력), fill |
| input_iterator | ❌ | ❌ | ❌ | find, copy, count |
| forward_iterator | ✅ | ❌ | ❌ | + 위 모든 것 |
| bidirectional_iterator | ✅ | ✅ | ❌ | + reverse, prev |
| random_access_iterator | ✅ | ✅ | ✅ | + sort, binary_search |
3. Forward Iterator 완전 구현
문제 시나리오: 단일 연결 리스트
단일 연결 리스트를 만들었는데, std::find나 range-based for에 넣을 수 없습니다. Forward iterator는 한 방향으로만 순회하며, 다중 패스(여러 번 순회해도 같은 결과)가 보장됩니다.
flowchart LR
subgraph list["단일 연결 리스트"]
N1[노드1] --> N2[노드2]
N2 --> N3[노드3]
N3 --> N4[null]
end
subgraph iter["Forward Iterator"]
I1[operator++] --> I2[다음 노드]
I2 --> I3[operator*]
end
완전한 Forward Iterator 예제
#include <iterator>
#include <algorithm>
#include <iostream>
template <typename T>
struct ListNode {
T value;
ListNode* next = nullptr;
};
template <typename T>
class ForwardListIterator {
ListNode<T>* node_ = nullptr;
public:
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
using pointer = T*;
using reference = T&;
ForwardListIterator() = default;
explicit ForwardListIterator(ListNode<T>* n) : node_(n) {}
T& operator*() const { return node_->value; }
T* operator->() const { return &node_->value; }
ForwardListIterator& operator++() {
node_ = node_ ? node_->next : nullptr;
return *this;
}
ForwardListIterator operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
friend bool operator==(const ForwardListIterator& a,
const ForwardListIterator& b) {
return a.node_ == b.node_;
}
friend bool operator!=(const ForwardListIterator& a,
const ForwardListIterator& b) {
return !(a == b);
}
};
template <typename T>
struct std::iterator_traits<ForwardListIterator<T>> {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
using pointer = T*;
using reference = T&;
};
template <typename T>
class ForwardList {
ListNode<T>* head_ = nullptr;
ListNode<T>* tail_ = nullptr;
public:
using iterator = ForwardListIterator<T>;
~ForwardList() {
while (head_) {
auto* next = head_->next;
delete head_;
head_ = next;
}
}
void push_back(const T& value) {
auto* node = new ListNode<T>{value, nullptr};
if (!head_) head_ = tail_ = node;
else { tail_->next = node; tail_ = node; }
}
iterator begin() { return iterator(head_); }
iterator end() { return iterator(nullptr); }
iterator begin() const { return iterator(head_); }
iterator end() const { return iterator(nullptr); }
};
int main() {
ForwardList<int> list;
list.push_back(1);
list.push_back(2);
list.push_back(3);
for (auto x : list)
std::cout << x << " "; // 1 2 3
auto it = std::find(list.begin(), list.end(), 2);
if (it != list.end())
std::cout << "\nFound: " << *it; // Found: 2
}
핵심 포인트:
operator*,operator++,operator==필수iterator_category = std::forward_iterator_tag로std::find,std::for_each등 사용 가능operator->는operator*의 주소 반환으로 구현 가능
실행 결과:
1 2 3
Found: 2
4. Bidirectional Iterator 완전 구현
문제 시나리오: 이중 연결 리스트
이중 연결 리스트는 reverse_iterator나 std::reverse와 함께 사용할 수 있어야 합니다. Bidirectional iterator는 **operator--**로 역방향 순회가 가능합니다.
flowchart LR
subgraph dlist["이중 연결 리스트"]
N1[prev] --> N2[노드]
N2 --> N3[next]
end
subgraph iter["Bidirectional Iterator"]
I1[operator++] --> I2[다음]
I2 --> I3[operator--]
I3 --> I4[이전]
end
완전한 Bidirectional Iterator 예제
#include <iterator>
#include <algorithm>
#include <iostream>
template <typename T>
struct DListNode {
T value;
DListNode* prev = nullptr;
DListNode* next = nullptr;
};
template <typename T>
class BidirectionalListIterator {
DListNode<T>* node_ = nullptr;
public:
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;
using pointer = T*;
using reference = T&;
BidirectionalListIterator() = default;
explicit BidirectionalListIterator(DListNode<T>* n) : node_(n) {}
T& operator*() const { return node_->value; }
T* operator->() const { return &node_->value; }
BidirectionalListIterator& operator++() {
node_ = node_ ? node_->next : nullptr;
return *this;
}
BidirectionalListIterator operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
BidirectionalListIterator& operator--() {
node_ = node_ ? node_->prev : nullptr;
return *this;
}
BidirectionalListIterator operator--(int) {
auto tmp = *this;
--*this;
return tmp;
}
friend bool operator==(const BidirectionalListIterator& a,
const BidirectionalListIterator& b) {
return a.node_ == b.node_;
}
friend bool operator!=(const BidirectionalListIterator& a,
const BidirectionalListIterator& b) {
return !(a == b);
}
};
template <typename T>
struct std::iterator_traits<BidirectionalListIterator<T>> {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;
using pointer = T*;
using reference = T&;
};
template <typename T>
class BidirectionalList {
DListNode<T> sentinel_; // 더미 노드
public:
using iterator = BidirectionalListIterator<T>;
BidirectionalList() {
sentinel_.next = &sentinel_;
sentinel_.prev = &sentinel_;
}
~BidirectionalList() {
while (sentinel_.next != &sentinel_) {
auto* node = sentinel_.next;
sentinel_.next = node->next;
delete node;
}
}
void push_back(const T& value) {
auto* node = new DListNode<T>{value, sentinel_.prev, &sentinel_};
sentinel_.prev->next = node;
sentinel_.prev = node;
}
iterator begin() { return iterator(sentinel_.next); }
iterator end() { return iterator(&sentinel_); }
iterator begin() const { return iterator(sentinel_.next); }
iterator end() const { return iterator(const_cast<DListNode<T>*>(&sentinel_)); }
};
int main() {
BidirectionalList<int> list;
list.push_back(1);
list.push_back(2);
list.push_back(3);
for (auto x : list)
std::cout << x << " "; // 1 2 3
// 역순 순회
std::cout << "\nReverse: ";
for (auto it = list.end(); it != list.begin(); ) {
--it;
std::cout << *it << " "; // 3 2 1
}
std::cout << "\n";
std::reverse(list.begin(), list.end());
for (auto x : list)
std::cout << x << " "; // 3 2 1
}
핵심 포인트:
operator--는operator++의 역방향std::list처럼 sentinel을 쓰면end()가 항상 유효한 반복자std::reverse는 bidirectional iterator를 요구
실행 결과:
1 2 3
Reverse: 3 2 1
3 2 1
5. Random Access Iterator 완전 구현
문제 시나리오: 슬라이스·링 버퍼
벡터의 일부 구간이나 링 버퍼를 std::sort에 넘기고 싶습니다. Random access iterator는 operator+, operator-, operator[], operator< 등을 제공해 임의 접근이 가능합니다.
flowchart TB
subgraph ra["Random Access Iterator"]
R1[operator+]
R2[operator-]
R3["operator"]
R4["operator"]
end
R1 --> R3
R2 --> R3
R4 --> R1
완전한 Random Access Iterator 예제 (슬라이스)
#include <iterator>
#include <algorithm>
#include <iostream>
#include <vector>
template <typename T>
class SliceIterator {
T* ptr_ = nullptr;
T* end_ = nullptr;
public:
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::random_access_iterator_tag;
using pointer = T*;
using reference = T&;
SliceIterator() = default;
SliceIterator(T* p, T* e) : ptr_(p), end_(e) {}
T& operator*() const { return *ptr_; }
T& operator const { return ptr_[n]; }
T* operator->() const { return ptr_; }
SliceIterator& operator++() { ++ptr_; return *this; }
SliceIterator operator++(int) {
auto tmp = *this;
++ptr_;
return tmp;
}
SliceIterator& operator--() { --ptr_; return *this; }
SliceIterator operator--(int) {
auto tmp = *this;
--ptr_;
return tmp;
}
SliceIterator& operator+=(difference_type n) {
ptr_ += n;
return *this;
}
SliceIterator& operator-=(difference_type n) {
ptr_ -= n;
return *this;
}
SliceIterator operator+(difference_type n) const {
return SliceIterator(ptr_ + n, end_);
}
SliceIterator operator-(difference_type n) const {
return SliceIterator(ptr_ - n, end_);
}
friend SliceIterator operator+(difference_type n, const SliceIterator& it) {
return it + n;
}
difference_type operator-(const SliceIterator& other) const {
return ptr_ - other.ptr_;
}
friend bool operator==(const SliceIterator& a, const SliceIterator& b) {
return a.ptr_ == b.ptr_;
}
friend bool operator!=(const SliceIterator& a, const SliceIterator& b) {
return a.ptr_ != b.ptr_;
}
friend bool operator<(const SliceIterator& a, const SliceIterator& b) {
return a.ptr_ < b.ptr_;
}
friend bool operator>(const SliceIterator& a, const SliceIterator& b) {
return b < a;
}
friend bool operator<=(const SliceIterator& a, const SliceIterator& b) {
return !(b < a);
}
friend bool operator>=(const SliceIterator& a, const SliceIterator& b) {
return !(a < b);
}
};
template <typename T>
struct std::iterator_traits<SliceIterator<T>> {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::random_access_iterator_tag;
using pointer = T*;
using reference = T&;
};
template <typename T>
class SliceRange {
T* data_ = nullptr;
std::size_t size_ = 0;
public:
SliceRange(T* data, std::size_t size) : data_(data), size_(size) {}
SliceIterator<T> begin() const {
return SliceIterator<T>(data_, data_ + size_);
}
SliceIterator<T> end() const {
return SliceIterator<T>(data_ + size_, data_ + size_);
}
};
int main() {
std::vector<int> v = {5, 2, 4, 1, 3};
SliceRange<int> slice(v.data(), v.size());
std::sort(slice.begin(), slice.end());
for (auto x : slice)
std::cout << x << " "; // 1 2 3 4 5
std::cout << "\n";
std::cout << "v[2] = " << v[2]; // 3 (원본 벡터도 정렬됨)
}
핵심 포인트:
operator+,operator-,operator+=,operator-=로std::advance대체operator[]로it + n접근operator<로std::sort요구 사항 충족difference_type은operator-(a, b)로 거리 계산
실행 결과:
1 2 3 4 5
v[2] = 3
링 버퍼 Random Access Iterator (인덱스 기반)
링 버퍼는 operator[]에서 (index_ + n) % capacity_로 인덱스를 계산합니다. operator+=, operator-=도 %로 wrap-around 처리. 주의: operator-(other)는 wrap-around 시 잘못된 값이 나올 수 있어, 에러 10에서 언급한 대로 bidirectional로 제한하는 것이 안전합니다.
// 핵심: operator[]는 (index_ + n) % capacity_
T& operator const {
return (*buf_)[(index_ + n) % capacity_];
}
RingBufferIterator& operator+=(difference_type n) {
index_ = (index_ + n) % capacity_;
return *this;
}
// RingBuffer::begin() → iterator(&data_, head_, capacity)
// RingBuffer::end() → iterator(&data_, (head_+size_)%capacity, capacity)
6. 자주 발생하는 에러와 해결법
에러 1: std::sort가 “random_access_iterator 불만족” 에러
문제 시나리오: ForwardList를 정렬하려고 std::sort(list.begin(), list.end())를 호출했더니 컴파일 에러가 발생합니다.
증상: std::sort(my_range.begin(), my_range.end()) 컴파일 에러
원인: sort는 random_access_iterator만 받습니다. forward_iterator나 bidirectional_iterator만 있으면 sort를 쓸 수 없습니다.
해결:
// ❌ forward_iterator만 있음 → sort 불가
using iterator_category = std::forward_iterator_tag;
// ✅ random_access_iterator 제공
using iterator_category = std::random_access_iterator_tag;
// operator+, operator-, operator[], operator< 등 추가
대안: std::list처럼 list.sort() 멤버 함수를 제공하거나, std::vector로 복사 후 정렬.
에러 2: iterator_traits 미정의
문제 시나리오: 커스텀 LineIterator를 std::distance(begin, end)에 넘겼더니 “incomplete type” 또는 “no matching function” 에러가 발생합니다.
증상: std::distance(it1, it2) 또는 std::advance(it, n) 등에서 에러
원인: iterator_traits가 value_type, difference_type, iterator_category를 제공하지 않음.
해결:
// ✅ iterator_traits 특수화
template <typename T>
struct std::iterator_traits<MyIterator<T>> {
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
};
에러 3: const 반복자와 begin/end
증상: const MyContainer& c에 대해 c.begin() 호출 시 에러
원인: begin()/end()가 non-const 멤버만 있음.
해결:
// ❌ const 객체에서 begin 호출 불가
auto begin() { return iterator(...); }
// ✅ const 오버로드 제공
auto begin() const { return iterator(...); }
auto end() const { return iterator(...); }
에러 4: 반복자 무효화
증상: 순회 중 컨테이너 수정 시 크래시 또는 undefined behavior
원인: vector::push_back 등으로 재할당 시 기존 반복자 무효화.
해결: 순회 중에는 기반 컨테이너를 수정하지 않기. 또는 span처럼 “수명 관리”를 문서화.
// ❌ 위험: 순회 중 수정
for (auto it = v.begin(); it != v.end(); ++it) {
v.push_back(0); // 재할당 가능 → it 무효화
}
// ✅ 순회 후 수정
std::vector<int> to_add;
for (auto x : v) { if (x > 0) to_add.push_back(x); }
for (auto x : to_add) v.push_back(x);
에러 5: operator++(int) 반환 타입
증상: it++ 사용 시 “cannot convert” 에러
원인: operator++(int)가 void를 반환하거나, 잘못된 타입 반환.
해결:
// ❌ 잘못된 코드
void operator++(int) { ++*this; }
// ✅ 올바른 코드: 반복자 복사본 반환
MyIterator operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
에러 6: end() 반복자가 역참조 가능
증상: *container.end() 접근 시 크래시
원인: end()가 유효한 요소를 가리킴.
해결: end()는 “끝 다음”을 가리켜야 합니다. sentinel을 사용하거나 nullptr 반환.
// ✅ end()는 항상 "끝 다음"
iterator end() { return iterator(nullptr); }
// 또는 sentinel
iterator end() { return iterator(&sentinel_); }
에러 7: 서로 다른 컨테이너의 반복자 비교
증상: list1.begin() == list2.end() 비교 시 논리적 오류 또는 UB
원인: 서로 다른 컨테이너의 반복자를 비교하는 것은 정의되지 않은 동작입니다.
해결: 같은 컨테이너에서 얻은 반복자끼리만 비교합니다.
// ❌ 위험: 서로 다른 컨테이너
auto it1 = vec1.begin();
auto it2 = vec2.end();
if (it1 == it2) { /* UB */ }
// ✅ 같은 컨테이너 내에서만
for (auto it = vec.begin(); it != vec.end(); ++it) { }
에러 8: output_iterator와 input_iterator 혼동
증상: std::copy에 input iterator를 출력으로 사용하려 할 때 에러
원인: std::copy(first, last, result)에서 result는 output iterator여야 합니다. *result = value가 가능해야 합니다.
해결: 출력 대상은 vector::iterator, back_insert_iterator 등 쓰기 가능한 반복자를 사용합니다.
// ❌ input_iterator는 *it로 읽기만 가능
LineIterator it(stream);
*it = "hello"; // 컴파일 에러 (const reference 반환)
// ✅ output: back_inserter 등 사용
std::vector<std::string> out;
std::copy(lines.begin(), lines.end(), std::back_inserter(out));
에러 9: iterator_category 과대 선언
증상: random_access_iterator_tag로 선언했는데 operator[]가 잘못 동작
원인: iterator_category를 실제 구현보다 상위로 선언하면, std::sort 등이 해당 연산을 사용하다가 잘못된 결과나 크래시가 발생합니다.
해결: 실제로 구현한 연산에 맞는 최소 카테고리만 선언합니다.
// ❌ operator+ 미구현인데 random_access 선언 → std::sort에서 크래시
using iterator_category = std::random_access_iterator_tag;
// ✅ 구현한 수준만 선언
using iterator_category = std::forward_iterator_tag;
에러 10: 링 버퍼·순환 구조에서 operator-() 오류
문제 시나리오: RingBufferIterator의 operator-(other)를 index_ - other.index_로만 구현했는데, wrap-around 시 거리가 잘못 계산됩니다.
해결: 링 버퍼는 random_access 대신 bidirectional로 선언하는 것이 안전합니다. operator-가 순환을 정확히 반영하기 어렵기 때문입니다.
7. 모범 사례 (Best Practices)
1. 최소한의 iterator_category로 구현
필요한 수준만 구현하면 유지보수가 쉬워집니다.
// sort가 필요 없으면 forward_iterator만
using iterator_category = std::forward_iterator_tag;
// reverse가 필요하면 bidirectional_iterator
using iterator_category = std::bidirectional_iterator_tag;
// sort, binary_search가 필요하면 random_access_iterator
using iterator_category = std::random_access_iterator_tag;
2. iterator_concept (C++20)
C++20에서는 iterator_concept가 iterator_category보다 우선합니다.
template <typename T>
class SliceIterator {
public:
using iterator_concept = std::random_access_iterator_tag;
using iterator_category = std::random_access_iterator_tag;
// ...
};
3. default 생성자 제공
많은 STL 알고리즘은 반복자를 기본 생성한 뒤 대입합니다.
MyIterator() = default;
MyIterator(T* p) : ptr_(p) {}
4. noexcept 지정
이동·복사가 예외를 던지지 않으면 noexcept를 붙입니다.
MyIterator(const MyIterator&) noexcept = default;
MyIterator(MyIterator&&) noexcept = default;
MyIterator& operator=(const MyIterator&) noexcept = default;
MyIterator& operator=(MyIterator&&) noexcept = default;
5. static_assert로 검증
static_assert(std::forward_iterator<ForwardListIterator<int>>);
static_assert(std::random_access_iterator<SliceIterator<int>>);
6. const_iterator와 iterator 분리
표준 컨테이너처럼 const 컨테이너에서는 const_iterator를 반환해 *it 수정을 막습니다.
template <typename T>
class MyContainer {
public:
using iterator = MyIterator<T>;
using const_iterator = MyConstIterator<T>;
iterator begin() { return iterator(data_); }
const_iterator begin() const { return const_iterator(data_); }
const_iterator cbegin() const { return begin(); }
// end(), cend() 동일
};
7. 반복자 무효화 규칙 문서화
컨테이너 수정 시 어떤 반복자가 무효화되는지 명확히 문서화합니다.
/**
* RingBuffer 반복자 무효화 규칙:
* - push_back: end() 반복자만 무효화, begin()~end()-1 유효
* - clear: 모든 반복자 무효화
* - 다른 스레드에서 동시 수정: UB
*/
8. C++20 iterator_concept 우선 사용
C++20에서는 iterator_concept가 iterator_category보다 우선합니다. contiguous_iterator 등 새 개념 지원 시 유용합니다.
template <typename T>
class SliceIterator {
public:
using iterator_concept = std::random_access_iterator_tag;
using iterator_category = std::random_access_iterator_tag;
// iterator_concept가 있으면 알고리즘은 이를 우선 사용
};
9. 반복자 수명과 기반 컨테이너 수명
반복자는 기반 컨테이너(또는 스트림)보다 오래 살면 안 됩니다. span, string_view처럼 “참조” 성격의 range는 수명 관리가 중요합니다.
// ❌ 위험: buf가 소멸한 후 it 사용
auto get_iter() {
RingBuffer<int> buf(10);
return buf.begin(); // buf 소멸 → it 무효화
}
// ✅ 안전: 컨테이너와 반복자 수명 일치
void process(RingBuffer<int>& buf) {
for (auto it = buf.begin(); it != buf.end(); ++it) { }
}
10. iterator_traits vs C++20 concepts
C++20에서는 std::input_iterator<It> 등 concepts로 직접 검증합니다. iterator_traits와 함께 사용하면 이중 검증으로 안전합니다.
// C++20: concepts로 컴파일 타임 검증
template <std::random_access_iterator It>
void my_sort(It first, It last) {
std::sort(first, last);
}
// static_assert로 구현 검증
static_assert(std::random_access_iterator<SliceIterator<int>>);
8. 프로덕션 패턴
패턴 1: 기존 컨테이너 래퍼
데이터를 복사하지 않고 기존 컨테이너의 반복자만 넘깁니다. begin()/end()가 container_->begin()/end()를 반환하는 래퍼 클래스로, std::ranges::sort(w) 시 원본 v가 정렬됩니다.
패턴 2: 스트라이드 반복자 (N개마다 하나)
operator++에서 stride_만큼 current_를 증가시키는 forward iterator. operator*는 *current_ 반환, operator==는 current_ 비교.
패턴 3: 인덱스 반복자 (간접 접근)
operator*에서 (*container_)[index_] 반환. operator++에서 index_ 증가. 컨테이너와 인덱스를 함께 보관하는 forward iterator.
패턴 4: C++20 Ranges와 통합
begin()/end()를 제공하면 std::ranges::range<SliceRange<int>>를 만족합니다. SliceIterator가 random_access_iterator이면 std::ranges::random_access_range도 만족합니다. static_assert로 검증하세요.
패턴 5: 필터·변환 반복자
FilterIterator: operator++에서 pred_(*current_)가 참일 때까지 ++current_. operator*는 *current_ 반환.
TransformIterator: operator*에서 f_(*it_) 반환. operator++에서 ++it_. 기반 반복자를 감싸 값만 변환합니다.
패턴 6: 프로덕션 링 버퍼 (스레드 안전 스냅샷)
멀티스레드 환경에서는 반복자 순회 중 데이터가 변경될 수 있으므로, 스냅샷을 반환해 순회합니다. snapshot()이 std::vector<T>를 복사해 반환하고, for (auto x : buf.snapshot()) 형태로 사용합니다. std::mutex로 push_back과 snapshot을 보호합니다.
참고: C++20 std::ranges::filter_view, transform_view가 동일 역할을 합니다.
패턴 7: 데이터베이스 커서 반복자 (Lazy Input Iterator)
쿼리 결과를 한 행씩 읽어 std::accumulate나 std::find_if에 넘기는 패턴입니다. Input iterator로 구현하며, operator* 호출 시 실제 DB fetch가 발생합니다. value_type = Row, iterator_category = input_iterator_tag, operator*에서 fetch_row() 호출.
패턴 8: 파이프라인 조합 (Filter + Transform)
여러 반복자를 조합해 복잡한 순회를 만듭니다. 프로덕션에서는 C++20 views::filter | views::transform을 우선 사용하고, 레거시 환경에서만 커스텀으로 구현합니다.
9. 구현 체크리스트
Input Iterator
-
operator*(읽기 전용,const참조) -
operator++(prefix, postfix) -
operator==,operator!= -
iterator_traits특수화 -
iterator_category = std::input_iterator_tag - 다중 패스 불가 주의 (한 번만 순회)
Forward Iterator
-
operator*,operator->(선택) -
operator++(prefix, postfix) -
operator==,operator!= -
iterator_traits특수화 -
iterator_category = std::forward_iterator_tag
Bidirectional Iterator
- Forward Iterator 모든 항목
-
operator--(prefix, postfix) -
iterator_category = std::bidirectional_iterator_tag
Random Access Iterator
- Bidirectional Iterator 모든 항목
-
operator+,operator-,operator+=,operator-= -
operator[] -
operator<,>,<=,>= -
iterator_category = std::random_access_iterator_tag
컨테이너
-
begin(),end()(const 오버로드 포함) -
end()가 “끝 다음”을 가리킴 - 순회 중 수정 시 무효화 규칙 문서화
참고 자료
- cppreference - Iterator requirements
- cppreference - iterator_traits
- C++20 Ranges - iterator concepts
이전 글: C++17 CMake 고급
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 왜 커스텀 반복자가 필요한지, 문제 시나리오부터 Forward·Bidirectional·Random Access 반복자 완전 구현, 흔한 실수, 모범 사례, 프로덕션 패턴까지. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
다음 글: C++20 Ranges 기초
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]
- C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
이 글에서 다루는 키워드 (관련 검색어)
C++, 반복자, iterator, input_iterator, output_iterator, forward_iterator, bidirectional_iterator, random_access_iterator, iterator_traits, STL 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ 반복자 기초 완벽 가이드 | iterator 카테고리·begin/end·역방향 반복자·실전 패턴
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
- C++ 패키지 매니저 | vcpkg·Conan으로
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기