본문으로 바로가기

C++20) Concepts ( 콘셉트, 개념 ) - 2

category C++/Modern 2020. 11. 27. 03:03

Concepts: Generic Programming의 미래

모든 내용은 Bjarne Stroustrup 교수님의 Good_Concepts에서 발췌하였습니다.

 

3. Concepts (개념) 사용하기

 

Concepts는 컴파일 시간 술어 (조건) 입니다. ( 즉, boolean 값을 생성하는 것 입니다. )

예를들어 템플릿 타입 인수 T가 있을 때, 다음과 같이 요구될 수 있습니다.

 

  • 반복자 타입 : Iterator<T>
  • 임의 접근 반복자 타입 : Random_access_iterator<T>
  • 숫자 타입 : Number<T>

표기법 C<T>에서 C는 Concepts이고 ( 위의 리스트가 이에 해당 ), 이는 'T가 충족되면 참' 을 의미하는 표현식 입니다.

C의 모든 조건이 맞지 않다면 거짓입니다.

마찬가지로 template 인수 집합이 술어를 충족해야 한다는 것도 지정할 수 있습니다.

예를 들어 Mergeable<In1, In2, Out> 과 같은 다중 형식 술어는 STL과 대부분의 다른 응용 프로그램 도메인을 설명하기 위해 필요합니다.

 

이것들은 매우 표현력이 뛰어나고, 컴파일하기에 저렴합니다. ( TMP 해결 방법보다 저렴합니다. )

 

학생들은 한 두 번의 강의 후에 Concepts를 사용할 수 있다. 물론 직접 자신만의 Concepts를 정의할 수 있고,

Concepts 라이브러리를 가질 수 있습니다. ( 5번 항목 참조 )

 

Concepts는 overloading을 가능하게 하고, 많은 ad-hoc 메타 프로그래밍과 많은 메타프로그래밍 scaffolding code의 필요성을 제거하며 generic programming도 현저하게 단순화 시킵니다.

 

3.1 템플릿 인터페이스 지정

 

먼저 Concepts를 사용하여 알고리즘을 지정하는 방법을 살펴보겠습니다.

std::find는 한 쌍의 Iterator대신 Sequence를 사용합니다.

template <typename S, typename T>
requires Sequence<S> && Equality_comparable<Value_type<S>, T>
Iterator_of<S> find(S& seq, const T& value);

라인별로 살펴봅시다.

  1. 이것은 두 개의 템플릿 유형 인수를 사용하는 템플릿입니다.
  2. 첫 번째 템플릿 인수는 Sequence(Sequence<S>)여야 하며, operator==를 통한 시퀀스 요소의 비교가 가능해야합니다. (Equality_comparable<Value_type<S>, T>)
  3. find() 함수는 reference로 Sequence를 가져오고, const reference로 찾을 값을 받습니다.  그리고, 반복자를 반환합니다. 
Sequence는 begin()과 end()가 있지만, find()를 이해하기 위한 것입니다. 중요하진 않습니다.

Value_type<S>Iterator_of<S>를 사용하기 위해 별칭 템플릿 ( alias templates ) 을 사용했습니다. 

정의는 다음과 같습니다.

template <typename X> using Value_type<X> = X::value_type;
template <typename X> using Iteartor_of<X> = X::Iterator;
별칭 템플릿 ( alias templates )은 Concepts와 특별한 관련이 없습니다. 표현하는데 유용합니다.

 

Equality_comparable Concepts는 표준 라이브러리 Concepts로 제안됩니다. 

en.cppreference.com/w/cpp/concepts/equality_comparable

그것은 operator==와 operator!=를 요구합니다. ( 이는 두 가지 인수를 취한다는 점을 유의하세요. )

많은 Concepts들이 2개 이상의 인수를 취합니다.

Concepts는 유형(types) 뿐만 아니라 유형간의 관계(relationships)도 표현할 수 있습니다.

이것은 STL과 기타 대부분의 라이브러리에 필수적입니다. ( 이러한 관계는 다음에 설명됩니다. )

Concepts는 단지 '유형의 유형' 만이 아닙니다.

 

이제 find의 사용에 대해 보겠습니다.

 

void use(vector<string>& vs, list<double>& lstd)
{
    auto p0 = find(vs, "Waldo"); // Well
    auto p1 = find(vs, 0.5772); //Ill-formed, string과 double의 비교를 할 수 없습니다.
    auto p2 = find(lstd, 0.5772); // Well
    auto p3 = find(lstd, "Waldo"); // Ill-formed, double과 string의 비교를 할 수 없습니다.
    
    if(p0 != vs.end()) { /* found Waldo */ }
    // ...
}

 

이것은 단지 하나의 예이고 아주 간단한 것입니다.

Ranges TS[Nie15] 에서 Concepts 사용에 대한 더 많은 예를 찾을 수 있습니다. ( Ranges에 대한 정리도 해둘 것입니다. )

 

3.2 속기 표기

 

템플릿 인수가 Sequence가 되도록 요구할 때, 다음과 같이 작성할 수 있습니다.

template <typename Seq> requires Sequence<Seq>
void algo(Seq& s);

즉 Sequence여야하는 Seq 유형의 인수가 필요합니다. 

 

하지만 다음과 같이 작성했을때, 

template <Sequence Seq>
void algo(Seq& s);

이건 위의 더 긴 버전과 정확히 동일하지만, 더 짧고 원했던 함수와 일치합니다.

마찬가지로 우리는 '동물이 있고, 닭이 있다' 라고 말하지 않습니다. '닭이 있다.' 라고 말합니다.

이처럼 Concepts에 대해서는 재작성 규칙이 간단하고 일반적입니다.

 

template <C T>

는 곧

template <typename T> requires C<T>

와 동일합니다.

 

이 간단한 속기를 하나의 Concepts에 사용합니다.

즉, 우리가 원할때 단순화 할 수 있습니다.

 

예를 들어

template <typename S, typename T>
requires Sequence<S> && Equality_comparable<Value_type<S>, T>
Iterator_of<S> find(S& seq, const T& value);

에서 

template <Sequence S, typaname T>
requires Equality_comparable<Value_type<S>, T>
Iterator_of<S> find(S& seq, const T& value);

이 짧은 형태가 긴 버전에 비해 명확성을 크게 향상 시킨다고 생각한다.

주로 다중 형식 Concepts에 대해서 ( 예 : Equality_comparable<Value_type<S>, T> )

그리고, 템플릿 인수 type을 반복적으로 참조해야 하는 경우 ( 예 : find의 S ) 명시적 requires를 사용합니다.

 

4. Concepts 정의

 

종종 라이브러리에서 Equality_comparable과 같은 유용한 Concepts를 찾을 수 있습니다 ( 예 : Ranges )

표준 라이브러리의 Concepts들을 보길 바랍니다. 어떻게 Concepts들을 정의했는지 확인해보세요.

template <typename T>
concept  quality_comparable =
	requires(T a, T b) {
    	    { a == b } -> std::same_as<bool>;
            { a != b } -> std::same_as<bool>;
    };

Equality_comparable Concepts는 변수 템플릿 ( variable template )으로 정의됩니다.

T 형은 반드시 bool 형을 반환하는 operator==와 operator!=를 제공해야 합니다. ( 기술적으로, bool로 변할 수 있다면 가능 )

요구 표현 ( requires expression ) 은 형식 ( type ) 을 어떻게 사용할 수 있는지를 직접 표현할 수 있게 해줍니다.

  • { a == b }는 operator==를 사용하여 두 T를 비교할 수 있어야 한다고 말합니다.
  • { a == b } -> bool 은 이러한 비교 결과가 bool ( 기술적으로, bool로 변할 수 있다면 가능 ) 이여야 한다고 말합니다.

요구 표현은 실제로 실행 ( execute )되지 않습니다. 대신, 컴파일러가 요구 사항들을 하나씩 살펴 봅니다.

그리고, 컴파일이 가능하다면 true를, 불가능 하다면 false를 반환합니다.

 

하나의 예제를 더 봅시다.

template <typename T>
concept bool Sequence = 
	requires(T t) {
            typename Value_type<T>;	// 반드시 value type을 가지고 있어야 합니다.
            typename Iterator_of<T>; // 반드시 iterator type을 가지고 있어야 합니다.
            
            { begin(t) } -> Iterator_of<T>; // 반드시 begin() 과 end() 가 있어야 합니다.
            { end(t) } -> Iterator_of<T>;
            
            requires Input_Iteartor<iterator_of<T>>;
            requires Same_type<Value_type<T>, Value_type<Iterator_of<T>>>;
        };

Sequence는 다음과 같습니다.

  • 타입 T는 두 개의 관련된 값인 ( two associated type ) Value_type<T>와 Iterator_of<T>를 가져야 합니다.
    Value_type<T>와 Iterator_of<T>는 그저 평범한 별칭 템플릿 ( alias template )일 뿐입니다.
    이러한 유형을 요구 표현에 나열하면, T 유형이 Sequence가 되어야 함을 나타 냅니다.
  • 타입 T는 각각 적절한 iterator를 반환하는 begin()과 end()를 제공해야 합니다.
  • 적절한 (appropriate) iterator 를 통해 T의 iterator 타입이 input_iterator여야 함을 의미합니다.
    Value_type은 해당 Iterator의 value type과 동일해야 합니다.
    Input_Iterator와 Same_type은 라이브러리의 개념이지만, 누구나 쉽게 작성할 수 있습니다.

마지막으로, Sortable을 살펴보겠습니다.

정렬하려면 타입이 Sequence 타입이여야 하며, 임의 접근 (random access)가 가능하고, operator<를 지원해야 합니다.

template <typename T>
concept Sortable = 
	Sequence<T> &&
        Random_access_iterator<iterator_of<T>> &&
        Less_than_comparable<Value_type<T>>;

Random_access_iteratorLess_than_comparableEqual_compatible과 유사하게 정의되어 있으며 라이브러리에서 조회할 수 있습니다.

 

종종 concepts 간의 관계( relationship )에 대한 요구사항을 만들고 싶어합니다.

예를 들어, Equality_compatible Concepts를 단일 유형을 요구하도록 정의했지만, 일반적으론 다음과 같은 두 가지 유형을 처리하도록 정의 됩니다.

template <typename T, typename U>
concept Equality_comparable = 
	requires (T a, U b) {
          { a == b } -> std::same_as<bool>;
          { a != b } -> std::same_as<bool>;
          { b == a } -> std::same_as<bool>;
          { b != a } -> std::same_as<bool>;
        };

이것은 int를 double로, string을 char* 로 비교하는것을 허용하지만, int를 string과 비교할 순 없습니다.