본문으로 건너뛰기
Previous
Next
C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional

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

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

이 글의 핵심

C++ 커스텀 반복자 : Forward·Bidirectional. 실무에서 겪은 문제·반복자 요구 사항.

💡 초보자를 위한 한 줄: 커스텀 타입에 begin()/end()만 제대로 있어도 범위 for·많은 알고리즘이 열립니다. 그다음 카테고리 태그(iterator_category)를 거짓말하지 않게 맞추는 게 핵심입니다. C++20이면 static_assert로 concepts 검증을 붙이면 안전합니다. 17-1 반복자 기초를 본 뒤 읽으면 연결이 잘 됩니다.

들어가며: “우리 컨테이너에 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를 올바르게 설정할 수 있습니다.
  • 흔한 실수와 해결법을 알 수 있습니다.
  • 프로덕션에서 사용하는 패턴을 익힐 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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을 우선 사용하고, 레거시 환경에서만 커스텀으로 구현합니다.

정리

단계할 일
요구사항cppreference Iterator requirements에 맞추기
최소 구현operator*·++·==/!= (+ 필요 시 ->)
카테고리iterator_category실제 연산과 일치시키기
검증static_assert / C++20 iterator concepts

핵심 원칙:

  1. Input은 다중 패스·되감기 기대 금지
  2. Forward 이상은 동일 값에 반복 * 허용
  3. 컨테이너는 end()가 “끝 다음”임을 문서화

초보자를 위한 체크리스트

  • iterator_traits(또는 C++20 iterator_concept)가 실제 동작과 맞는가?
  • const/const_iterator 오버로드를 대칭으로 제공했는가?
  • 무효화 규칙(삽입·삭제 시)을 주석·문서에 적었는가?

💡 초보자 팁: 아래 9. 구현 체크리스트·1. 반복자 요구 사항·6. 자주 발생하는 에러를 함께 보세요.

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()가 “끝 다음”을 가리킴
  • 순회 중 수정 시 무효화 규칙 문서화

참고 자료



자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 왜 커스텀 반복자가 필요한지, 문제 시나리오부터 Forward·Bidirectional·Random Access 반복자 완전 구현, 흔한 실수, 모범 사례, 프로덕션 패턴까지. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 다음 글: C++ Ranges 완벽 가이드

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

이 글에서 다루는 키워드 (관련 검색어)

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분 만에 찾기

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.