범위 기반 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_for 200907L (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" 멤버 중 하나만 있어도 멤버 해석을 사용했음 → 둘 다 있을 때만 사용.

더 알아보기