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++ 에선 classunion에만 가능하다는 점을 이용해 구현된 예시를 보여준다.

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_traitsstd::is_integral<T>::valuetrue여야만 enable_iftype이 정의가 될 것이므로 이것이 정의되는 타입인자 T만이 이 함수를 오버로딩 후보군으로 컴파일러가 평가할 수 있기 때문에 항상 Tis_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;
}

이 방법은 decltypedeclval을 이용해 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_ttypename = {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 타입인가?
};

Categories:

Updated:

Comments