template for 확장

template for 확장 (template for expansion, C++26부터)

여러 (아마 서로 다른) 표현식에 같은 논리를 적용하는 문장 여러 개를, 더 읽기 쉬운 한 문장으로 표현하고 싶을 때 쓰는 게 template for예요. 컴파일 타임에 결정되는 언어 구성을 그 요소들로 확장해 줘요. 이 페이지에서는 template for 문의 문법과, 확장 이니셜라이저의 형태에 따라 달라지는 세 가지 범주를 정리할게요.

출처: cppreference

본문

template for는 컴파일 타임에 결정 가능한 언어 구성을 그 요소들로 확장해요.

여러 (아마 이질적인) 표현식에 같은 논리를 적용하는 문장 여러 개보다 읽기 쉬운 동등한 형태로 쓰여요.

문법

attr(선택) template for ( init-statement(선택) item-declaration : expansion-initializer ) compound-statement
항목 의미
attr - 임의 개수의 속성
init-statement - 다음 중 하나 — 표현식 문(널 문 ;일 수도 있음) — 단순 선언 (보통 이니셜라이저가 있는 변수 선언, 임의 개수의 변수나 구조적 바인딩 선언을 선언할 수 있음) — 별명 선언. 어떤 init-statement든 세미콜론으로 끝나야 한다는 점에 유의하세요. 그래서 흔히 비공식적으로 "표현식이나 선언 뒤에 세미콜론이 붙은 것"이라고 표현해요.
item-declaration - 다음 중 하나 — 다음 제한을 가진 단순 선언: · 선언자가 하나뿐이다. · 선언자가 이니셜라이저를 가지면 안 된다. · 선언 지정자 시퀀스는 타입 지정자와 constexpr만 담을 수 있고, 클래스열거를 정의할 수 없다. — 구조적 바인딩 선언
expansion-initializer - 다음 중 하나 — 표현식 — 중괄호로 감싸고 (선택적으로 꼬리 쉼표가 있는) 쉼표로 구분된 표현식 목록
compound-statement - 임의의 복합 문

동작 방식 (Explanation)

루프를 compound-statement 안에서 미리 종료해야 한다면 break을 종료 문으로 쓸 수 있어요.

현재 반복을 compound-statement 안에서 미리 종료해야 한다면 continue을 단축키로 쓸 수 있어요.

expansion-initializer의 문법과 속성에 따라 template for 문은 다음 세 범주 중 하나로 나뉘어요.

열거 확장 문 (Enumerating expansion statement)

expansion-initializer가 중괄호로 감싼 표현식 목록이면, 그 문은 열거 확장 문이에요.

표현식 목록의 요소 수를 N, 표현식 목록의 I번째 요소(인덱스는 0부터)를 /*expr-I*/라고 할 때, 열거 확장 문은 expansion-initializer의 임시 객체 수명 확장(아래 참고)을 제외하면 다음과 동등해요:

{
    init-statement
    {
        item-declaration = /*expr-0*/;
        compound-statement
    }
    // ...
    {
        item-declaration = /*expr-(N-1)*/;
        compound-statement
    }
}

표현식 목록은 팩 확장의 결과일 수 있어요.

반복 확장 문 (Iterating expansion statement)

expansion-initializer가 확장-반복 가능한(expansion-iterable, 아래 참고) 표현식이면, 그 문은 반복 확장 문이에요.

반복 확장 문은 expansion-initializer의 임시 객체 수명 확장(아래 참고)을 제외하면 다음과 동등해요. /* */로 감싼 변수와 표현식은 설명용(오직 exposition)이에요:

constexpr auto /*N*/ = [&] consteval
{
    std::ptrdiff_t result = 0;
    auto b = /*begin-expr*/;
    auto e = /*end-expr*/;
    for (; b != e; ++b)
        ++result;
    return result;
}();

{
    init-statement
    constexpr(선택) decltype(auto) /*range*/ = (expansion-initializer);
    constexpr(선택) auto /*begin*/ = /*begin-expr*/;
    {
        constexpr(선택) /*iter*/ = /*begin*/ + decltype(begin - begin){0};
        item-declaration = */*iter*/;
        compound-statement
    }
    // ...
    {
        constexpr(선택) /*iter*/ = /*begin*/ + decltype(begin - begin){/*N*/ - 1};
        item-declaration = */*iter*/;
        compound-statement
    }
}

/*range*/, /*begin*/, /*iter*/는 item-declaration에 constexpr 지정자가 있는 경우에만 constexpr로 선언돼요.

설명용 표현식 /*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 조회는 수행되지 않음)로 찾아요.

expansion-initializer가 비-배열 타입의 표현식이고, /*begin-expr*//*end-expr*/ 둘 다 위 규칙으로 잘 구성되면, expansion-initializer는 확장-반복 가능(expansion-iterable) 이에요.

구조 분해 확장 문 (Destructuring expansion statement)

expansion-initializer가 확장-반복 가능하지 않은 표현식이면, 그 문은 구조 분해 확장 문이에요.

expansion-initializer 타입의 구조적 바인딩 크기N이라고 할 때, 구조 분해 확장 문은 expansion-initializer의 임시 객체 수명 확장(아래 참고)을 제외하면 다음과 동등해요. /* */로 감싼 변수와 표현식은 설명용이에요:

  • N이 0이면, 그 문은 다음과 동등해요:
{
    init-statement
    constexpr(선택) auto&& /*range*/ = (expansion-initializer);
}
  • 그 외에는, 그 문은 다음과 동등해요:
{
    init-statement
    constexpr(선택) auto&& [/* u0, u1, ..., u(N-1) */] = (expansion-initializer);
    {
        item-declaration = /*v0*/;
        compound-statement
    }
    // ...
    {
        item-declaration = /*v(N-1)*/;
        compound-statement
    }
}

/*range*/, /*begin*/, /*iter*/는 item-declaration에 constexpr 지정자가 있는 경우에만 constexpr로 선언돼요.

expansion-initializer가 lvalue이면 /*vI*//*uI*/이고, 그렇지 않으면 /*vI*/static_cast<decltype(/*uI*/)&&>(/*uI*/)예요.

임시 확장 이니셜라이저 (Temporary expansion initializer)

열거 확장 문에서, 표현식 목록 expansion-initializer의 요소 expr 안에 임시 객체가 생성되고 그 객체가 expr의 전체 표현식 끝에서 파괴될 예정이면, 그 객체는 expr에서 초기화된 item-declaration의 수명 동안 유지돼요:

// foo()가 값을 반환한다면
template for (auto& x : {foo().items()}) { /* ... */ }

// 다음과 같이 확장됨:
{
    // foo()가 반환한 임시 객체에 대해:
    {
        auto& x = foo().items(); // 객체는 세미콜론에서 파괴될 예정이지만,
                                 // 블록 끝까지 수명이 연장됨
                                 // ("x"의 수명과 같음)
        { /* ... */ }            // 여기서 "x"를 사용하는 것은 잘 정의됨
    }
}

반복·구조 분해 확장 문에서, expansion-initializer 안에 임시 객체가 생성되고 그 객체가 expansion-initializer 끝에서 파괴될 예정이면, 그 객체는 expansion-initializer에서 초기화된 참조의 수명 동안 유지돼요:

struct T
{
    std::vector<int> vec{1, 2};
    std::tuple<int> tup{3, 4};
};

template for (auto& x: T().vec) { /* ... */ }

// 다음과 같이 확장됨:
{
    // T()가 반환한 임시 객체에 대해:
    decltype(auto) range = (T().vec); // 객체는 세미콜론에서 파괴될 예정이지만,
                                      // 그 수명은 블록 끝까지 연장됨
                                      // ("range"의 수명과 같음)
    auto begin = range.begin();
    /* 확장된 복합 문들 */           // 여기서 벡터 요소에 접근하는 것은
                                      // 잘 정의됨
}

template for (auto& x: T().tup) { /* ... */ }

// 다음과 같이 확장됨:
{
    // T()가 반환한 임시 객체에 대해:
    auto&& [u0, u1] = T().tup; // 객체는 세미콜론에서 파괴될 예정이지만,
                               // 블록 끝까지 수명 연장됨
                               // ("u0"과 "u1"의 수명과 같음)
    /* 확장된 복합 문들 */     // 여기서 "u0"과 "u1"에 접근하는 것은
                               // 잘 정의됨
}

주의할 점 (Notes)

template for는 아직 C++26의 확장 문(expansion statement) 기능으로, __cpp_expansion_statements 매크로가 202506L 값을 가질 때 사용할 수 있어요.

키워드

template, for

예제

여기서는 세 가지 범주의 template for가 실제로 어떻게 쓰이는지 확인해 볼게요.

#include <array>
#include <vector>

constexpr int f1(const auto&... containers)
{
    int result = 0;
    template for (const auto& c : {containers...}) // 열거 확장 문
        result += c[0];

    return result;
}

constexpr int f2()
{
    constexpr std::array<int, 3> arr{1, 2, 3};
    int result = 0;
    template for (constexpr int s : auto(arr)) // 반복 확장 문
        result += s;

    return result;
}

struct S
{
    int i;
    short s;
};

constexpr long f3(S s)
{
    long result = 0;
    template for (auto x : s) // 구조 분해 확장 문
        result += sizeof(x);

    return result;
}

int main()
{
    constexpr int c1[] = {1, 2, 3};
    constexpr int c2[] = {4, 3, 2, 1};
    static_assert(f1(c1, c2) == 5); // c1[0] + c2[0]

    static_assert(f2() == 6); // 1 + 2 + 3

    static_assert(f3(S{}) == sizeof(int) + sizeof(short));
}

f1은 팩 containers...를 중괄호 표현식 목록으로 감싼 열거 확장이라, 각 컨테이너의 c[0]을 더해 1 + 4 = 5가 돼요. f2auto(arr)를 확장-반복 가능한 범위로 보고 각 요소 s를 더해 1+2+3 = 6이 돼요. f3S를 데이터 멤버로 구조 분해해서 sizeof(int) + sizeof(short)를 구해요. 모두 constexpr 문맥에서 컴파일 타임에 계산돼요.

더 알아보기