Concept의 테스트 환경
concept를 지원하는 컴파일러 버전을 확인하시려면 다음의 경로에서 확인 할 수 있습니다
https://en.cppreference.com/w/cpp/compiler_support/20
또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.
Concept이 나오게 된 배경
c++ 20 이전에는 함수나 클래스에 접근 하는 방식은 다음과 같았습니다.
- 명시적인(specific) 호출
- 템플릿을 사용한 generic 호출
명시적인 호출을 사용한 방식을 사용하면 필요한 모든 데이터 형식에 대한 모든 함수를 Overloading 해야 합니다.
하지만 이 작업은 많은 비용이 소모되기도 합니다.
그렇다고 c++의 암묵적인 변환을 사용하게 되면 의도치 않은 문제가 발생합니다.
void printInt(int i) {
std::cout << i << '\n';
}
template<typename T>
auto add(T first, T second) {
return first + second;
}
int main()
{
printInt(true); // 암시적 변환 : 1
printInt(3.15); // 암시적 변환 : 3
// bool -> int로 승격
// ret은 int로 변경
cout << add(true, false) << "\n"; // 암시적 변환 : 1
}
이러한 문제를 해결 하기 위해서 함수 템플릿이나 클래스 템플릿을 사용 할 수 있지만
명시적인 제약 사항이 존재하지 않아서 이러한 문제로 오류 발생시 문제를 검출하기가 까다로웠습니다.
std::list<int> myList{ 1, 10, 3, 2, 5 };
// std::sort는 임의 접근 반복자만 허용합니다.
// 사용자는 임의 접근 반복자만 요구하는 것을 명시적으로 알 수 없습니다.
// 복잡한 오류 메시지가 사용자를 반겨줍니다.
std::sort(begin(myList), end(myList));
위와 같은 문제를 해결하기 위해서 c++20에서는 Concept라는 개념이 추가되었습니다.
Concept는 컴파일 시점의 술어로써 템플릿 매개변수에 의미론적(sementic)인 제약을 가합니다.
위의 예시에서의 std::sort에 std::random_access_iterator라는 concept를 제약 사항으로 설정 한 다면
list의 iterator는 std::bidirectional_iterator의 콘셉트만 지원하고 std::random_access_iterator는
지원하지 않는다는 명확한 오류 메시지를 전달 할 수 있다는 장점을 가집니다.
Concept의 장점
- 템플릿 매개변수에 대한 요구 조건들이 인터페이스의 일부가 됩니다.
- 콘셉트를 기반으로 함수를 중복 적재하거나 클래스 템플릿을 특수화 할 수 있습니다.
- 클래스나 클래스 템플릿의 일반적 멤버 함수에도 콘셉트를 적용 할 수 있습니다.
- 컴파일러는 템플릿 매개변수에 대한 요구 조건들을 기반으로 좀 더 개선된 오류 메시지를 생성 할 수 있습니다.
- 다른 사람들과 미리 정의한 콘셉트를 활용 할 수 있습니다.
- auto와 콘셉트 용법이 통합 되었습니다. auto 대신 콘셉트를 사용 할 수 있습니다.
- c++ 14에서는 일반적 람다가 도입되었습니다.
- c++ 20에서는 제약 있는 자리표(contrained placeholder)와 제약 없는 자리표(uncontrained placeholder)을 사용하면 auto로 선언된 함수가 있으면 자동으로 템플릿 함수로 변환됩니다.
- 함수 선언에 콘셉트가 있으면 그 함수 선언은 자동으로 함수 템플릿이 됩니다.
Concept의 적용 방법
콘셉트를 적용하는 여러 가지 방법에 대해서 예제로 공유드립니다.
requires 절 선언
requires라는 키워드로 시작하는 requires 절은 템플릿 매개변수나 함수 선언에 대한 요구 조건 또는 제약을 서술 합니다.
requires 키워드 다음에는 다음과 같은 형식이 올 수 있습니다.
- 하나의 명명된 콘셉트
- 명명된 콘셉트의 논리곱(&&)이나 논리 합(||)
- requires 표현식 같은 컴파일 시점 술어(compile-time predicate)
// require 절 행
// 하나의 명명된 콘셉트가 사용됨
template<typename T>
requires std::integral<T>
auto gcd(T a, T b) {
if (b == 0) return a;
else return gcd(b, a % b);
}
// 명명된 콘셉트의 논리곱(&&)
template<typename T>
requires std::integral<T> && std::movable<T>
auto gcd2(T a, T b) {
if (b == 0) return a;
else return gcd(b, a % b);
}
// 비형식 인수 컴파일 시점 술어
template<int i>
requires(i >= 0)
int64_t multiplication(int j)
{
return (i * j);
}
int main()
{
// std::integral는 템플릿 매개 변수를 기본형 정수로 제한합니다.
cout << gcd(10, 100) << '\n'; // 10
//cout << gcd(10.0f, 100.0f) << '\n'; // error 관련 제약 조건이 충족되지 않습니다.
cout << multiplication<2>(3) << '\n'; // 6;
//cout << multiplication<-1>(3) << '\n'; // error 관련 제약 조건이 충족되지 않습니다.
}
후행 require 절 선언
// 후행 requires 절 행
// 하나의 명명된 콘셉트가 사용됨
template<typename T>
auto gcd(T a, T b) requires std::integral<T> {
if (b == 0) return a;
else return gcd1(b, a % b);
};
템플릿 매개변수로 사용
템플릿 매개변수에 콘셉트를 지정할 수 도 있습니다.
// 제약이 있는 템플릿 매개변수
template<std::integral T>
auto gcd(T a, T b){
if (b == 0) return a;
else return gcd2(b, a % b);
}
// 단축 함수 템플릿 - 제약 있는 형식 매개변수
// 이 형태만 a와 b의 형식이 다를 수 있습니다.
std::integral auto gcd1(std::integral auto a, std::integral auto b){
if (b == 0) return a;
else return gcd3(b, a % b);
}
template<typename T>
struct Test {};
// 템플릿 특수화로 사용된 콘셉트
template<std::regular Reg>
struct Test<Reg> {};
// 가변 인수 템플릿에서 사용
template<std::integral... Args>
bool all(Args... args) { return (... && args); }
template<std::integral... Args>
bool any(Args... args) { return (... || args); }
template<std::integral... Args>
bool none(Args... args) { return !(... || args); }
int main()
{
Test<int> t; // std:regular를 통한 특수화
Test<int&> t2; // 일반 템플릿 Test 구조체 호출
cout << all(5, true, false) << '\n'; // false
cout << any(5, true, false) << '\n'; // true
cout << none(5, true, false) << '\n'; // false
}
함수 중복 적재(overloading)에서 사용
함수 중복 적재시에도 콘셉트를 지정 할 수 있습니다.
void overload(auto t) {
cout << "auto : " << t << '\n';
}
// 콘셉트를 사용해서 범위를 지정할 수 있습니다.
void overload(std::integral auto t) {
cout << "integral : " << t << '\n';
}
void overload(long l) {
cout << "long : " << l << '\n';
}
int main()
{
overload(3.14); // auto : 3.14
overload(2020); // integral : 2020
overload(2022L); // long : 2022
}
Concept의 정의 방법
// concept 정의 구문
template <template-parameter-list>
concept concept-name = constraint-expression;
constraint-expression은 다음 중 하나입니다.
- 기존 concept나 컴파일 시점 술어의 논리 조합
- 논리 조합에는 논리 연산자( &&, ||, ! ) 을 사용할 수 있습니다.
- 컴파일 시점의 술어는 컴파일 시점에 bool 값을 돌려주는 호출 가능 객체 입니다.
// concepts 파일에 정의된 integral, signed_integral, unsigned_integral의 형식
// integral은 is_integral_v 컴파일 술어를 사용해서 정의합니다.
template <class _Ty>
concept integral = is_integral_v<_Ty>;
// unsigned_integral은 콘셉트인 integral, signed_integral을 사용해서 정의합니다.
template <class _Ty>
concept unsigned_integral = integral<_Ty> && !signed_integral<_Ty>;
// signed_integral는 콘셉트인 integral을 사용해서 정의합니다.
// static_cast<_Ty>(-1) < static_cast<_Ty>(0) 컴파일 술어를 사용
template <class _Ty>
concept signed_integral = integral<_Ty> && static_cast<_Ty>(-1) < static_cast<_Ty>(0);
- 요구조건 표현식
// 요구 조건 표현식
// parameter-list : 함수 선언의 파라미터와 같음
// requirement-seq : 단순 요구조건, 형식 요구조건, 복합 요구 조건, 중첩 요구 조건으로 구성된 순차열
requires ( parameter-list(optional) ) { requirement-seq }
요구 조건 표현식 종류
단순 요구 조건
아래 예제와 같이 괄호에 묶여서 선언되어 사용됩니다.
// Addable는 T타입에 대해서 a + b 표현식이 유효해야 합니다.
template<typename T>
concept Addable = requires(T a, T b) {
a + b;
};
// Subtractable는 T타입에 대해서 a - b 표현식이 유효해야 합니다.
template<typename T>
concept Subtractable = requires(T a, T b) {
a - b;
};
// Swappable은 T, U가 swap 표현식을 만족 해야 합니다.
template<typename T, typename 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));
};
형식 요구 조건
형식 요구 조건을 표현 할 때는 typename과 형식 이름을 사용합니다.
template<typename T>
using Ref = T&;
template<typename T>
concept RequirementType = requires {
// T가 key_type을 가져야 합니다.
typename T::key_type;
typename T::allocator_type;
// Ref를 T로 인스턴스화 할 수 있어야 합니다.
typename Ref<T>;
};
auto printValueType(RequirementType auto a) {
cout << typeid(a).name() << endl;
}
int main()
{
// printValueType(std::vector<int>{1, 2, 3}); // 관련 제약 조건이 충족되지 않습니다
printValueType(std::map<int, int>{ {1, 2}, { 2, 3 }, { 3, 4 } });
}
복합 요구 조건
복합 요구 조건이 단순 요구 조건가 다른 점은
- 퓨현식에 {}(중괄호)를 감싸야 합니다.
- noexcept 지정자와 반환 형식 요구조건을 추가로 붙일 수 있습니다.
// 복합 요구 조건은 다음의 형식을 만족해야 합니다.
{ expression } noexcept(optional) return-type-requirement(optional) ;
template<typename T>
concept C2 = requires(T x)
{
// *x라는 표현식이 유효해야 합니다
// T::inner라는 타입도 유효 해야 합니다.
// *x의 결과는 T::inner로 변환 가능해야합니다.
{*x} -> std::convertible_to<typename T::inner>;
// x + 1라는 표현식이 유효 해야 하며 그 결과가 int형식이어야 합니다.
{x + 1} -> std::same_as<int>;
};
중첩 요구 조건
중첩 요구 조건은 requires안에 requires로 시작하는 표현식이 있는 것을 말합니다.
template<class T>
concept Semiregular = DefaultConstructible<T> &&
CopyConstructible<T> && Destructible<T> && CopyAssignable<T> &&
requires(T a, size_t n)
{
requires Same<T*, decltype(&a)>; // 중첩 요구 조건
requires Same<T*, decltype(new T)>; // 중첩 요구 조건
requires Same<T*, decltype(new T[n])>; // 중첩 요구 조건
{ a.~T() } noexcept; // 복합 요구 조건
{ delete new T }; // 복합 요구 조건
{ delete new T[n] }; // 복합 요구 조건
};
미리 정의된 Concept
매번 concept를 만드는 것 보다는 미리 정의된 concept를 재사용하는 것을 권장합니다.
추가로 미리 정의된 concept의 정의로 이동해서 구현 방식을 참고하는 것이 concept의 사용방법을 익히는데
많은 도움이 됩니다.
이름 | 설명 |
std::three_way_comparable<T, U> | <=>연산자 a <=> b 일때 a가 작으면 -1, 같으면 0, a가 크면 1을 리턴합니다. 다음의 조건을 만족하는지 체크합니다. 1. (a <=> b == 0) == bool(a == b)가 true 2. (a <=> b != 0) == bool(a != b)가 true 3. ( (a <=> b) <=> 0) 와 ( 0 <=> (a <=> b) )가 상등(equal) 4. (a <=> b < 0) == bool(a < b)가 true 5. (a <=> b > 0) == bool(a > b)가 true 6. (a <=> b <= 0) == bool(a <= b)가 true 7. (a <=> b >= 0) == bool(a >= b)가 true |
std::same_as<T, U> | 두 형식이 같은지 체크 |
std::derived_from<T, U> | template<class _Derived, class _Base> 형식의 콘셉트 첫번째 인자가 두번째 인자의 파생 형식인지 체크 |
std::convertible_to<T, U> | template <class _From, class _To> 형식의 콘셉트 첫번째 인자가 두번째 인자로 변환이 가능하지 체크 std::convertible_to<char* , std::string> : true std::convertible_to<std::string, char*> : false |
std::common_reference_with<T, U> | 두 형식을 어떤 공통의 참조 형식으로 변환 할 수 있음 std::common_reference_with<T, U>일 때 T, U가 어떤 형식 참조형식인 C로 변환 가능 체크 |
std::common_with<T, U> | std::common_reference_with 비슷한데 참조형식이 아니고 값 형식이여도 가능 |
std::assignable_from<T, U> | std::assignable_from<_LTy, _RTy> _LTy이 참조 형식이어야 하고 _RTy을 _LTy으로 배정 할 수 있어야 합니다. |
std::swappable<T, U> | 두 형식의 값을 교환 할 수 있음 |
std::integral<T> | 정수 형식(char, short, int, long, unsigned 형식) |
std::signed_integral<T> | 부호 있는 정수 형식(char, short, int, long,) |
std::unsigned_integral<T> | 부호 없는 정수 형식(unsigned + char, short, int, long,) |
std::floating_point<T> | 부동 소수점 형식(float, double, long double) |
std::destructible<T> | 소멸자 사용 가능 여부 |
std::constructible_from<T, ...Args> | std::constructible_from<_Ty, ... _ArgTys> 형식으로 _Ty 객체를 생성할 수 있는지 체크 |
std::default_initializable<T> | 객체 기본 생성자 호출 가능 여부 |
std::move_constructible<T> | 객체 이동 생성자 호출 가능 여부 |
std::copy_constructible<T> | 객체 복사 생성자 호출 가능 여부 |
std::equality_comparable<T> | 객체 상등 비교 가능 여부 |
std::totally_ordered<T> | 전 순서 집합 가능 여부 |
std::moveable<T> | 이동 가능 여부 is_object_v && move_constructible && assignable_from<T&, T> && swappable |
std::copyable<T> | 복사 가능 여부 copy_constructible && movable && assignable_from<T&, T&>, <T&, const T> <T&, const T&> |
std::semiregular<T> | 준 정규 형식 여부 copyable && default_initializable |
std::regular<T> | 정규 형식 여부 semiregular && equality_comparable |
std::invocable<F, ...Args> | std::invoke를 호출 할 수 있는지 여부 |
std::regular_invocable<F, ...Args> | invocable에 상등을 보존하고 함수 인수들을 수정하지 않습니다. |
std::predicate<F, T, U> | invocable하며 bool 값을 돌려 줍니다. |
std::input_iterator<It> | 입력 반복자 |
std::output_iterator<It, T> | 출력 반복자 |
std::forward_iterator<It> | 순방향(전진) 반복자 |
std::bidirectional_iterator<It> | 양방향 반복자 |
std::random_access_iterator<It> | 임의 접근 반복자 |
std::contiguous_iterator<It> | 연속 반복자 연속 반복자를 지원하려면 컨테이너 요소들을 메모리에 연속해서 저장 해야 합니다. (std::array, std::vector, std::string) |
std::permutable<It> | 요소들이 제자리 순서 변경이 가능함 std::forward_iterator |
std::mergeable<It1, It2, Out> | 정렬된 순차열들을 병합해서 출력 순차열로 산출 할 수 있음 |
std::sortable<It> | 요소들의 순서를 변경해서 정렬된 순차열을 만들 수 있음 |
'c++ > modern c++' 카테고리의 다른 글
[c++20] consteval과 constinit (0) | 2022.06.16 |
---|---|
[c++20] module(모듈) (0) | 2022.06.13 |
[c++] condition variable(조건 변수) (0) | 2020.07.10 |
[c++] std::lock 관련 함수들 (0) | 2020.05.22 |
[c++] std::mutex (0) | 2020.03.28 |