`requires` 표현식(requires expression, C++20부터)

requires 표현식(requires expression, C++20부터)

어떤 템플릿 인자가 그 요구사항들을 만족하는지 컴파일 타임에 bool 값으로 묻고 싶을 때 쓰는 것이 requires 표현식이에요. 템플릿 매개변수에 대해 '이런 식은 성립하니?', '이런 중첩 타입은 있니?' 같은 조건들을 검사해서, 성립하면 true를, 아니면 false를 돌려주는 복수식(prvalue) 표현식이죠.

출처: cppreference

본문

문법

requires { requirement-seq } (1)
requires ( parameter-list(optional) ) { requirement-seq } (2)

동작 방식(Explanation)

요구사항(requirement)은 스코프 안에 있는 템플릿 매개변수, parameter-list의 매개변수, 그리고 둘러싼 문맥에서 보이는 다른 모든 선언을 참조할 수 있어요.

템플릿 엔티티의 선언에서 사용된 requires 표현식에 템플릿 인자를 치환(substitution)했을 때, 그 요구사항에서 잘못된 타입이나 표현식이 만들어지거나 요구사항의 의미론적 제약을 위반할 수 있어요. 그런 경우 requires 표현식은 false로 평가되고 프로그램이 ill-formed가 되지는 않아요. 치환과 의미론적 제약 검사는 어휘 순서대로 진행되며, requires 표현식의 결과를 결정하는 조건을 만나는 순간 멈춰요. 치환(있을 경우)과 의미론적 제약 검사가 모두 성공하면 requires 표현식은 true로 평가돼요.

만약 모든 가능한 템플릿 인자에 대해 치환 실패가 일어난다면 프로그램은 ill-formed이며 진단은 필요 없어요:

template<class T>
concept C = requires
{
    new int[-(int)sizeof(T)]; // 모든 T에 대해 잘못된 표현식: ill-formed, 진단 불필요
};

requires 표현식의 요구사항에 잘못된 타입이나 표현식이 들어 있고, 그 표현식이 템플릿 엔티티의 선언 안에 나타나지 않는다면, 프로그램은 ill-formed예요.

로컬 매개변수(Local parameters)

requires 표현식은 매개변수 목록으로 로컬 매개변수를 도입할 수 있어요. 이 매개변수들은 연결(linkage), 저장(storage), 수명(lifetime)이 없고, 요구사항을 정의하기 위한 표기법으로만 쓰여요.

각 매개변수의 타입은 함수 매개변수의 실제 타입을 결정하는 것과 같은 방식으로 정해져요:

template<typename T>
concept C = requires(T p[2])
{
    (decltype(p))nullptr; // OK, p는 T* 타입
};

다음 조건 중 하나라도 만족하면 프로그램은 ill-formed예요.

  • 로컬 매개변수가 기본 인자(default argument)를 가진다.
  • 매개변수 목록이 줄임표(ellipsis)로 끝난다.
template<typename T>
concept C1 = requires(T t = 0) // 오류: t가 기본 인자를 가짐
{
    t;
};

template<typename T>
concept C2 = requires(T t, ...) // 오류: 줄임표로 끝남
{
    t;
};

단순 요구사항(Simple requirements)

expression ;
자리 의미
expression requires로 시작하지 않는 표현식

단순 요구사항은 표현식이 유효함을 주장해요. expression은 비평가 피연산자(unevaluated operand)예요.

template<typename T>
concept Addable = requires(T a, T b)
{
    a + b; // "표현식 a + b는 컴파일되는 유효한 표현식이다"
};

template<class T, class U = T>
concept Swappable = requires(T&& t, U&& u)
{
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};

키워드 requires로 시작하는 요구사항은 항상 중첩 요구사항(nested requirement)으로 해석돼요. 따라서 단순 요구사항은 괄호로 묶지 않은 requires 표현식으로 시작할 수 없어요.

타입 요구사항(Type requirements)

typename type-name ; (1)
typename splice-specifier ; (2) (C++26부터)
typename splice-specialization-specifier ; (3) (C++26부터)

타입 요구사항은 type-name이 이름 짓는 타입 또는 splice-specifier·splice-specialization-specifier(C++26부터)가 가리키는 타입이 유효함을 주장해요. 이를 통해 특정 이름의 중첩 타입이 존재하는지, 또는 클래스/별칭 템플릿 특수화가 타입을 이름 짓는지 확인할 수 있어요. 클래스 템플릿 특수화를 이름 짓는 타입 요구사항은 그 타입이 완전(complete)할 것을 요구하지는 않아요.

template<typename T>
using Ref = T&;

template<typename T>
concept C = requires
{
    typename T::inner; // 필수 중첩 멤버 이름
    typename S<T>;     // 필수 클래스 템플릿 특수화
    typename Ref<T>;   // 필수 별칭 템플릿 치환
};

template<class T, class U>
using CommonType = std::common_type_t<T, U>;

template<class T, class U>
concept Common = requires(T&& t, U&& u)
{
    typename CommonType<T, U>; // CommonType<T, U>가 유효하고 타입을 이름 짓는다
    { CommonType<T, U>{std::forward<T>(t)} };
    { CommonType<T, U>{std::forward<U>(u)} };
};

복합 요구사항(Compound requirements)

{ expression } ; (1)
{ expression } noexcept ; (2)
{ expression } -> type-constraint ; (3)
{ expression } noexcept -> type-constraint ; (4)
자리 의미
expression 표현식
type-constraint 제약(constraint)

복합 요구사항은 표현식의 속성들을 주장해요. 치환과 의미론적 제약 검사는 다음 순서로 진행돼요.

  1. 템플릿 인자(있을 경우)가 expression에 치환된다.

  2. noexcept가 지정되어 있으면, 그 표현식이 예외를 던지지 않아야 한다. 그렇지 않으면 둘러싼 requires 표현식은 false가 돼요.

  3. type-constraint가 있으면:

  a) 템플릿 인자가 type-constraint에 치환된다.

  b) decltype(( expression ))이 type-constraint가 부과한 제약을 만족해야 한다. 그렇지 않으면 둘러싼 requires 표현식은 false가 돼요.

expression은 비평가 피연산자(unevaluated operand)예요.

template<typename T>
concept C2 = requires(T x)
{
    // 표현식 *x가 유효해야 하고
    // AND 타입 T::inner가 유효해야 하며
    // AND *x의 결과가 T::inner로 변환 가능해야 한다
    { *x } -> std::convertible_to<typename T::inner>;

    // 표현식 x + 1이 유효해야 하고
    // AND std::same_as<decltype((x + 1)), int>가 만족되어야 한다
    // 즉 (x + 1)이 int 타입의 prvalue이어야 한다
    { x + 1 } -> std::same_as<int>;

    // 표현식 x * 1이 유효해야 하고
    // AND 그 결과가 T로 변환 가능해야 한다
    { x * 1 } -> std::convertible_to<T>;
};

중첩 요구사항(Nested requirements)

requires constraint-expression ;
자리 의미
constraint-expression 제약(constraints)을 나타내는 표현식

중첩 요구사항은 로컬 매개변수에 대해 추가적인 제약을 지정할 때 쓸 수 있어요. constraint-expression은 치환된 템플릿 인자(있을 경우)에 의해 만족되어야 해요. 중첩 요구사항에 템플릿 인자를 치환하면, constraint-expression이 만족되는지를 판단하는 데 필요한 만큼만 그 표현식에 대한 치환이 일어나요.

template<class T>
concept Semiregular = DefaultConstructible<T>
    && CopyConstructible<T> && CopyAssignable<T> && Destructible<T>
    && requires(T a, std::size_t n)
{
    requires Same<T*, decltype(&a)>;            // 중첩: "Same<...>가 참으로 평가"
    { a.~T() } noexcept;                        // 복합: "a.~T()"가 예외를 던지지 않는 유효한 표현식
    requires Same<T*, decltype(new T)>;         // 중첩: "Same<...>가 참으로 평가"
    requires Same<T*, decltype(new T[n])>;      // 중첩: "Same<...>가 참으로 평가"
    { delete new T };                           // 복합
    { delete new T[n] };                        // 복합
};

주의(Note)

키워드 requiresrequires 절(requires clause)를 도입할 때도 쓰여요.

template<typename T>
concept Addable = requires(T x) { x + x; }; // requires 표현식

template<typename T>
    requires Addable<T>                      // requires 절, requires 표현식이 아님
T add(T a, T b) { return a + b; }

template<typename T>
    requires requires(T x) { x + x; }        // 애드혹 제약, 키워드가 두 번 쓰였음에 주의
T add(T a, T b) { return a + b; }

더 알아보기

  • 제약과 개념(constraints and concepts): requires 절(requires clause)과 개념 정의 전반.
  • 코루틴: requires 표현식은 템플릿 메타프로그래밍에서 제약 검사에 쓰여요.
  • SFINAE: 치환 실패를 컴파일 오류 대신 오버로드 후보에서 빼내는 원리 — requires의 동작 방식과 관련.