본문으로 건너뛰기
Previous
Next
C++ 반복자 | 'Iterator' 완벽 가이드

C++ 반복자 | 'Iterator' 완벽 가이드

C++ 반복자 | 'Iterator' 완벽 가이드

이 글의 핵심

C++ 반복자(iterator) 는 컨테이너 요소를 순회하는 객체입니다. 범위 기반 for와 vector에서 begin/end로 쓰이며, size_t·ptrdiff_t로 거리·인덱스를 다룰 때와 Composite 패턴으로 트리 순회를 통일할 때도 자주 씁니다.

반복자 기본

반복자(iterator) 는 컨테이너 요소를 순회하는 객체입니다. 범위 기반 forvector에서 begin/end로 쓰이며, size_t·ptrdiff_t로 거리·인덱스를 다룰 때와 Composite 패턴으로 트리 순회를 통일할 때도 자주 씁니다.

반복자 개념도

다음은 mermaid 예제 코드입니다.

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
    }
}

반복자 종류

반복자 계층 구조

다음은 mermaid 예제 코드입니다.

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]

반복자 기능 비교표

반복자 타입읽기쓰기전진후진임의 접근예시 컨테이너
Inputistream_iterator
Outputostream_iterator
Forwardforward_list
Bidirectionallist, set, map
Random Accessvector, 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*(쓰기), ++쓰기 전용, 읽기와 섞으면 제약 많음
ForwardInput+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_iteratorconcept으로 재정리되었고, 커스텀 반복자는 연산자뿐 아니라 의미론(복사 횟수, 무효화)까지 맞춰야 합니다. 자세한 문법 요구사항은 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 차이:

다음은 mermaid 예제 코드입니다.

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_conceptstd::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로 역참조 결과를 받는 방식과, 연산자 -> 체인이 기대와 다를 수 있으니 범위 기반 forauto& 선택과 함께 읽는 것이 좋습니다. 연속 반복자가 아니면 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 이후에는 beginend가 다른 타입인 범위가 일반적입니다(널 종료 문자열, istream 뷰 등). 이때 레거시 std::sort(first, last)처럼 동일 타입을 가정한 API와 충돌하지 않도록, std::ranges:: 알고리즘을 쓰는 편이 안전합니다.

예외·무효화·noexcept

저수준 반복자 루프는 예외 안전성반복자 무효화 규칙이 합쳐져 버그가 나기 쉽습니다. vector에서 eraseerase 반환 반복자를 사용하거나, 삽입으로 재할당이 일어날 수 있으면 인덱스 기반으로 바꾸는 등, 무효화를 먼저 정리한 뒤 반복자 설계를 하는 것이 좋습니다.

“직접 반복자 클래스” 대신

내부 라이브러리가 아니라면 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++ 반복자 | ‘Iterator’ 완벽 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 반복자 | ‘Iterator’ 완벽 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  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 순서를 권장합니다.


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

C++, iterator, 반복자, STL, range 등으로 검색하시면 이 글이 도움이 됩니다.