C++ 문법 정리 - 7 type_traits, SFINAE, enable_if, concept
1편, 2편, 3편, 4편, 5편, 6편에서 이어집니다.
문법 정리
type_traits
type_traits
에서는 is_void
같이 if(std::is_void<T>::value)
처럼 쓸 수 있는 여러 템플릿 메타 함수들을 제공한다.
여기서 튜토리얼에서 std::is_class
에 대한 흥미로운 예시를 소개하는데, int T::*
라는 T
타입의 인트 멤버를 가리키는 포인터라는 문법도 새롭게 소개하고 이러한 문법이 c++
에선 class
와 union
에만 가능하다는 점을 이용해 구현된 예시를 보여준다.
static_assert
static_assert(std::is_integral<T>::value)
와 같이 컴파일 타임에 assertion을 걸 수 있다.
SFINAE(Substitution Failure Is Not An Error)
치환 실패는 오류가 아니라는 원칙이 있다.
오버로딩을 할 때 원하지 않는 후보들이 오버로딩 후보군에서 제외되게 하기위해서 enable_if
를 사용한다.
즉, c++에서 템플릿 타입을 제한하는 방식은 그것을 TMP를 통해 오버로딩 후보군에서 제외하는 방식으로 구현된다는 것이 핵심이다.
enable_if
다음과 같이 정의되어있다.
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { typedef T type; };
첫 템플릿 인자가 true
라면 T
타입의 type
이라는 타입을 enable_if
내부에 가지게 컴파일된다.
enable_if
로 타입을 제한하는법
그리고 다음과 같이 enable_if<...>::type
을 써서 타입을 유추하게하여 오버로딩 후보군에서 제외하는 테크닉을 쓸 수 있다.
template <typename T,
typename = typename std::enable_if<std::is_integral<T>::value>::type>
void test(const T& t) {
std::cout << "t : " << t << std::endl;
}
type_traits
의 std::is_integral<T>::value
가 true
여야만 enable_if
의 type
이 정의가 될 것이므로 이것이 정의되는 타입인자 T
만이 이 함수를 오버로딩 후보군으로 컴파일러가 평가할 수 있기 때문에 항상 T
는 is_integral<T>::value
가 참이라는 것이 보장된다.
위 식에서 typename = typename ...
같은 부분은 기본값을 설정하는 문법을 써서 사용하는 곳에서 두 번째에 타입을 안넘겨도 상관없게 하는 문법이며 컨벤션중 하나이다. 뒤에 = nullptr
을 붙이는 방법도 있지만 위의 방법이 선호되곤 한다.
우리가 std::is_integral_v
가 ::value
를 가져오는 단축문법이듯이, std::is_enable_t
는 ::type
에 대한 단축문법이므로 그것으로 써도 무방하다. c++17 부터 가능하다.
decltype
으로 멤버함수의 존재성을 제한하는법
template <typename T, typename = decltype(std::declval<T>().func())>
void test(const T& t) {
std::cout << "t.func() : " << t.func() << std::endl;
}
이 방법은 decltype
과 declval
을 이용해 func
라는 멤버 함수가 있는 타입 T
만을 제한하는 방법이다.
더 재미있는 것은 이런식으로 func
함수의 리턴값이 int
인 것까지 제한을 할 수 있다는 것이다.
와 이거 그냥 Typescript아니냐?
template<typename T,
typename = std::enable_if_t<std::is_integral_v<decltype(declval<T>().func())>>>
int fn(T t) {
return t.func();
}
struct A {
public:
int func() {
return 1;
}
};
int main() {
A a;
cout << fn(a);
return 0;
}
더 복잡한 예시는 첫 인자는 int
, 두 번째 인자는 std::string
, 반환값은 int
인 것도 만들 수 있다.
template<typename T,
typename = std::enable_if_t<std::is_integral_v<decltype(declval<T>().func(
declval<int>(),
declval<std::string>()))>>>
int fn(T t) {
return t.func(1, "");
}
struct A {
public:
int func(int k, std::string v) {
return 1;
}
};
int main() {
A a;
cout << fn(a);
return 0;
}
void_t
void_t
는 typename = {std::enable_if, decltype}
가 너무 많아지는 것을 방지하기 위해 정의된 템플릿으로 전달된 타입 인자들 중 하나라도 올바르지 않은게 있을 경우 오버로딩 후보군에서 제외될 수 있도록 해주는 유틸리티이다.
template <typename Cont, typename = decltype(std::declval<Cont>().begin()),
typename = decltype(std::declval<Cont>().end())>
를 다음과 같이 정리할 수 있다.
template <typename Cont,
typename = std::void_t<decltype(std::declval<Cont>().begin()),
decltype(std::declval<Cont>().end())>>
그런데 여기 템플릿의 두 번째 인자로 명시적으로 void
를 전달할 경우 저 default 타입이 검사되지 않아서 그냥 컴파일이 되버릴 수도 있는데, 따라서 라이브러리 제작자들은 타입검사하는 부분을 void
이기 때문에 함수의 반환값이 없다면 그냥 저걸 함수의 반환값으로 설정해버리거나 private한 진짜 함수를 따로 만들어버릴 수 있다.
template <typename Cont>
std::void_t<decltype(std::declval<Cont>().begin()),
decltype(std::declval<Cont>().end())>
print(const Cont& container)
concept
c++20의 concept
는 템플릿 코드의 타입 제한의 가독성을 높이는 키워드이다.
쉽게 말해 타입 인자에 대한 제약을 가지는 타입(?)을 정의한다.
template<typename T>
concept Integral = std::is_integral_v<T>;
template<Integral... T>
int add(T... a) {
return (a + ...);
}
이렇게 Integral
을 정의하고 T
가 항상 std::is_integral<T>::value
가 참임을 나타낸다.
또한 concept
를 정의해서 쓰는것이 아닌 인라인으로 바로 사용할 수도 있다.
template <typename T>
requires std::is_integral_v<T>
T add(T a, T b) {
return a + b;
}
requires
키워드는 concept
에서 같이 쓰이는 키워드인데, 요구사항을 의미한다.
만약 iterator
처럼 begin()
, end()
가 T::iterator
를 반환하게 하고싶다면 다음과 같이 가능하다.
template<typename T>
concept HasBeginEnd = requires(T t) {
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
};
same_as<T>
는 is_same<T>::value
와 다르게 requires
블록 내부에서 값이 같은지 확인하는 유틸리티이다.
또한 사기적으로 arithmetic operator나 logical operator도 사용하여 타입 검사도 가능하다.
template<typename T>
concept Arithmetic = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // a, b의 덧셈 결과가 T 타입인가?
{ a - b } -> std::same_as<T>; // a, b의 뺄셈 결과가 T 타입인가?
{ a / b } -> std::same_as<T>; // a, b의 나눗셈 결과가 T 타입인가?
};
template<typename T>
concept Boolean = requires(T a, T b) {
{ a || b } -> std::same_as<bool>; // a, b의 논리합 결과가 bool 타입인가?
{ a && b } -> std::same_as<bool>; // a, b의 논리곱 결과가 bool 타입인가?
};
Comments