C++에서 템플릿 관련 컴파일 에러는 읽기 힘들기로 악명이 높다.
(1줄만 고치면 되는 문제인데 오류 메시지가 1000줄이 되는 바람에 터미널에서 잘려서 오류 메시지를 읽지 못하는 경험도 해봤다... -_-)
C++20에 도입된 concepts를 이용하면, 템플릿 제약조건을 만족하지 못하는 함수/클래스에 대해서 에러 메시지를 좀 더 읽기 좋게 출력하도록 만들 수 있다.
또한, 코드를 사용하는 입장에서도 어떤 제약조건을 만족해야 하는지 명시적으로 문서화되는 효과가 있다.
그러니, 한번 써보자.
클래스 템플릿이나 함수 템플릿의 매개변수를 제한하는 데 사용되는 요구사항을 정의
템플릿 매개변수로 전달된 타입에 대한 제약조건에 이름을 붙여 정의하는 것
template <parameter-list>
concept concept-name = constraints-expression;
constraints-expression
bool
로 평가될, 컴파일 타임 상수로 평가될 표현식을 여기에 적으면 된다.// `T`가 열거형인지 판별하는 `Enum` 콘셉트를 정의
template <typename T>
concept Enum = std::is_enum<T>::value;
앞에서 정의한 콘셉트를 실제로 사용한 표현식
concept-name<argument-list>
true
나 false
로 평가된다.enum class MyFruit { APPLE, BANANA, MELON };
class MyClass {};
// 앞 예시에서 정의한 `Enum` 콘셉트를 사용
static_assert(Enum<MyFruit>);
static_assert(!Enum<MyClass>);
앞서 말한 제약 표현식 자리에는 반드시 bool
로 평가될, 컴파일 타임 상수 표현식을 적어야 한다.
그런데, 요구사항을 bool
을 반환하는 표현식으로 어떻게 적을 수 있을까?
예를 들어, 어떤 클래스에 멤버 함수 unique_id()
가 존재하고, 그 멤버가 std::size_t
를 반환하는 것을 검증하는 콘셉트 UIDSizeT
를 정의하고 싶다면?
물론 C++11 식으로 <type_traits>
헤더에 존재하는 템플릿 메타프로그래밍 API를 이용해 해결할 수도 있을 것이다.
// `T`가 `unique_id()` 멤버함수를 가지며, 그 반환형은 `std::size_t`인지 검증하는 콘셉트
template <typename T>
concept UIDSizeT = std::is_same<decltype(std::declval<T>().unique_id()), std::size_t>::value;
근데... 필자만 그런지 모르겠지만, 정말 정말 보기 싫게 생겼다.
다행히 이것보다 깔끔하게 요구사항을 표현하는 요구 표현식 (requires expression)도 콘셉트와 함께 추가됐다.
요구사항을 만족하는지 여부를
bool
로 반환하는 표현식
requires (parameter-list) { requirements; }
parameter-list
: 매개변수, 생략 가능하다.requirements
: 요구사항, 여러 개 있다면 세미콜론으로 구분해 적어야 한다.requirements
에 적을 수 있는 요구사항은 단순(simple), 타입(type), 복합(compound), 중첩(nested)의 4가지로 나뉜다.
requires
로 시작하지 않는 단순한 표현식
template <typename T>
concept Addable = requires (T a, T, b) {
a + b; // 단순 표현식의 예시; a와 b 사이의 덧셈 연산이 유효한지 체크됨
};
typename
으로 해당 타입이 존재할 수 있는지 검증
template <typename T>
concept HasValueType = requires {
typename T::value_type; // 타입 요구사항: `T` 안에 `value_type`이라는 타입이 있어야 함
};
표현식이 noexcept 인지 & 표현식 타입이 type-constraint를 만족하는지 검증
{ expression } noexcept(optional) -> type-constraint(optional);
expression
: 평가될 표현식 1개만, 여기엔 세미콜론이 없다는 것에 주의하자.type-constraints
: 타입 제약 조건, 여기에 expression
의 타입이 첫번째 타입 매개변수로 자동으로 전달된다.// `T`가 `unique_id()` 멤버함수를 가지며, 그 반환형은 `std::size_t`인지 검증하는 콘셉트
template <typename T>
concept UIDSizeT = requires(T obj) {
{ obj.unique_id() } -> std::same_as<std::size_t>; // 복합 요구사항
});
요구사항 안에 요구사항을 넣고 싶을 때 사용
requires constraint-expression;
template <typename T>
concept C = requires (T t) {
requires sizeof(t) == 4; // 중첩 요구사항
++t; --t; t++; t--; // 단순 요구사항들
};
이미 존재하는 콘셉트 표현식을 &&
나 ||
로 합쳐서 쓸 수도 있다.
// Incrementable과 Decrementable 콘셉트를 합쳐 새로운 콘셉트 작성
template <typename T>
concept IncrementableAndDecrementable = Incrementable<T> && Decrementable<T>;
<concepts>
헤더에 미리 정의된 표준 콘셉트가 많으니, 적절히 활용하자.
auto
에 제약 조건 지정하기auto
쓰는 변수, 함수 리턴 타입, 축약 함수 템플릿(C++20), 제네릭 람다 표현식(C++14)에 쓸 수 있다고 한다.
Incrementable auto val1 = 1; // `int` 추론, Incrementable 하므로 OK
Incrementable auto val2 = "abc"s; // `std::string` 추론, Incrementable 하지 않으므로 컴파일 오류
이제 콘셉트를 만들었으니, 이걸 템플릿 함수/클래스에 적용해봐야 할 것이다.
전혀 어려울 것이 없다. 다만 방법이 2가지로 나뉜다.
typename
대체첫번째로, typename
대신에 concept를 쓰는 방법이 있다.
이 때, T
가 해당 concept의 첫번째 타입 매개변수로 자동으로 전달된다.
template <std::convertible_to<bool> T>
void handle(const T& obj);
둘째로, 요구 구문을 쓰는 방법이 있다.
이 때, 아래처럼 앞에 쓸 수도, 뒤에 쓸 수도 있다.
template <typename T> requires 상수_표현식
void func();
template <typename T>
void func() requires 상수_표현식;
요구 구문은 상수_표현식
이라면 뭐든 가능하므로, 꼭 concept를 쓰지 않아도 된다.
예를 들어, <type_traits>
에 있는 TMP API를 활용할 수도 있을 것이다.
template <typename T>
requires std::convertible_to<T, bool>
void handle(const T& obj);
클래스의 멤버 함수에 추가적인 제약 조건을 명시하는 것도 가능하다.
template <typename T>
class GameBoard
{
public:
void move(int xSrc, int ySrc, int xDest, int yDest) requires std::movable<T>;
}
위와 같이 명시하면, 아무 T
에 대해 GameBoard<T>
를 인스턴스화 할 수는 있겠으나,
GameBoard<T>::move()
멤버 함수는 T
가 std::movable<T>
를 만족할 때만 호출할 수 있을 것이다.
특정 타입 제약 조건을 만족하는 타입들만 클래스 템플릿 특수화/함수 템플릿 오버로딩도 할 수 있다.
// 일반적인 `T`에 대해서는 이 버전이 호출됨
template <typename T>
std::size_t find(const T& value);
// `T`가 부동 소수점인 경우 이 버전이 호출됨
template <std::floating_point T>
std::size_t find(const T& value);