Constraints and concepts (since C++20)
클래스 템플릿 , 함수 템플릿 ( 제네릭 람다 포함), 그리고 다른 템플릿화된 함수들 (일반적으로 클래스 템플릿의 멤버들)은 제약 조건 과 연관될 수 있으며, 이는 템플릿 인자에 대한 요구사항을 지정하고, 가장 적합한 함수 오버로드와 템플릿 특수화를 선택하는 데 사용될 수 있습니다.
이러한 요구사항 들의 명명된 집합을 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
>
|
|||||||||
| 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 이후) 유형이 있습니다:
|
4)
폴드 확장 제약 조건
|
(since C++26) |
선언과 관련된 제약 조건은 다음 순서로 피연산자들이 있는 논리 AND 표현식을 정규화 하여 결정됩니다:
- 각 제약된 타입 템플릿 매개변수 또는 제약된 플레이스홀더 타입 으로 선언된 상수 템플릿 매개변수에 대해 도입된 제약 표현식이 나타나는 순서대로;
- 템플릿 매개변수 목록 뒤에 오는 requires 절 내의 제약 표현식;
- 축약 함수 템플릿 선언에서 제약된 플레이스홀더 타입 을 가진 각 매개변수에 대해 도입된 제약 표현식;
- 후행 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에서 가져옴 }
폴드 확장 제약 조건
폴드 확장 제약 조건
은 제약 조건
N 을 팩 확장 매개변수의 요소 수라고 하면:
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
|
(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 표현식 이 사용되는 것입니다.
표현식은 다음 형식 중 하나를 가져야 합니다:
- 기본 표현식(primary expression) , 예를 들어 Swappable < T > , std:: is_integral < T > :: value , ( std:: is_object_v < Args > && ... ) , 또는 괄호로 묶인 모든 표현식.
-
&&연산자로 연결된 기본 표현식들의 시퀀스. -
||연산자로 연결된 위에서 언급된 표현식들의 시퀀스.
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를 포섭하는 것은 위에서 설명된 규칙을 사용하여 두 제약이 동일한 경우에만 성립합니다.
|
(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) | 조건부 트리비얼 특수 멤버 함수 |
키워드
결함 보고서
다음 동작 변경 결함 보고서는 이전에 발표된 C++ 표준에 소급 적용되었습니다.
| DR | 적용 대상 | 게시된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 2428 | C++20 | 개념에 속성을 적용할 수 없었음 | 허용됨 |
참고 항목
| 요구 표현식 (C++20) | bool 타입의 prvalue 표현식을 생성하며 제약 조건을 설명합니다 |