C++ Iterator 카테고리 이해하기: begin/end부터 성능 비교까지

들어가며: 순회 중 erase했는데 프로그램이 죽습니다

반복자(iterator)는 STL 컨테이너의 원소를 순회·접근하는 객체입니다. 포인터처럼 *it로 역참조, ++it로 다음 원소로 이동, it != end()로 비교할 수 있습니다. STL 알고리즘은 모두 [begin, end) 반복자 범위를 받아 동작하므로, 반복자를 이해해야 STL을 제대로 활용할 수 있습니다. end()는 마지막 원소의 다음 위치를 가리키므로, 범위는 끝을 포함하지 않는 [begin, end)로 표현됩니다. 문제의 코드:

// ❌ 나쁜 예: erase 후 반복자 무효화
std::vector<int> vec = {1, 2, 0, 3, 0, 4};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    if (*it == 0) vec.erase(it);  // it 무효화 → 다음 ++it에서 미정의 동작!
}

vec.erase(it)는 it가 가리키던 원소를 제거하며, 지운 위치와 그 뒤를 가리키던 반복자를 모두 무효화합니다. ++it를 하면 이미 무효화된 반복자를 사용해 미정의 동작이 됩니다. 해결법:

// ✅ 올바른 사용: erase가 반환하는 새 반복자 사용
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 0) it = vec.erase(it);
    else ++it;
}
// ✅ 또는 erase-remove idiom
vec.erase(std::remove(vec.begin(), vec.end(), 0), vec.end());

erase 후 크래시, end() 비교 누락 같은 반복자 문제 사례

시나리오 1: erase 루프에서 반복자 무효화

증상: 0을 제거하는 루프에서 vec.erase(it) 후 ++it 시 크래시. 원인: erase는 해당 원소를 제거하며, 그 위치 이후의 반복자들이 무효화됩니다. erase는 삭제된 원소의 다음을 가리키는 반복자를 반환하므로, it = vec.erase(it)로 받아야 합니다. 해결: it = vec.erase(it) 또는 erase-remove idiom 사용.

시나리오 2: find 반환값을 end()와 비교하지 않음

증상: *std::find(...)로 역참조 시 크래시. 원인: find는 값을 찾지 못하면 end()를 반환합니다. end()는 “마지막 원소 다음”을 가리키므로 역참조하면 미정의 동작입니다. 해결: auto it = std::find(...); if (it != vec.end()) { /* *it 사용 */ }

시나리오 3: 범위 기반 for 안에서 컨테이너 수정

증상: for (auto& x : vec) 루프 안에서 vec.push_back() 호출 시 크래시. 원인: 범위 기반 for는 시작할 때 begin/end를 한 번 받아 두고 그 반복자로 순회합니다. 순회 중 push_back이 재할당을 일으키면 기존 반복자가 모두 무효화되고, 재할당이 없더라도 미리 받아 둔 end가 새 끝과 달라집니다. 해결: 순회 중에는 컨테이너를 수정하지 않거나, 인덱스 기반 루프 또는 erase-remove idiom 사용.

시나리오 4: 역방향 반복자에서 base() 잘못 사용

증상: reverse_iterator를 erase에 넘기려 할 때 잘못된 원소가 삭제됩니다. 원인: reverse_iterator::base()는 “역방향으로 보던 마지막 원소의 다음”을 가리킵니다. rit.base()와 rit가 가리키는 논리적 위치가 다릅니다. 해결: vec.erase(std::next(rit).base()) 또는 vec.erase((++rit).base())로 삭제할 원소에 맞는 반복자 사용.

시나리오 5: const 반복자 vs 비const 반복자 혼용

증상: cbegin()/cend()로 얻은 반복자로 *it = 42 시 컴파일 에러. 원인: cbegin()/cend()는 const 반복자를 반환합니다. const 반복자로는 원소를 수정할 수 없습니다. 해결: 읽기 전용이면 cbegin/cend, 수정이 필요하면 begin/end 사용.

시나리오 6: 빈 컨테이너에서 begin == end

증상: 빈 vector에서 *vec.begin() 역참조 시 크래시. 원인: 빈 컨테이너에서는 begin() == end()입니다. begin()을 역참조하면 미정의 동작입니다. 해결: if (!vec.empty()) 또는 it != vec.end() 검사 후 역참조.

시나리오 7: std::distance에 역순 범위

증상: list 반복자로 std::distance(last, first)를 호출하면 끝없이 돌거나 크래시합니다. 원인: Random Access가 아닌 반복자에서 distance는 first에서 ++를 반복해 last에 닿을 때까지 셉니다. last가 앞에 있으면 닿지 못하므로 미정의 동작입니다. Random Access 반복자는 last - first로 계산하므로 음수를 반환합니다. 해결: std::distance(vec.begin(), it)처럼 항상 순방향 범위를 넘깁니다.

시나리오 8: back_inserter 없이 빈 벡터에 copy

증상: std::copy(src.begin(), src.end(), dst.begin())에서 dst 비어 있으면 크래시. 해결: std::back_inserter(dst) 사용 또는 dst.resize(src.size()) 후 복사.

시나리오 9: map/set 순회 중 erase

증상: map/set 순회 중 erase(it) 후 ++it 시 크래시. 원인: erase(it)는 반복자를 무효화합니다. it = m.erase(it)로 반환값을 받아야 합니다. 해결: it = m.erase(it) 사용. C++20에서는 std::erase_if(m, pred) 활용.


input부터 random access까지 반복자 카테고리

C++ 표준은 반복자 카테고리를 계층적으로 정의합니다. 각 카테고리는 지원하는 연산이 다르며, 알고리즘은 “필요한 최소 카테고리”만 요구합니다.

카테고리 계층 구조

flowchart TD
    Input["Input: 읽기, 전진, 1회 순회"]
    Output["Output: 쓰기, 전진, 1회 순회"]
    Forward["Forward: 전진, 다회 순회"]
    Bidirectional["Bidirectional: 전진·후진"]
    RandomAccess["Random Access: 임의 위치 이동"]
    Input --> Forward
    Output --> Forward
    Forward --> Bidirectional
    Bidirectional --> RandomAccess

위 다이어그램 설명: Input/Output은 1회 스캔만 가능하며, Forward는 다회 순회, Bidirectional은 -- 지원, Random Access는 it + n, it[n] 지원합니다.

카테고리별 지원 연산

카테고리지원 연산예시 컨테이너
Input*it, ++it, it == it2istream_iterator
Output*it = x, ++itostream_iterator, back_inserter
ForwardInput + 다회 순회forward_list, unordered_*
BidirectionalForward + --itlist, map, set
Random AccessBidirectional + it + n, it[n], <vector, deque, array

C++20에는 그 위에 Contiguous 카테고리(std::contiguous_iterator)가 추가되었습니다. 원소가 메모리에 연속으로 놓여 있다는 보장까지 주는 반복자로, vector, array, string의 반복자가 여기에 해당합니다. 다만 하위 호환 때문에 이들의 iterator_traits<It>::iterator_category는 여전히 random_access_iterator_tag이고, 연속성은 C++20 콘셉트나 iterator_concept로 확인합니다.

카테고리 확인 예제

#include <iterator>
#include <vector>
#include <list>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3};
    std::list<int> lst = {1, 2, 3};
    // vector: Random Access
    auto v_it = vec.begin();
    v_it += 2;           // OK
    int x = v_it[0];    // OK
    std::cout << "vector: " << x << "\n";
    // list: Bidirectional (Random Access 아님)
    auto l_it = lst.begin();
    ++l_it; ++l_it;     // OK
    // l_it += 2;       // 에러: list 반복자는 += 미지원
    std::cout << "list: " << *l_it << "\n";
}

vector는 연속 메모리이므로 it + n, it[n]이 O(1)입니다. list는 노드 기반이므로 +=가 없으며, ++/--만 O(1)입니다. std::sort는 Random Access를 요구하므로 list에는 sort 멤버 함수를 사용해야 합니다.

iterator_traits로 카테고리 확인

#include <iterator>
#include <vector>
#include <list>
#include <type_traits>
int main() {
    using VIt = std::vector<int>::iterator;
    using LIt = std::list<int>::iterator;
    // iterator_category: Random Access vs Bidirectional
    static_assert(std::is_same_v<
        std::iterator_traits<VIt>::iterator_category,
        std::random_access_iterator_tag
    >);
    static_assert(std::is_same_v<
        std::iterator_traits<LIt>::iterator_category,
        std::bidirectional_iterator_tag
    >);
}

std::iterator_traits<It>::iterator_category로 해당 반복자의 카테고리를 컴파일 타임에 확인할 수 있습니다. 알고리즘 오버로딩이나 SFINAE에 활용됩니다.


begin과 end: 반개구간, cbegin, std::begin

반개구간 [begin, end)

STL의 모든 범위는 반개구간(half-open range) [begin, end)를 사용합니다. begin은 첫 원소를, end는 마지막 원소의 다음을 가리킵니다. end는 역참조하면 안 됩니다.

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {10, 20, 30};
    // [begin, end) 시각화
    // vec:  [10] [20] [30]
    //        ^         ^
    //      begin     end (역참조 금지)
    auto it = vec.begin();
    std::cout << *it << "\n";  // 10
    ++it;
    std::cout << *it << "\n";  // 20
    ++it;
    std::cout << *it << "\n";  // 30
    ++it;
    // it == vec.end() → true
    // *it;  // ❌ 미정의 동작
}

[begin, end)는 “begin 포함, end 미포함”입니다. 원소 개수는 std::distance(begin, end) 또는 vec.size()와 같습니다. 빈 범위면 begin == end입니다.

begin() / end() vs std::begin() / std::end()

#include <vector>
#include <array>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3};
    std::array<int, 3> arr = {4, 5, 6};
    int c_arr[] = {7, 8, 9};
    // 멤버 함수 (컨테이너)
    auto v_b = vec.begin();
    auto v_e = vec.end();
    // std::begin/end (C 배열과 C++ 컨테이너 모두)
    auto a_b = std::begin(arr);
    auto a_e = std::end(arr);
    auto c_b = std::begin(c_arr);
    auto c_e = std::end(c_arr);
    std::cout << *v_b << " " << *a_b << " " << *c_b << "\n";  // 1 4 7
}

std::begin/std::end는 C 배열과 C++ 컨테이너 모두에 동작합니다. 제네릭 코드에서는 std::begin(r)/std::end(r)를 사용하면 배열·벡터·커스텀 타입을 모두 지원할 수 있습니다.

cbegin() / cend() — 읽기 전용

#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3};
    auto it = vec.cbegin();
    int x = *it;   // OK: 읽기
    // *it = 42;   // 에러: const 반복자로 수정 불가
    auto it2 = vec.begin();
    *it2 = 42;     // OK: 비const 반복자
}

cbegin/cend는 const_iterator를 반환합니다. 원소를 수정하지 않는 읽기 전용 순회에 사용하면, 실수로 수정하는 것을 방지할 수 있습니다.

빈 컨테이너 처리

#include <vector>
#include <algorithm>
void process(const std::vector<int>& vec) {
    if (vec.empty()) return;  // 또는
    if (vec.begin() == vec.end()) return;
    auto it = std::find(vec.begin(), vec.end(), 42);
    if (it != vec.end()) {
        // *it 사용
    }
}

빈 컨테이너에서는 begin() == end()이므로, find 등은 end()를 반환합니다. 역참조 전에 항상 it != end()를 확인하세요.

범위 기반 for와 begin/end

#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3};
    // 범위 기반 for는 내부적으로 begin/end 사용
    for (auto& x : vec) {
        x *= 2;  // 수정 가능 (auto&)
    }
    // const 순회
    for (const auto& x : vec) {
        // x 수정 불가
    }
}

for (auto& x : range)는 range에 멤버 begin/end가 있으면 그것을, 없으면 인자 의존 탐색(ADL)으로 찾은 begin(range)/end(range)를 호출해 순회합니다. C 배열은 배열 크기로 끝을 계산합니다.


rbegin, rend와 base()의 한 칸 차이

rbegin() / rend() 기본 사용

역방향 반복자는 끝에서 처음으로 순회합니다. rbegin()은 마지막 원소를, rend()는 첫 원소의 앞을 가리킵니다.

#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // 역순 출력: 5 4 3 2 1
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << "\n";
    // std::reverse_copy와 동일한 효과
    std::vector<int> reversed(vec.rbegin(), vec.rend());
    // reversed: {5, 4, 3, 2, 1}
}

++rit는 역방향으로 한 칸 이동합니다(즉, 논리적으로 앞으로). rbegin()은 end()의 앞쪽, rend()는 begin()의 앞에 대응됩니다.

reverse_iterator와 base()

reverse_iterator::base()는 “역방향으로 보던 마지막 원소의 다음”을 가리키는 일반 반복자를 반환합니다. 따라서 rit가 가리키는 원소를 지우려면 (std::next(rit)).base()를 사용해야 합니다.

#include <algorithm>
#include <vector>
#include <iterator>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int target = 3;
    auto rit = std::find(vec.rbegin(), vec.rend(), target);
    if (rit != vec.rend()) {
        // rit가 가리키는 3을 지우려면: base()의 "앞" 위치
        // rit.base() = 3 다음 (4를 가리킴)
        // (++rit).base() = 3을 가리킴
        vec.erase((++rit).base());
        // 또는 vec.erase(std::next(rit).base());
    }
    // vec: {1, 2, 4, 5}
}

reverse_iterator가 가리키는 논리적 위치와 base()가 가리키는 위치는 한 칸 어긋나 있습니다. rit가 가리키는 원소를 지우려면 (++rit).base()로 erase에 넘깁니다.

역방향 순회 다이어그램

flowchart LR
    subgraph forward["정방향 (begin, end)"]
        B[begin] --> E[end]
    end
    subgraph reverse["역방향 (rbegin, rend)"]
        RB[rbegin] --> RE[rend]
    end
    B -.->|대응| RE
    E -.->|대응| RB

위 다이어그램 설명: rbegin()은 end()-1의 논리적 위치(마지막 원소)를, rend()는 begin()의 앞(첫 원소 앞)을 가리킵니다. ++rit는 정방향으로 보면 --에 해당합니다.

crbegin() / crend() — const 역방향

#include <iostream>
#include <vector>
void print(const std::vector<int>& vec) {
    for (auto rit = vec.crbegin(); rit != vec.crend(); ++rit) {
        std::cout << *rit << " ";  // 읽기만
        // *rit = 0;  // 에러
    }
}

crbegin/crend는 const 역방향 반복자를 반환합니다. 읽기 전용 역순 순회에 사용합니다.


back_inserter, ostream_iterator 같은 반복자 어댑터

반복자 어댑터는 다른 반복자나 컨테이너를 감싸서 새로운 동작을 제공합니다. STL 알고리즘에 “출력 대상”을 지정할 때 자주 사용합니다.

back_inserter — 벡터 끝에 추가

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;
    // std::copy가 dst.end()에 쓰려 하면 UB
    // back_inserter는 push_back을 호출해 안전하게 추가
    std::copy(src.begin(), src.end(), std::back_inserter(dst));
    for (int x : dst) std::cout << x << " ";  // 1 2 3 4 5
}

std::back_inserter(dst)는 std::back_insert_iterator를 반환합니다. *it = value 시 dst.push_back(value)를 호출합니다. dst 크기를 미리 할당할 필요가 없습니다.

front_inserter — 리스트 앞에 삽입

#include <list>
#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> src = {1, 2, 3};
std::list<int> lst;
std::copy(src.begin(), src.end(), std::front_inserter(lst));
// lst: {3, 2, 1} — 역순 삽입

front_inserter는 push_front를 사용합니다. list, deque만 지원. vector는 사용 불가.

insert_iterator — 특정 위치에 삽입

#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> src = {10, 20, 30};
std::vector<int> dst = {1, 2, 3};
auto it = std::find(dst.begin(), dst.end(), 2);
std::copy(src.begin(), src.end(), std::inserter(dst, it));
// dst: {1, 10, 20, 30, 2, 3}

std::inserter(container, pos)는 pos 위치에 insert를 호출합니다.

ostream_iterator — 스트림 출력

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::copy(vec.begin(), vec.end(),
              std::ostream_iterator<int>(std::cout, "\n"));
    // 출력: 1\n2\n3\n4\n5
}

ostream_iterator<T>(stream, delim)는 *it = value 시 stream << value << delim를 호출합니다. 알고리즘 결과를 바로 출력할 때 유용합니다.

istream_iterator — 스트림 입력

#include <iostream>
#include <iterator>
#include <vector>
std::vector<int> vec{std::istream_iterator<int>(std::cin),
                     std::istream_iterator<int>()};  // EOF까지

기본 생성한 istream_iterator<T>()는 스트림 끝을 나타내므로, cin에서 EOF(또는 읽기 실패)까지 읽어 vector를 채웁니다. 여기서 소괄호 vec(...)를 쓰면 컴파일러가 이 줄을 함수 선언으로 해석하는 “most vexing parse”에 걸리므로 중괄호로 초기화했습니다.

어댑터용도지원 컨테이너
back_inserter끝에 추가vector, deque, list, string
front_inserter앞에 삽입list, deque
inserter특정 위치 삽입대부분 STL 컨테이너
ostream_iterator스트림 출력cout, ofstream
istream_iterator스트림 입력cin, ifstream

std::distance와 std::advance

반복자 이동과 거리 계산에 std::advance와 std::distance를 사용하면 컨테이너 종류에 관계없이 동작하는 제네릭 코드를 작성할 수 있습니다.

std::advance — 반복자 n칸 이동

#include <iterator>
#include <vector>
#include <list>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {1, 2, 3, 4, 5};
auto v_it = vec.begin();
std::advance(v_it, 3);  // vector: O(1)
auto l_it = lst.begin();
std::advance(l_it, 3);  // list: O(n), ++ 루프
std::advance(l_it, -2);  // 음수: -- 로 역방향

std::advance(it, n)은 it를 n칸 이동합니다. Random Access면 O(1), Bidirectional면 O(n)입니다.

std::distance — 두 반복자 사이 거리

#include <iterator>
#include <vector>
#include <algorithm>
std::vector<int> vec = {10, 20, 30, 40, 50};
auto dist = std::distance(vec.begin(), vec.end());  // 5
auto it = std::find(vec.begin(), vec.end(), 40);
if (it != vec.end()) {
    size_t idx = std::distance(vec.begin(), it);  // 3
}

std::distance(first, last)는 [first, last) 원소 개수를 반환합니다. Random Access면 O(1), 그 외 O(n)입니다. first가 last보다 뒤에 있으면 Random Access 반복자는 음수를 반환하고, 그 외 반복자는 미정의 동작입니다.

std::next와 std::prev

#include <iterator>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // advance: it 자체 수정 (void)
    auto it1 = vec.begin();
    std::advance(it1, 2);
    // next: 원본 유지, 새 반복자 반환
    auto it2 = std::next(vec.begin(), 2);
    // prev: 역방향 next
    auto it3 = std::prev(vec.end(), 1);  // 마지막 원소
}

std::next(it, n)은 it를 수정하지 않고 n칸 이동한 복사본을 반환합니다. std::prev(it, n)은 역방향 이동입니다.


순회, 조건부 삭제, 부분 범위 처리 예제

예제 1: begin/end로 모든 컨테이너 순회

#include <vector>
#include <list>
#include <array>
#include <iostream>
template<typename Container>
void print(const Container& c) {
    for (auto it = std::begin(c); it != std::end(c); ++it)
        std::cout << *it << " ";
    std::cout << "\n";
}
int main() {
    std::vector<int> vec = {1, 2, 3};
    std::list<int> lst = {4, 5, 6};
    int c_arr[] = {10, 11, 12};
    print(vec);
    print(lst);
    print(c_arr);
}

std::begin/std::end로 템플릿 하나로 모든 컨테이너와 C 배열을 처리할 수 있습니다.

예제 2: erase 루프 (조건부 삭제)

#include <vector>
std::vector<int> vec = {1, 0, 2, 0, 3, 0, 4};
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 0) it = vec.erase(it);
    else ++it;
}
// vec: {1, 2, 3, 4}

erase는 삭제된 원소의 다음 반복자를 반환합니다. it = vec.erase(it)로 받아야 합니다.

예제 3: 역방향으로 특정 값 찾아 삭제

#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
auto rit = std::find(vec.rbegin(), vec.rend(), 2);
if (rit != vec.rend())
    vec.erase((++rit).base());  // 마지막 2 제거

find에 rbegin/rend를 넘기면 역방향으로 첫 번째(마지막에 있는) target을 반환합니다. (++rit).base()로 해당 원소를 가리키는 반복자를 얻습니다.

예제 4: iterator_traits 활용

#include <iterator>
#include <vector>
#include <list>
template<typename Iterator>
void advance_if_random_access(Iterator& it, int n) {
    if constexpr (std::is_same_v<
            typename std::iterator_traits<Iterator>::iterator_category,
            std::random_access_iterator_tag>)
        it += n;
    else {
        if (n >= 0) while (n--) ++it;
        else while (n++) --it;
    }
}
// vector: O(1), list: O(n)

iterator_traits로 카테고리를 확인해 Random Access면 +=, 아니면 ++/-- 루프를 사용합니다. 표준 라이브러리의 std::advance가 내부적으로 하는 일이 바로 이것이므로, 실제 코드에서는 직접 만들지 말고 std::advance나 std::next를 쓰면 됩니다.

예제 5: 부분 범위 처리

#include <vector>
#include <algorithm>
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
std::sort(vec.begin(), vec.begin() + 3);  // 앞 3개만
auto it = std::find(vec.begin() + 2, vec.begin() + 5, 9);
if (it != vec.begin() + 5) {
    auto idx = std::distance(vec.begin(), it);  // 인덱스
}

vec.begin() + n으로 부분 범위 지정. Random Access에서만 + 지원. std::distance로 인덱스 계산.

예제 6: const 반복자로 읽기 전용 API

#include <vector>
#include <algorithm>
bool contains(const std::vector<int>& vec, int value) {
    return std::find(vec.cbegin(), vec.cend(), value) != vec.cend();
}
int sum(const std::vector<int>& vec) {
    int s = 0;
    for (auto it = vec.cbegin(); it != vec.cend(); ++it) s += *it;
    return s;
}

const 참조 함수에서는 cbegin/cend로 수정 불가를 명시합니다.


Random Access 반복자 최소 구현

자체 컨테이너나 특수한 순회 로직이 필요할 때 커스텀 반복자를 구현할 수 있습니다. iterator_traits를 특수화하거나 std::iterator_traits가 자동 추론할 수 있도록 필요한 타입을 정의합니다.

최소 요구사항 (Random Access Iterator)

#include <cstddef>
#include <iterator>
#include <algorithm>
#include <iostream>
template<typename T, std::size_t N>
class FixedArray {
public:
    T data[N];
    class Iterator {
    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&;
        Iterator() = default;
        explicit Iterator(T* ptr) : ptr_(ptr) {}
        reference operator*() const { return *ptr_; }
        pointer operator->() const { return ptr_; }
        reference operator[](difference_type n) const { return ptr_[n]; }
        Iterator& operator++() { ++ptr_; return *this; }
        Iterator operator++(int) { Iterator t = *this; ++ptr_; return t; }
        Iterator& operator--() { --ptr_; return *this; }
        Iterator operator--(int) { Iterator t = *this; --ptr_; return t; }
        Iterator& operator+=(difference_type n) { ptr_ += n; return *this; }
        Iterator& operator-=(difference_type n) { ptr_ -= n; return *this; }
        Iterator operator+(difference_type n) const { return Iterator(ptr_ + n); }
        friend Iterator operator+(difference_type n, Iterator it) { return it + n; }
        Iterator operator-(difference_type n) const { return Iterator(ptr_ - n); }
        difference_type operator-(const Iterator& other) const { return ptr_ - other.ptr_; }
        bool operator==(const Iterator& other) const { return ptr_ == other.ptr_; }
        bool operator!=(const Iterator& other) const { return ptr_ != other.ptr_; }
        bool operator<(const Iterator& other) const { return ptr_ < other.ptr_; }
        bool operator>(const Iterator& other) const { return ptr_ > other.ptr_; }
        bool operator<=(const Iterator& other) const { return ptr_ <= other.ptr_; }
        bool operator>=(const Iterator& other) const { return ptr_ >= other.ptr_; }
    private:
        T* ptr_ = nullptr;
    };
    Iterator begin() { return Iterator(data); }
    Iterator end() { return Iterator(data + N); }
};
int main() {
    FixedArray<int, 5> arr = {{1, 2, 3, 4, 5}};
    std::sort(arr.begin(), arr.end());
    auto it = std::find(arr.begin(), arr.end(), 3);
    if (it != arr.end())
        std::cout << "Index: " << std::distance(arr.begin(), it) << "\n";
}

iterator_category, value_type, difference_type, pointer, reference를 정의하면 std::iterator_traits가 이를 그대로 읽어 갑니다. Random Access 반복자라고 선언했다면 표에 나온 연산(후위 증감, -=, n + it, [], 네 가지 대소 비교)을 모두 제공해야 합니다. 일부만 구현해도 특정 컴파일러의 std::sort가 우연히 컴파일될 수는 있지만, 다른 알고리즘이나 다른 표준 라이브러리에서는 깨집니다. 더 자세한 구현은 나만의 Iterator 만들기에서 다룹니다.

범위 기반 for 지원

// begin/end만 정의하면 범위 기반 for 자동 지원
for (int x : arr) {
    std::cout << x << " ";
}

begin()과 end()를 제공하면 for (auto x : range)가 자동으로 동작합니다. std::begin/std::end도 이 멤버를 호출합니다.


참고 자료


이전 글: C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
다음 글: C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기


같이 보면 좋은 글