범위 기반 for 문
범위 기반 for 문 (Range-based for loop)
배열이나 컨테이너의 모든 요소를 처음부터 끝까지 돌리고 싶을 때, 인덱스를 하나하나 관리하지 않아도 되는 반복문이 있으면 편하겠죠? 범위 기반 for 문은 전통적인 for 루프의 더 읽기 쉬운 대안이에요. 이 페이지에서 그 구문과 동작 방식을 설명할게요.
출처: cppreference
⚠️ 원래 대상 URL
https://en.cppreference.com/w/cpp/language/range는 cppreference에서 빈 페이지(문서 없음)라, 실제 문서인range-for페이지에서 번역했어요.
본문
범위 기반 for 문은 범위(sequence or range)에 대해 for 루프를 실행해요. 컨테이너의 모든 요소처럼 값의 범위에 대해 동작하는 전통적인 for 루프의 더 읽기 쉬운 동등물로 쓰여요.
구문 (Syntax)
attr (optional) for ( init-statement (optional) item-declaration : range-initializer ) statement
attr— 임의 개수의 속성.init-statement— (since C++20) 다음 중 하나.- 표현식문(널 문
;일 수 있음), - 단순 선언(보통 초기화자가 있는 변수 선언, 임의 개수의 변수를 선언하거나 구조적 바인딩 선언일 수 있음),
- 별칭 선언. (since C++23)
- 표현식문(널 문
어떤 init-statement든 세미콜론으로 끝나야 한다는 점에 주의하세요. 그래서 비공식적으로 "표현식이나 선언 뒤에 세미콜론"으로 자주 표현돼요.
item-declaration— 다음 중 하나.- init-declarator-list에 init-declarator가 하나만 있고 초기화자가 없는 단순 선언,
- 초기화자가 없는 구조적 바인딩 선언. (since C++17)
선언 지정자 시퀀스는 타입 지정자와 constexpr만 담을 수 있고, 클래스나 열거형을 정의할 수 없어요.
range-initializer— 표현식 또는 중괄호로 감싼 초기화자 목록.statement— 어떤 문(보통 복합문).
설명 (Explanation)
위 구문은 다음 코드와 동등한 코드를 만들어요. 단 range-initializer의 임시 객체 수명 확장은 예외예요(since C++23)(/* */로 감싼 변수와 표현식은 설명용(exposition)일 뿐).
{ // (until C++17)
auto&& /* range */ = range-initializer ;
for (auto /* begin */ = /* begin-expr */, /* end */ = /* end-expr */;
/* begin */ != /* end */; ++/* begin */)
{
item-declaration = */* begin */;
statement
}
}
{
auto&& /* range */ = range-initializer ; // (since C++17)(until C++20)
auto /* begin */ = /* begin-expr */;
auto /* end */ = /* end-expr */;
for ( ; /* begin */ != /* end */; ++/* begin */)
{
item-declaration = */* begin */;
statement
}
}
{
init-statement // (since C++20)
auto&& /* range */ = range-initializer ;
auto /* begin */ = /* begin-expr */;
auto /* end */ = /* end-expr */;
for ( ; /* begin */ != /* end */; ++/* begin */)
{
item-declaration = */* begin */;
statement
}
}
range-initializer는 반복할 시퀀스나 범위를 초기화하도록 평가돼요. 시퀀스의 각 요소는 차례로 역참조되어 item-declaration에 주어진 타입과 이름의 변수를 초기화하는 데 쓰여요.
설명용 표현식 /* begin-expr */와 /* end-expr */은 다음과 같이 정의돼요.
/* range */의 타입이 배열 타입R에 대한 참조면:R이 경계N이면,/* begin-expr */은/* range */,/* end-expr */은/* range */ + N.R이 알 수 없는 경계의 배열이거나 불완전 타입의 배열이면, 프로그램은 ill-formed.
/* range */의 타입이 클래스 타입C에 대한 참조이고,C의 스코프에서 "begin"과 "end" 이름을 탐색했을 때 각각 선언이 적어도 하나 발견되면,/* begin-expr */은/* range */.begin(),/* end-expr */은/* range */.end().- 그 외,
/* begin-expr */은begin(/* range */),/* end-expr */은end(/* range */)인데, 여기서 "begin"과 "end"는 인자 종속 탐색(ADL)으로 찾아요(비-ADL 탐색은 수행되지 않음).
루프로서, 범위 기반 for 문은 루프를 일찍 빠져나오는 break 문과, 현재 반복을 끝내고 다음 요소로 넘어가는 continue 문을 담을 수 있어요.
init-statement에서 도입된 이름을 statement의 가장 바깥 블록에서 다시 선언하면 프로그램은 ill-formed예요.
for (int i : {1, 2, 3})
int i = 1; // error: redeclaration
임시 범위 초기화자 (Temporary range initializer)
range-initializer가 임시 객체를 반환하면, 그 수명은 전달 참조 /* range */에 묶이면서 루프가 끝날 때까지 확장돼요.
range-initializer 안의 모든 임시 객체의 수명은, 그들이 원래 range-initializer의 끝에서 파괴되지 않을 때만 확장돼요. (since C++23)
// if foo() returns by value
for (auto& x : foo().items()) { /* ... */ } // until C++23 undefined behavior
이 문제는 임시 객체를 init-statement의 변수에 묶으면 해결할 수 있어요.
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK (since C++20)
C++23에서도 중간 함수 호출의 비참조 매개변수는 수명 확장을 받지 못해요(일부 ABI에서 callee에서 파괴되기 때문). 하지만 그것은 어차피 버그가 있는 함수에서만 문제가 돼요.
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t) { return t; } // always returns a dangling reference
T g();
void foo()
{
for (auto e : f1(g())) {} // OK: lifetime of return value of g() extended
for (auto e : f2(g())) {} // UB: lifetime of f2's value parameter ends early
}
참고 (Notes)
range-initializer가 중괄호로 감싼 초기화자 목록이면,/* range */는std::initializer_list에 대한 참조로 추론돼요.- 제네릭 코드에서 전달 참조로 추론하는 것, 즉
for (auto&& var : sequence)를 쓰는 것이 안전하고 실제로 선호돼요. - 범위 타입에 "begin"과 "end"라는 이름의 멤버가 있으면 멤버 해석(member interpretation)을 사용해요. 이는 그 멤버가 타입·데이터 멤버·함수·열거자든 상관없고, 접근성도 상관없어요. 따라서
class meow { enum { begin = 1, end = 2 }; /* rest of class */ };같은 클래스는 네임스페이스 스코프의 "begin"/"end" 함수가 있어도 범위 기반 for 문에 쓸 수 없어요. item-declaration에 선언된 변수는 보통statement에서 쓰지만, 그럴 필요는 없어요.- C++17부터
/* begin-expr */과/* end-expr */의 타입은 같지 않아도 되고, 실제로/* end-expr */의 타입이 반복자일 필요도 없어요. 그냥 하나와 부등 비교가 가능하기만 하면 돼요. 이로써 술어로 범위를 구분할 수 있어요(예: "반복자가 널 문자를 가리킴"). (since C++17) - copy-on-write 의미론을 가진 (비-const) 객체에 쓰면, 범위 기반 for 문은 (암시적으로) 비-const
begin()멤버 함수를 호출해 깊은 복사를 일으킬 수 있어요.- 그것이 바람직하지 않으면(예: 루프가 실제로 객체를 수정하지 않는 경우)
std::as_const를 쓰면 피할 수 있어요. (since C++17)
- 그것이 바람직하지 않으면(예: 루프가 실제로 객체를 수정하지 않는 경우)
struct cow_string { /* ... */ }; // a copy-on-write string
cow_string str = /* ... */;
// for (auto x : str) { /* ... */ } // may cause deep copy
for (auto x : std::as_const(str)) { /* ... */ }
- 기능 테스트 매크로:
__cpp_range_based_for200907L (C++11) 범위 기반 for 루프- 201603L (C++17) 다른 begin/end 타입을 쓰는 범위 기반 for 루프
- 202211L (C++23) range-initializer의 모든 임시 객체 수명 확장
키워드 (Keywords)
for
예제 (Example)
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v = {0, 1, 2, 3, 4, 5};
for (const int& i : v) // access by const reference
std::cout << i << ' ';
std::cout << '\n';
for (auto i : v) // access by value, the type of i is int
std::cout << i << ' ';
std::cout << '\n';
for (auto&& i : v) // access by forwarding reference, the type of i is int&
std::cout << i << ' ';
std::cout << '\n';
const auto& cv = v;
for (auto&& i : cv) // access by f-d reference, the type of i is const int&
std::cout << i << ' ';
std::cout << '\n';
for (int n : {0, 1, 2, 3, 4, 5}) // the initializer may be a
// braced-enclosed initializer list
std::cout << n << ' ';
std::cout << '\n';
int a[] = {0, 1, 2, 3, 4, 5};
for (int n : a) // the initializer may be an array
std::cout << n << ' ';
std::cout << '\n';
for ([[maybe_unused]] int n : a)
std::cout << 1 << ' '; // the loop variable need not be used
std::cout << '\n';
for (auto n = v.size(); auto i : v) // the init-statement (C++20)
std::cout << --n + i << ' ';
std::cout << '\n';
for (typedef decltype(v)::value_type elem_t; elem_t i : v)
// typedef declaration as init-statement (C++20)
std::cout << i << ' ';
std::cout << '\n';
for (using elem_t = decltype(v)::value_type; elem_t i : v)
// alias declaration as init-statement (C++23)
std::cout << i << ' ';
std::cout << '\n';
}
출력:
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
1 1 1 1 1 1
5 5 5 5 5 5
0 1 2 3 4 5
0 1 2 3 4 5
결함 보고서 (Defect reports)
- CWG 1442 (C++11): 비멤버 "begin"/"end" 탐색에 보통 비정규화된 탐색이 포함되는지가 불명확했음 → 포함하지 않음.
- CWG 2220 (C++11): init-statement에서 도입된 이름이 재선언될 수 있었음 → 그 경우 ill-formed.
- CWG 2825 (C++11): range-initializer가 중괄호 초기화자 목록이면 비멤버 "begin"/"end"를 탐색함 → 이 경우 멤버 "begin"/"end"를 탐색.
- P0962R1 (C++11): "begin" 또는 "end" 멤버 중 하나만 있어도 멤버 해석을 사용했음 → 둘 다 있을 때만 사용.
더 알아보기
- for_each — 범위의 요소에 단항 함수 객체를 적용(함수 템플릿 & 알고리즘 함수 객체)
- ranges::for_each (C++20)