Concept의 테스트 환경

concept를 지원하는 컴파일러 버전을 확인하시려면 다음의 경로에서 확인 할 수 있습니다

https://en.cppreference.com/w/cpp/compiler_support/20

concept 컴파일러 지원 버전

VisualStudio 버전별 컴파일러 버전

또한 프로젝트 우 클릭 -> 구성 속성 -> 일반 -> c++ 언어 표준을 ISO C++ 20 표준으로 수정하였습니다.

C++ 컴파일러 버전 변경

Concept이 나오게 된 배경

c++ 20 이전에는 함수나 클래스에 접근 하는 방식은 다음과 같았습니다.

  1. 명시적인(specific) 호출
  2. 템플릿을 사용한 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 키워드 다음에는 다음과 같은 형식이 올 수 있습니다.

  1. 하나의 명명된 콘셉트
  2. 명명된 콘셉트의 논리곱(&&)이나 논리 합(||)
  3. 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

+ Recent posts