C++/C++ 특수 강좌 & 모던 C++

C++20 Concepts 알아보기 (1)

카루-R 2022. 2. 16. 14:26
반응형

환영합니다, Rolling Ress의 카루입니다.

오늘은 C++20에 드디어 추가된 concept에 대하여 알아보겠습니다. 템플릿 매개변수에 제약을 거는 거죠.

concept이란, 템플릿을 사용할 때 일정한 타입들을 묶어주는 기능이라고 생각하는 게 편합니다.

타입 이름에 별칭을 붙여주는 동시에, 그 자체로 bool값을 가지게 되죠.

<type_traits>에 있는 is_same_v 와 같이 ~_v 변수랑 비슷하다고 보시면 됩니다. 다만, 변수가 아니라 타입으로 쓸 수 있습니다.

선언은 이렇게 합니다.

template<typename T> //반드시 template 필요
concept 컨셉이름 = bool값의 식 또는 requires 구문

// Concepts TS 시절에는 아래와 같은 구문도 지원했으나, 이제는 지원하지 않습니다.
// concept bool 컨셉이름 = 초깃값;

일단 코드부터 보겠습니다. C++11이상에서 동작합니다.

template<typename T1, typename T2, 
    typename = typename enable_if<is_convertible<T1, int>::value 
    && is_convertible<T2, int>::value>::type>
long long int add_int(T1 t1, T2 t2) 
{
    return {t1 + t2}; 
}

C++11부터인가 아마 사용 가능할 거예요. 매개변수가 둘 다 int로 변환 가능하면 더해서 long long으로 반환하는 간단한 함수이죠. 단 변환 불가능하면 컴파일이 되지 않습니다. 이를 위해 enable_if를 사용하죠.

enable_if<bool>::type에서 조건식이 false이면 type이 존재하지 않습니다. 그래서 컴파일이 안돼요.

typename = typename 이부분의 첫 typename 템플릿 선언이고 '='은 기본 타입 지정, 뒤의 typename은 enable_if<bool>::type이 타입 이름이라는 걸 알려주는 역할을 합니다. 같은 typename이지만 용도가 달라요.

근데 너무 길어요. 줄입시다. C++17 이상에서 동작합니다.

template<typename T1, typename T2,
  typename = enable_if_t<is_convertible_v<T1, int> 
  && is_convertible_v<T2, int>>>
long long add_int(T1 t1, T2 t2) 
{ 
    return {t1 + t2}; 
}

typename enable_if<>::type을 enable_if_t로, is_convertible<.>::value를 is_convertible_v<.>로 줄였습니다. 보기 좀 더 편하네요. _v 는 <type_trais>에 있는 대부분의 값에 사용할 수 있습니다. is_same_v처럼요. 아마 이것도 inline 변수가 아닌가 싶은데, 

하지만 여전히 길어요. 장난합니까? 함수 몸체는 한 줄인데 템플릿이 두 줄?

그래서 Contracts가 도입될 예정이었습니다.

template<typename T1, typename T2>
[[expects: is_convertible_v<T1, int> && is_convertible_v<T2, int>]]
long long add_int(T1 t1, T2 t2) 
{
    return {t1 + t2};
}

그러나 무슨 이유에서인지 C++20에서도 Contracts는 도입되지 않는다고 합니다.

대신, 이걸 우리는 Concepts로 해결할 수 있습니다. 그나마 다행이죠.

// concept 선언. int로 변환 가능한 T는 Integer 컨셉을 만족시킨다.
template<typename T>
concept Integer = is_convertible_v<T, int>;

이렇게 하면 Integer이라는 컨셉이 생성됩니다. bool 변수에요. 동시에 하나의 타입이기도 합니다.

template<typename T1, typename T2> requires Integer<T1> && Integer<T2>
long long add_int(T1 t1, T2 t2) 
{
    return {t1 + t2}; 
}

함수 선언부입니다. 이렇게 requires 뒤에 템플릿 인자가 요구하는 컨셉을 적으면 됩니다.

template<typename T1, typename T2>
long long add_int(T1 t1, T2 t2) requires Integer<T1> && Integer<T2> 
{ 
    return {t1 + t2}; 
}

함수 머리 바로 뒤로 보내도 작동합니다, 이 경우엔 C# 제네릭의 where과 비슷한 역할을 하죠.

long long add_int(Integer auto t1, Integer auto t2) 
{ 
    return {t1 + t2}; 
}

// Concepts TS 시절에는 아래와 같은 구문을 지원했으나, 정식으론 지원하지 않습니다.
// long long add_int(Integer t1, Integer t2)

심지어는 이렇게도 가능합니다! Concepts TS에는 auto가 없어도 되는데, C++20에는 autodecltype(auto)를 반드시 사용하라고 하네요.

자, 이번엔 TS에서 나온 Concepts에 대해 알아봅시다.

#include <iostream> // 다들 아시죠?
#include <type_traits>
using namespace std;

template<typename T>
concept Integer = is_convertible_v<T, int>;

Integer{T}
T add_int(T t1, T t2) 
{
    return t1 + t2;
}

decltype(auto) getval(auto s) 
{ 
    return s;
}

int main() 
{
    [[maybe_unused]] Integer auto x = getval(3);
    cout << add_int(int{1}, int{2}) << endl;
}

이건 T라는 Integer 타입을 선언하면서, 매개변수의 타입이 같을 때에만 컴파일 해 줍니다.

main()을 바꿔볼까요.

int main() {
    auto var1 = getval(string{"HAHAHAHA"});
    cout << "var1: " << var1 << endl;
    
    auto var2 = getval(3U);
    cout << "var2: " << var2 << endl;
    
    auto var3 = getval("JAJA GGHS");
    cout << "var3: " << var3 << endl;
    
    return 0;
}

getval()은 받은 것을 그대로 반환하는 함수입니다. 출력 결과는 따라서

var1: HAHAHAHA
var2: 3
var3: JAJA GGHS

자, 그런데 auto는 아무 타입이나 다 정해버리죠. 그렇다면 Integer만 받고 싶다면 어떻게 할까?

아까 그 컨셉을 이용하면 됩니다.

auto를 모두 Integer로 바꾸어 컴파일 하면

In function 'int main()':
error: deduced initializer does not satisfy placeholder constraints
    Integer var1 = getval(string{"HAHAHAHA"});
    
note: constraints not satisfied

이야..똑똑합니다. var1과 var3를 주석처리 하겠습니다.

var2: 3

성공적으로 타입 추론이 됩니다. 이로써 auto 대신 컨셉을 통해 타입 추론도 할 수 있죠.

// C++20 이전, 타입 추론의 제한
auto var2 = getval(3U);
static_assert(is_convertible_v<decltype(var2), int>, 
   "ERROR: getval() requires integral type");

// Concepts TS / C++20에서의 타입 추론 제한
Integer var2 = getval(3U); // Concepts TS
Integer auto var2 = getval(3U); // C++20 Stnadard

윗 세 줄은 현재 사용 가능한 방법이고, 아랫줄은 Concepts TS 또는 C++20 표준안에서 사용 가능한 방법입니다.

 

Concepts에 대한 포스팅을 한 번 더 하게 된다면 requires 대해 더 자세히 알아보겠습니다.

typename 처럼 뜻이 두 개가 있거든요.

반응형