C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional

C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional

이 글의 핵심

C++ 커스텀 반복자 완벽 가이드에 대한 실전 가이드입니다. Forward·Bidirectional 등을 예제와 함께 상세히 설명합니다.

들어가며: “우리 컨테이너에 std::sort를 쓸 수 없어요”

문제 시나리오

도메인 특화 컨테이너를 만들었는데, for (auto x : myContainer)std::sort(v.begin(), v.end())에 그대로 넣을 수 없어 답답했던 경험이 있으신가요? 표준 vector, listbegin()/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 없이 복사해야 함
  • 연결 리스트: 자체 LinkedListstd::find_if로 검색하고 싶은데 반복자 없음
  • 파일 스트림: istream을 라인 단위로 ranges::for_each에 넘기고 싶은데 input_iterator가 없음
  • 네트워크 패킷: 수신 버퍼를 청크 단위로 순회해 파싱하고 싶은데 반복자 인터페이스 부재
  • 데이터베이스 커서: 쿼리 결과를 한 행씩 std::accumulate에 넘기고 싶은데 반복자 미지원

원인: RingBufferbegin()/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_iteratorinput + 다중 패스singly-linked list
bidirectional_iteratorforward + --list, map
random_access_iteratorbidirectional + +, -, [], <vector, array, SliceIterator

이 글을 읽으면:

  • Forward·Bidirectional·Random Access 반복자를 완전히 구현할 수 있습니다.
  • iterator_traitsiterator_category를 올바르게 설정할 수 있습니다.
  • 흔한 실수와 해결법을 알 수 있습니다.
  • 프로덕션에서 사용하는 패턴을 익힐 수 있습니다.

목차

  1. 반복자 요구 사항
  2. Input Iterator 완전 구현
  3. Forward Iterator 완전 구현
  4. Bidirectional Iterator 완전 구현
  5. Random Access Iterator 완전 구현
  6. 자주 발생하는 에러와 해결법
  7. 모범 사례 (Best Practices)
  8. 프로덕션 패턴
  9. 구현 체크리스트

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::sortrandom_access_iterator_tag만 허용하고, std::findinput_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::findstd::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_tagstd::find, std::copy 등 사용 가능
  • operator*const 참조 반환 (읽기 전용)

실행 결과:

Found: cherry

반복자 카테고리 비교표

카테고리다중 패스역방향임의 접근사용 가능 알고리즘
output_iteratorcopy (출력), fill
input_iteratorfind, 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_tagstd::find, std::for_each 등 사용 가능
  • operator->operator*의 주소 반환으로 구현 가능

실행 결과:

1 2 3
Found: 2

4. Bidirectional Iterator 완전 구현

문제 시나리오: 이중 연결 리스트

이중 연결 리스트reverse_iteratorstd::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_typeoperator-(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()) 컴파일 에러

원인: sortrandom_access_iterator만 받습니다. forward_iteratorbidirectional_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 미정의

문제 시나리오: 커스텀 LineIteratorstd::distance(begin, end)에 넘겼더니 “incomplete type” 또는 “no matching function” 에러가 발생합니다.

증상: std::distance(it1, it2) 또는 std::advance(it, n) 등에서 에러

원인: iterator_traitsvalue_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)에서 resultoutput 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-() 오류

문제 시나리오: RingBufferIteratoroperator-(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_conceptiterator_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_conceptiterator_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>>를 만족합니다. SliceIteratorrandom_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::mutexpush_backsnapshot을 보호합니다.

참고: C++20 std::ranges::filter_view, transform_view가 동일 역할을 합니다.

패턴 7: 데이터베이스 커서 반복자 (Lazy Input Iterator)

쿼리 결과를 한 행씩 읽어 std::accumulatestd::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()가 “끝 다음”을 가리킴
  • 순회 중 수정 시 무효화 규칙 문서화

참고 자료


이전 글: 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분 만에 찾기