Namespaces
Variants

Constraints and concepts (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

클래스 템플릿 , 함수 템플릿 ( 제네릭 람다 포함), 그리고 다른 템플릿화된 함수들 (일반적으로 클래스 템플릿의 멤버들)은 제약 조건 과 연관될 수 있으며, 이는 템플릿 인자에 대한 요구사항을 지정하고, 가장 적합한 함수 오버로드와 템플릿 특수화를 선택하는 데 사용될 수 있습니다.

이러한 요구사항 들의 명명된 집합을 concept  라고 합니다. 각 concept은 컴파일 시간에 평가되는 predicate이며, 제약 조건으로 사용되는 템플릿의 인터페이스 일부가 됩니다:

#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
// "Hashable" 개념의 선언 - 어떤 타입 "T"가 다음을 만족하면 충족됨:
// 타입 "T"의 값 "a"에 대해, std::hash<T>{}(a) 표현식이 컴파일되고
// 그 결과가 std::size_t로 변환 가능해야 함
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
struct meow {};
// 제약된 C++20 함수 템플릿:
template<Hashable T>
void f(T) {}
//
// 동일한 제약을 적용하는 대체 방법들:
// template<typename T>
//     requires Hashable<T>
// void f(T) {}
//
// template<typename T>
// void f(T) requires Hashable<T> {}
//
// void f(Hashable auto /* 매개변수-이름 */) {}
int main()
{
    using std::operator""s;
    f("abc"s);    // OK, std::string이 Hashable을 만족함
    // f(meow{}); // Error: meow는 Hashable을 만족하지 않음
}

제약 조건 위반은 컴파일 타임에, 템플릿 인스턴스화 과정 초기에 감지되어 이해하기 쉬운 오류 메시지를 제공합니다:

std::list<int> l = {3, -1, 10};
std::sort(l.begin(), l.end()); 
// 개념 없이 일반적인 컴파일러 진단 메시지:
// invalid operands to binary expression ('std::_List_iterator<int>' and
// 'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lines of output ...
//
// 개념을 사용한 일반적인 컴파일러 진단 메시지:
// error: cannot call std::sort with std::_List_iterator<int>
// note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied

개념의 의도는 구문적 제약(HasPlus, Array)보다는 의미론적 범주(Number, Range, RegularFunction)를 모델링하는 것입니다. ISO C++ 핵심 가이드라인 T.20 에 따르면, "의미 있는 의미론을 명시할 수 있는 능력은 구문적 제약과 대비되는 진정한 개념의 정의적 특성입니다."

목차

개념

개념(concept)은 명명된 요구 사항(requirements) 집합입니다. 개념의 정의는 네임스페이스 범위(namespace scope)에서 나타나야 합니다.

개념의 정의는 다음과 같은 형태를 가집니다

template < template-parameter-list >

concept concept-name attr  (선택 사항) = constraint-expression ;

attr - 임의 개수의 attributes 시퀀스
// 개념
template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;

개념은 재귀적으로 자기 자신을 참조할 수 없으며 제약될 수 없습니다:

template<typename T>
concept V = V<T*>; // 오류: 재귀적 개념
template<class T>
concept C1 = true;
template<C1 T>
concept Error1 = true; // 오류: C1 T가 개념 정의를 제약하려고 시도함
template<class T> requires C1<T>
concept Error2 = true; // 오류: requires 절이 개념을 제약하려고 시도함

개념의 명시적 인스턴스화, 명시적 특수화, 또는 부분 특수화는 허용되지 않습니다 (제약 조건의 원래 정의 의미를 변경할 수 없음).

개념은 id-표현식에서 이름을 가질 수 있습니다. id-표현식의 값은 true 만약 제약 조건 표현식이 만족되면, 그리고 false 그렇지 않으면입니다.

개념은 타입 제약의 일부로 이름을 지정할 수도 있으며,

type-constraint 에서는 개념이 매개변수 목록에서 요구하는 것보다 하나 적은 템플릿 인수를 사용합니다. 이는 문맥상 추론된 타입이 개념의 첫 번째 인수로 암시적으로 사용되기 때문입니다.

template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;
template<Derived<Base> T>
void f(T); // T는 Derived<T, Base>에 의해 제약됩니다

제약 조건

제약 조건은 템플릿 인자에 대한 요구사항을 지정하는 논리 연산과 피연산자의 시퀀스입니다. 이들은 requires expressions 내부에 나타나거나 개념의 본문으로 직접 사용될 수 있습니다.

제약 조건에는 세 가지 (C++26 이전) 네 가지 (C++26 이후) 유형이 있습니다:

1) 접속사
2) 논리합
3) atomic constraints
4) 폴드 확장 제약 조건
(since C++26)

선언과 관련된 제약 조건은 다음 순서로 피연산자들이 있는 논리 AND 표현식을 정규화 하여 결정됩니다:

  1. 각 제약된 타입 템플릿 매개변수 또는 제약된 플레이스홀더 타입 으로 선언된 상수 템플릿 매개변수에 대해 도입된 제약 표현식이 나타나는 순서대로;
  2. 템플릿 매개변수 목록 뒤에 오는 requires 내의 제약 표현식;
  3. 축약 함수 템플릿 선언에서 제약된 플레이스홀더 타입 을 가진 각 매개변수에 대해 도입된 제약 표현식;
  4. 후행 requires 내의 제약 표현식.

이 순서는 제약 조건이 만족 여부를 확인할 때 인스턴스화되는 순서를 결정합니다.

재선언

제약된 선언은 동일한 구문 형태를 사용해서만 재선언될 수 있습니다. 진단은 필요하지 않습니다:

// 처음 두 f 선언은 정상입니다
template<Incrementable T>
void f(T) requires Decrementable<T>;
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, 재선언
// 논리적으로 동등하지만 구문적으로 다른 이 세 번째 f 선언을 포함하는 것은
// 형식 오류이며, 진단이 필요하지 않습니다
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T);
// 다음 두 선언은 서로 다른 제약 조건을 가집니다:
// 첫 번째 선언은 Incrementable<T> && Decrementable<T>
// 두 번째 선언은 Decrementable<T> && Incrementable<T>
// 비록 논리적으로 동등하더라도 말입니다
template<Incrementable T> 
void g(T) requires Decrementable<T>;
template<Decrementable T> 
void g(T) requires Incrementable<T>; // 형식 오류, 진단이 필요하지 않음

접속사

두 제약 조건의 결합은 제약 조건 표현식에서 && 연산자를 사용하여 형성됩니다:

template<class T>
concept Integral = std::is_integral<T>::value;
template<class T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
template<class T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;

두 제약 조건의 논리곱(conjunction)은 두 제약 조건이 모두 만족될 때만 만족됩니다. 논리곱은 왼쪽에서 오른쪽으로 평가되며 단락 평가(short-circuited)됩니다 (왼쪽 제약 조건이 만족되지 않으면 오른쪽 제약 조건으로의 템플릿 인자 치환은 시도되지 않습니다: 이는 직접적 문맥 외부에서의 치환으로 인한 실패를 방지합니다).

template<typename T>
constexpr bool get_value() { return T::value; }
template<typename T>
    requires (sizeof(T) > 1 && get_value<T>())
void f(T);   // #1
void f(int); // #2
void g()
{
    f('A'); // OK, #2를 호출합니다. #1의 제약 조건을 확인할 때,
            // 'sizeof(char) > 1'이 만족되지 않으므로 get_value<T>()는 확인되지 않습니다
}

논리합

두 제약 조건의 논리합은 제약 조건 표현식에서 || 연산자를 사용하여 형성됩니다.

두 제약 조건의 분리는 두 제약 조건 중 하나가 충족되면 만족됩니다. 분리는 왼쪽에서 오른쪽으로 평가되며 단락 평가됩니다(왼쪽 제약 조건이 충족되면 오른쪽 제약 조건으로의 템플릿 인수 치환은 시도되지 않습니다).

template<class T = void>
    requires EqualityComparable<T> || Same<T, void>
struct equal_to;

원자적 제약 조건

원자적 제약은 표현식 E E 내에 나타나는 템플릿 매개변수로부터 제약된 개체의 템플릿 매개변수를 포함하는 템플릿 인자로의 매핑으로 구성되며, 이를 매개변수 매핑 이라고 합니다.

원자적 제약 조건은 제약 조건 정규화 과정 중에 형성됩니다. E 는 절대로 논리 AND 또는 논리 OR 표현이 아닙니다 (해당 표현들은 각각 연접 및 분접을 형성합니다).

원자적 제약 조건의 만족 여부는 매개변수 매핑과 템플릿 인자를 표현식 E 에 대입하여 확인합니다. 대입 결과 유효하지 않은 타입이나 표현식이 발생하면 제약 조건은 만족되지 않습니다. 그렇지 않은 경우, E 는 lvalue-to-rvalue 변환 후 반드시 bool 타입의 prvalue 상수 표현식이어야 하며, 해당 표현식이 true 로 평가될 때에만 제약 조건이 만족됩니다.

치환 후 E 의 타입은 정확히 bool 이어야 합니다. 변환은 허용되지 않습니다:

template<typename T>
struct S
{
    constexpr operator bool() const { return true; }
};
template<typename T>
    requires (S<T>{})
void f(T);   // #1
void f(int); // #2
void g()
{
    f(0); // 오류: #1을 확인할 때 S<int>{}가 bool 타입을 가지지 않음,
          // #2가 더 나은 매치임에도 불구하고
}

두 원자적 제약은 소스 수준에서 동일한 표현식으로 형성되고 매개변수 매핑이 동등한 경우 동일한 것으로 간주됩니다.

template<class T>
constexpr bool is_meowable = true;
template<class T>
constexpr bool is_cat = true;
template<class T>
concept Meowable = is_meowable<T>;
template<class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>;
template<class T>
concept GoodMeowableCat = Meowable<T> && is_cat<T>;
template<Meowable T>
void f1(T); // #1
template<BadMeowableCat T>
void f1(T); // #2
template<Meowable T>
void f2(T); // #3
template<GoodMeowableCat T>
void f2(T); // #4
void g()
{
    f1(0); // 오류, 모호함:
           // Meowable과 BadMeowableCat의 is_meowable<T>는 서로 다른 원자적 제약을 형성하며
           // 동일하지 않아(따라서 서로를 포함하지 않음)
    f2(0); // OK, #4 호출, #3보다 더 제약적임
           // GoodMeowableCat은 is_meowable<T>를 Meowable에서 가져옴
}

폴드 확장 제약 조건

폴드 확장 제약 조건 은 제약 조건 C 와 폴드 연산자( && 또는 || )로 구성됩니다. 폴드 확장 제약 조건은 팩 확장 입니다.

N 을 팩 확장 매개변수의 요소 수라고 하면:

  • 팩 확장이 유효하지 않은 경우(예: 크기가 다른 팩을 확장하는 경우), 폴드 확장 제약 조건은 만족되지 않습니다.
  • N 0 인 경우, 폴드 연산자가 && 이면 폴드 확장 제약 조건이 만족되고, 폴드 연산자가 || 이면 만족되지 않습니다.
  • 양의 N 을 가진 폴드 확장 제약 조건의 경우, [ 1 , N ] 범위의 각 i 에 대해, 각 팩 확장 매개변수는 증가하는 순서로 해당하는 i 번째 요소로 대체됩니다:
  • 폴드 연산자가 && 인 폴드 확장 제약 조건의 경우, j 번째 요소의 대체가 C 를 위반하면 폴드 확장 제약 조건이 만족되지 않습니다. 이 경우 j 보다 큰 i 에 대해 대체가 발생하지 않습니다. 그렇지 않으면 폴드 확장 제약 조건이 만족됩니다.
  • 폴드 연산자가 || 인 폴드 확장 제약 조건의 경우, j 번째 요소의 대체가 C 를 만족하면 폴드 확장 제약 조건이 만족됩니다. 이 경우 j 보다 큰 i 에 대해 대체가 발생하지 않습니다. 그렇지 않으면 폴드 확장 제약 조건이 만족되지 않습니다.


template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;
// C++23에서 이 두 g() 오버로드는 서로 동일하지 않고 서로를 포함하지 않는 별개의 원자적 제약 조건을 가짐: g() 호출이 모호함
// C++26에서는 폴드가 확장되고 오버로드 #2의 제약 조건(이동과 복사 모두 필요)이 오버로드 #1의 제약 조건(이동만 필요)을 포함함
template <class... T>
requires (A<T> && ...) void g(T...); // #1
template <class... T>
requires (C<T> && ...) void g(T...); // #2


(C++26부터)

제약 조건 정규화

제약 조건 정규화 는 제약 조건 표현식을 원자적 제약 조건들의 논리곱과 논리합의 연속으로 변환하는 과정입니다. 표현식의 정규 형식 은 다음과 같이 정의됩니다:

  • 표현식 ( E ) 의 정규 형식은 E 의 정규 형식입니다.
  • 표현식 E1 && E2 의 정규 형식은 E1 E2 의 정규 형식의 논리곱입니다.
  • 표현식 E1 || E2 의 정규 형식은 E1 E2 의 정규 형식의 논리합입니다.
  • 표현식 C < A1, A2, ... , AN > 의 정규 형식(여기서 C 는 concept을 지칭함)은 C 의 각 atomic constraint의 매개변수 매핑에서 C 의 해당 템플릿 매개변수에 A1 , A2 , ... , AN 을 대입한 후 C 의 constraint 표현식의 정규 형식입니다. 매개변수 매핑에 대한 이러한 대입 결과 유효하지 않은 타입이나 표현식이 발생하는 경우, 프로그램의 형식이 잘못되었으며 진단은 필요하지 않습니다.
template<typename T>
concept A = T::value || true;
template<typename U>
concept B = A<U*>; // OK: 분리의 정규화 결과
                   // - T::value (매핑 T -> U*) 와
                   // - true (빈 매핑)의 분리.
                   // T::value가 모든 포인터 타입에 대해 ill-formed이더라도
                   // 매핑에 유효하지 않은 타입이 없음
template<typename V>
concept C = B<V&>; // 다음의 분리로 정규화됨
                   // - T::value (매핑 T-> V&*) 와
                   // - true (빈 매핑)의 분리.
                   // 매핑에서 유효하지 않은 타입 V&*가 생성됨 => ill-formed NDR
  • 표현식 ( E && ... ) ( ... && E ) 의 정규 형식은 폴드 확장 제약 조건이며, 여기서 C E 의 정규 형식이고 폴드 연산자는 && 입니다.
  • 표현식 ( E || ... ) ( ... || E ) 의 정규 형식은 폴드 확장 제약 조건이며, 여기서 C E 의 정규 형식이고 폴드 연산자는 || 입니다.
  • 표현식 ( E1 && ... && E2 ) ( E1 || ... || E2 ) 의 정규 형식은 다음의 정규 형식입니다:
  • ( E1 && ... ) && E2 ( E1 || ... ) || E2 (각각), 만약 E1 이 확장되지 않은 팩을 포함하는 경우, 또는
  • E1 && ( ... && E2 ) E1 || ( ... || E2 ) (각각) 그렇지 않은 경우.
(C++26부터)
  • 다른 모든 표현식의 정규 형식은 E 이며, 이는 표현식이 E 이고 매개변수 매핑이 항등 매핑인 원자적 제약입니다. 여기에는 폴드 표현식 들이 포함되며, && || 연산자를 통해 폴딩하는 표현식들도 포함됩니다.

사용자 정의 오버로드된 && 또는 || 는 제약 조건 정규화에 아무런 영향을 미치지 않습니다.

requires

키워드 requires requires 절  을 도입하는 데 사용되며, 이는 템플릿 인자나 함수 선언에 대한 제약 조건을 지정합니다.

template<typename T>
void f(T&&) requires Eq<T>; // 함수 선언자의 마지막 요소로 나타날 수 있음
template<typename T> requires Addable<T> // 또는 템플릿 매개변수 목록 바로 뒤에
T add(T a, T b) { return a + b; }

이 경우, 키워드 requires 뒤에는 반드시 어떤 상수 표현식이 와야 합니다 (따라서 requires true 와 같이 작성하는 것이 가능합니다). 그러나 의도는 (위 예시에서처럼) 명명된 개념이나 명명된 개념들의 연접/선접, 혹은 requires 표현식 이 사용되는 것입니다.

표현식은 다음 형식 중 하나를 가져야 합니다:

template<class T>
constexpr bool is_meowable = true;
template<class T>
constexpr bool is_purrable() { return true; }
template<class T>
void f(T) requires is_meowable<T>; // 정상
template<class T>
void g(T) requires is_purrable<T>(); // 오류, is_purrable<T>()는 기본 표현식이 아님
template<class T>
void h(T) requires (is_purrable<T>()); // 정상

제약 조건의 부분 순서

추가 분석을 진행하기 전에, 모든 명명된 개념의 본문과 모든 정규화 과정을 통해 requires 표현식 을 대체하여 제약 조건들이 원자적 제약에 대한 연접과 선접의 연속으로만 구성되도록 합니다.

제약 조건 P 포섭한다(subsume) 고 말하는 것은 P Q 내의 원자적 제약 조건들의 동일성에 대해 P 함의한다(implies) Q 임을 증명할 수 있는 경우입니다. (타입과 표현식은 동등성에 대해 분석되지 않습니다: N > 0 N >= 0 을 포섭하지 않습니다).

구체적으로, 먼저 P 는 논리합 정규형으로 변환되고 Q 는 논리곱 정규형으로 변환됩니다. P Q 를 포함하는 경우는 다음 조건일 때만 성립합니다:

  • P 의 논리합 정규형에 있는 모든 논리합 절은 Q 의 논리곱 정규형에 있는 모든 논리곱 절을 포섭하는데,
  • 논리합 절이 논리곱 절을 포섭하는 것은 논리합 절에 있는 원자 제약 U 와 논리곱 절에 있는 원자 제약 V 가 존재하여 U V 를 포섭하는 경우에만 성립하며,
  • 원자 제약 A 가 원자 제약 B 를 포섭하는 것은 위에서 설명된 규칙을 사용하여 두 제약이 동일한 경우에만 성립합니다.
  • 폴드 확장 제약 조건 A 가 다른 폴드 확장 제약 조건 B 를 포함하는 경우는, 두 제약 조건이 동일한 폴드 연산자를 가지고, A 의 제약 조건 C B 의 제약 조건을 포함하며, 두 C 모두 동등한 비확장 팩을 포함할 때입니다.
(C++26부터)

서브섬션 관계는 제약 조건들의 부분 순서를 정의하며, 이를 통해 다음을 결정하는 데 사용됩니다:

선언문 D1 D2 가 제약 조건을 가지며 D1 의 연관 제약 조건이 D2 의 연관 제약 조건을 포함하는 경우(또는 D2 가 비제약 조건인 경우), D1 D2 보다 최소한 동등하게 제약된 것으로 간주됩니다. D1 D2 보다 최소한 동등하게 제약되고, D2 D1 보다 최소한 동등하게 제약되지 않는 경우, D1 D2 보다 더 제약된 것입니다.

다음 조건들이 모두 충족될 경우, 비템플릿 함수 F1 은 비템플릿 함수 F2 보다 더 부분 순서화 제약을 받는 것으로 간주됩니다:

  • 동일한 매개변수-타입-목록을 가집니다 , 명시적 객체 매개변수 의 타입은 생략 (C++23부터) .
  • 멤버 함수인 경우, 둘 다 동일한 클래스의 직접 멤버입니다.
  • 둘 다 비정적 멤버 함수인 경우, 객체 매개변수에 대해 동일한 타입을 가집니다.
  • F1 F2 보다 더 제약적입니다.
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
// RevIterator는 Decrementable을 포함하지만, 그 반대는 성립하지 않음
template<Decrementable T>
void f(T); // #1
template<RevIterator T>
void f(T); // #2, #1보다 더 제약적임
f(0);       // int는 Decrementable만 만족하므로 #1 선택
f((int*)0); // int*는 두 제약 조건 모두 만족하므로 더 제약적인 #2 선택
template<class T>
void g(T); // #3 (비제약)
template<Decrementable T>
void g(T); // #4
g(true); // bool은 Decrementable을 만족하지 않으므로 #3 선택
g(0);    // int는 Decrementable을 만족하므로 더 제약적인 #4 선택
template<typename T>
concept RevIterator2 = requires(T t) { --t; *t; };
template<Decrementable T>
void h(T); // #5
template<RevIterator2 T>
void h(T); // #6
h((int*)0); // 모호함

참고 사항

기능 테스트 매크로 표준 기능
__cpp_concepts 201907L (C++20) 제약 조건
202002L (C++20) 조건부 트리비얼 특수 멤버 함수

키워드

concept , requires , typename

결함 보고서

다음 동작 변경 결함 보고서는 이전에 발표된 C++ 표준에 소급 적용되었습니다.

DR 적용 대상 게시된 동작 올바른 동작
CWG 2428 C++20 개념에 속성을 적용할 수 없었음 허용됨

참고 항목

요구 표현식 (C++20) bool 타입의 prvalue 표현식을 생성하며 제약 조건을 설명합니다