C++ 범위 기반 for | auto·참조·임시 객체·구조화 바인딩 실전 가이드

C++ 범위 기반 for | auto·참조·임시 객체·구조화 바인딩 실전 가이드

이 글의 핵심

범위 기반 for는 컨테이너를 순회하는 표준 문법입니다. 값 복사과 참조의 차이, 임시 객체 수명, 구조화 바인딩, ADL begin/end까지 한 번에 정리합니다.

범위 기반 for란?

범위 기반 for(range-based for, C++11)는 시퀀스 전체를 순회할 때 인덱스나 반복자를 직접 쓰지 않고, “범위”에 대해 요소를 하나씩 꺼내는 문법입니다.

std::vector<int> v = {1, 2, 3};
for (int x : v) {
    std::cout << x << '\n';
}

내부적으로는 대략 **begin(v)/end(v)**로 얻은 반복자로 루프를 돌며, 각 단계에서 역참조 결과를 루프 변수에 넣습니다.

for (auto&& __range = (v); ; ) {
    auto __begin = begin(__range);
    auto __end = end(__range);
    for (; __begin != __end; ++__begin) {
        int x = *__begin;  // 선언 형태에 따라 다름
        // ...
    }
}

정확한 규칙은 표준의 “range-for 문” 절을 따릅니다. 반복문 가이드와 함께 보면 좋습니다.


auto vs auto& vs const auto&

auto (값)

요소의 복사본을 만듭니다. 수정해도 원본 컨테이너는 변하지 않습니다. 작은 int, double에는 부담이 적습니다.

for (auto x : vec) {
    x *= 2;   // vec의 요소는 안 바뀜
}

auto& (비상수 참조)

요소에 대한 별칭입니다. 수정하면 원본이 바뀝니다. const 컨테이너나 const 요소면 컴파일 오류가 날 수 있습니다.

for (auto& x : vec) {
    x *= 2;   // vec의 요소가 바뀜
}

const auto& (상수 참조)

복사 없이 읽기할 때 널리 씁니다. 임시 객체도 수명이 루프 본문으로 연장되어 안전하게 바인딩할 수 있습니다.

for (const auto& s : get_strings()) {
    std::cout << s;  // get_strings()가 임시여도 OK
}

선택 가이드

목적권장
읽기만, 큰 타입const auto&
요소 수정auto& (비const 범위)
복사본으로 가볍게 쓰기auto (작은 POD)
전달 참조·전방 시그니처auto&& (제네릭 코드에서)

auto&&: 전달 참조로, 범위의 value_type이 좌값/우값에 따라 참조 축약됩니다. 템플릿 라이브러리 코드에서 흔합니다.

for (auto&& e : container) {
    // e가 좌값 참조 또는 우값 참조로 바인딩
}

임시 객체 문제

범위 표현식이 임시인 경우

C++11 이후 규칙에 따라, 범위 표현식이 임시 객체이면 그 수명이 루프 전체로 연장됩니다. 그래서 다음은 안전합니다.

for (const auto& x : make_vector()) { /* ... */ }

주의해야 하는 것은 중첩된 임시잘못된 참조 저장이 아니라, 프록시 반복자·무효화입니다.

vector<bool>와 프록시 참조

std::vector<bool>의 특수화는 요소가 진짜 bool&가 아닐 수 있습니다. auto&로 받아 프록시를 통해 수정하는 패턴은 동작하지만, 일반화된 템플릿에서 bool&를 기대하면 깨질 수 있습니다.

무효화

순회 중에 컨테이너를 재할당·삽입으로 무효화하면 반복자가 깨집니다. 범위 for도 내부적으로 반복자를 쓰므로 동일한 주의가 필요합니다.

잘못된 패턴 (참조가 원본 범위보다 오래 살 때)

const std::string* p = nullptr;
{
    std::vector<std::string> v = {"a"};
    for (const auto& s : v) {
        p = &s;  // 루프 밖에서 p 사용 금지
    }
} // v 소멸
// *p  // 미정의 동작

임시 범위의 수명 연장은 “그 for 문 안”에서만 보장되고, 루프 밖으로 참조를 빼내는 것은 여전히 위험합니다.


구조화된 바인딩과 조합 (C++17)

구조화 바인딩을 쓰면 pair, tuple, mapvalue_type 등을 한 번에 풀어서 순회할 수 있습니다.

std::map<int, std::string> m;
for (const auto& [key, val] : m) {
    std::cout << key << ": " << val << '\n';
}

주의: std::mapauto& [k,v]로 순회하면 **값 타입이 std::pair<const Key, T>**라서 키는 수정하면 안 되는 경우가 많고, 의도와 다르면 const auto& [k,v]가 안전합니다.

std::vector<std::pair<int, int>> pairs = {{1,2},{3,4}};
for (auto [a, b] : pairs) {        // 복사
    std::cout << a << b;
}
for (auto& [a, b] : pairs) {       // 참조로 수정 가능
    ++a;
}

C 배열·구조체 멤버에도 같은 방식으로 조합할 수 있습니다.


커스텀 타입 지원: begin / end

범위 for는 ADL(실인수 의존 이름 탐색)로 **begin/end**를 찾습니다.

  • **std::begin(x) / std::end(x)**에 맞거나
  • 멤버 **x.begin()/x.end()**가 있거나
  • 네임스페이스에 begin(x)/end(x) 비멤버가 있으면 됩니다.
struct MyRange {
    int* data;
    size_t n;
    int* begin() { return data; }
    int* end() { return data + n; }
};

MyRange r = ...;
for (int x : r) { /* ... */ }

비멤버 버전 예:

struct Buffer;
const int* begin(const Buffer& b);
const int* end(const Buffer& b);

const 정확성: const 객체에 대해 const 오버로드begin/end가 필요합니다.


실전 패턴

1. 인덱스가 필요할 때

C++20 std::ranges::views::enumerate 또는 전통적으로 인덱스 for를 쓰거나, 별도 카운터를 둡니다.

size_t i = 0;
for (const auto& x : vec) {
    use(i, x);
    ++i;
}

2. initializer_list와 임시

for (int x : {1, 2, 3}) { }

3. 역순 순회

범위 for는 역방향이 아닙니다. rbegin/rend가 있으면:

for (auto it = vec.rbegin(); it != vec.rend(); ++it) { }
// 또는 C++20 ranges reverse_view

4. const 컨테이너와 수정 의도

void print(const std::vector<int>& v) {
    for (int x : v) { }           // 복사
    for (const auto& x : v) { }   // 읽기 권장
}

5. 가독성: 범위가 긴 표현식

for (const auto& item : obj.get_container().get_items()) {
    // get_container() 등이 매 반복마다 호출되지는 않음(범위는 한 번 평가)
}

표준 의미론상 범위 표현식은 한 번 평가됩니다.


C++20 std::ranges와의 관계

C++20 **std::ranges**는 (지연 평가 시퀀스)와 함께 쓰일 때도 범위 for와 자연스럽게 맞물립니다.

#include <ranges>
std::vector<int> v = {1, 2, 3, 4, 5};
for (int x : v | std::views::filter([](int n) { return n % 2 == 0; })) {
    std::cout << x << ' ';
}

여기서 파이프된 표현식 전체가 “범위”이며, 뷰는 보통 가벼운 값이라 복사 비용이 크지 않습니다. 범위 표현식의 타입ranges 개념을 만족하면 begin/end 탐색 규칙이 확장됩니다(프로젝트가 C++20을 쓴다면 ranges 문서 참고).


vector<bool> 심화 (프록시)

std::vector<bool>비트 압축 특수화로, operator[]bool에 대한 진짜 참조가 아닌 프록시를 돌려줄 수 있습니다. 템플릿에서 std::vector<T>T&를 가정하면 T = bool에서 깨질 수 있어, 제네릭 코드에서는 vector<bool>을 특별 취급하거나 std::deque<bool> / std::vector<char> 등을 검토합니다. 범위 for에서 auto& x로 순회·대입하는 일상적 사용은 대체로 문제 없습니다.


요약

주제핵심
auto요소 복사, 원본 불변
auto& / const auto&참조, 수정 vs 읽기 전용
임시범위 임시는 루프 수명으로 연장; 밖으로 참조 빼내기 금지
구조화 바인딩map, pair, tuple 순회에 유용
커스텀 타입begin/end 또는 멤버 begin/end

관련 글: auto 키워드, 구조화 바인딩, 타입 추론.


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

  • C++ auto 키워드
  • C++ 구조화 바인딩
  • C++ auto 타입 추론

관련 글

  • C++ for·while 마스터
  • 모던 C++ (C++11~C++20) 핵심 문법 치트시트