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, map의 value_type 등을 한 번에 풀어서 순회할 수 있습니다.
std::map<int, std::string> m;
for (const auto& [key, val] : m) {
std::cout << key << ": " << val << '\n';
}
주의: std::map을 auto& [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) 핵심 문법 치트시트