Type_traits

Date:     Updated:

카테고리:

태그:

모두의 코드 씹어먹는 c++ 자료를 보고 정리한 내용입니다.


공포의 템플릿

template <class _CharT, class _Traits, class _Yp, class _Dp>
typename enable_if<
  is_same<void, typename __void_t<decltype(
                  (declval<basic_ostream<_CharT, _Traits>&>() << declval<
                     typename unique_ptr<_Yp, _Dp>::pointer>()))>::type>::value,
  basic_ostream<_CharT, _Traits>&>::type
operator<<(basic_ostream<_CharT, _Traits>& __os,
           unique_ptr<_Yp, _Dp> const& __p) {
  return __os << __p.get();
}

위 코드는 libc++ 라이브러리에서 가져온 코드로, unique_ptr의 주소값을 출력해주는 basic_ostreamoperator <<를 구현한 것이다. 도대체 왜 c++ 개발자들은 저런 혐오스러운 코드를 작성하는 것일까?

템플릿 메타 함수

템플릿 메타 함수란, 사실 함수는 아니지만 마치 함수 처럼 동작하는 템플릿 클래스들을 말한다. 이들이 메타 함수인 이유는 보통의 함수들은 값에 대해 연산을 수행하지만, 메타 함수는 타입에 대해 연산을 수행한다는 점이 조금 다르다.

예를 들어 어떤 함수가 음수인지 아닌지 판별하는 함수 is_negative는 아래와 같이 사용될 것이다.

if (is_negative(x)) {
  // Do something...
}

템플릿 메타 함수도 매우 비슷하다. 예를 들어 어떤 타입이 void 인지 아닌지 판단하는 함수 is_void는 아래와 같이 사용될 것이다.

if (is_void<T>::value) {
  // Do something
}

is_void

템플릿 메타 프로그래밍에서 if 문은 템플릿 특수화를 통해 구현된다. is_void 의 경우도 마찬가지 이다.

template <typename T>
struct is_void {
  static constexpr bool value = false;
};

template <>
struct is_void<void> {
  static constexpr bool value = true;
};

위 클래스는 일반적인 모든 타입 T에 대해 매칭되고, 아래 클래스는 void 타입에 대해 특수화 된 클래스이다.

따라서 is_void<void>를 하게 된다면, 바로 위 특수화 된 템플릿이 매칭 되어서 valueture가 되고, 그 외에 타입인 경우에는 일반적인 템플릿 클래스가 매칭되어 valuefalse가 될 것이다.

c++ 표준 라이브러리 중 하나인 type_traits 에서는 is_void 처럼 타입들에 대해 여러가지 연산을 수행할 수 있는 메타 함수들을 제공하고 있다. 한 가지 더 예를 들어보자면 정수 타입인지 확인해주는 is_intergral이 있다.

class A {};

// 정수 타입만 받는 함수
template <typename T>
void only_integer(const T& t) {
  static_assert(std::is_integral<T>::value);
  std::cout << "T is an integer \n";
}

int main() {
  int n = 3;
  only_integer(n);

  A a;
  only_integer(a);
}

[!error] test2.cc:8:3: error: static assertion failed

참고로 static_assert는 c++ 11에 추가된 키워드로(함수가 아니다), 인자로 전달된 식이 참인지 아닌지를 컴파일 타임에 확인한다. 다시 말해 bool 타입의 constexprstatic_assert로 확인할 수 있고 그 외의 경우에는 컴파일 에러가 발생한다.

만약 static_assert에 전달된 식이 참이라면, 컴파일러에 의해 해당 식은 무시되고, 거짓이라면 해당 문장에서 컴파일 오류를 발생시킨다.

따라서 static_assertstd::is_integral을 잘 조합하면 T 가 반드시 정수 타입임을 강제할 수 있다.

is_class

type_traits에 정의되어 있는 메타 함수들 중에서 흥미로운 함수로 is_class가 있다. 이 메타 함수는 인자로 전달된 타입이 클래스인지 아닌지 확인해주는 메타 함수이다.

실제 구현 코드를 보면 매우 기괴하다.

namespace detail {
template <class T>
char test(int T::*);
struct two {
  char c[2];
};
template <class T>
two test(...);
}  // namespace detail

template <class T>
struct is_class
    : std::integral_constant<bool, sizeof(detail::test<T>(0)) == 1 &&
                                     !std::is_union<T>::value> {};

[1] std::integral_constant

std::integral_constantstd::integral_constant<T, T v> 로 정의되어 있는데, 그냥 vstatic 인자로 가지는 클래스이다. 쉽게 말해 그냥 어떠한 값을 static 객체로 가지고 있는 클래스를 만들어주는 템플릿 이라고 생각하면 된다.

예를 들어 std::integral_constant<bool, false>는 그냥 integral_constant<bool, false>::valuetrue 인 클래스이다.

따라서 만약

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value

이 부분이 false 라면 is_class

template <class T>
struct is_class : std::integral_constant<bool, false> {};

로 정의되고, 따라서 is_class::valuefalse가 된다. 반면 해당 부분이 true로 연산된다면 is_class::value 역시 true가 될 것이다.

결과적으로

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value

위 코드는 T가 클래스라면 참이고, 클래스가 아니라면 거짓이 될 것이다.

그렇다면 앞 부분인 sizeof(detail::test<T>(0)) == 1은 왜 T가 클래스일 때만 1이 될까?

[2] 데이터 멤버를 가리키는 포인터 (Pointer to Data member)

template <class T>
char test(int T::*);

위 코드에서 int T::* 라는 문법은 매우 생소하겠지만, 이는 T의 int 멤버를 가리키는 포인터 라는 의미이다.

예시를 하나 보자면

class A {
 public:
  int n;

  A(int n) : n(n) {}
};

int main() {
  int A::*p_n = &A::n;

  A a(3);
  std::cout << "a.n : " << a.n << std::endl;
  std::cout << "a.*p_n : " << a.*p_n << std::endl;
}

[!result] a.n : 3 a.*p_n : 3

int A::*p_n

p_nAint 멤버를 가리킬 수 있는 포인터를 의미한다. 주소값을 넣기 전 까지는 실제 존재하는 어떠한 객체의 int 멤버를 가리키는 것이 아니다.

이와 같은 형태의 포인터를 데이터 멤버를 가리키는 포인터 라고 한다. 그리고 여기에 한 가지 제한점이 있는데, 이 문법은 클래스에만 사용할 수 있다는 것이다.

template <class T>
char test(int T::* );  // (1)

따라서 위 문장은 T가 클래스가 아니라면 불가능한 문장이다. 참고로 만약 T가 클래스라면 해당 클래스에 int 멤버가 없더라도 유효한 코드이다. 아무 것도 가리킬 수 없을 뿐이다. 하지만 어차피 여기에선 아무 상관이 없다. T가 클래스인지 아닌지만 판별하면 되니까 말이다.

struct two {
  char c[2];
};
template <class T>
two test(...);

반면 두 번째 test의 경우 T가 무엇이냐에 관계없이 항상 인스턴스화 될 수 있다.

[3] detail::test< T >(0)

자 그렇다면 T 가 클래스라고 해보자. detail::test<T>(0)을 컴파일 할 때, 컴파일러는 1번 후보인

template <class T>
char test(int T::* );  // (1)

와, 2번 후보인

struct two {
  char c[2];
};
template <class T>
two test(...);  // (2)

사이에서 어떤 것으로 오버로딩 할지 결정 해야 한다. 이 경우 1번이 좀 더 구체적이므로(인자가 명시되어 있기 때문) 우선순위가 더 높고, 결과적으로 컴파일러는 1 번으로 오버로딩 하게 된다. 따라서 detail::test<T>(0) 의 리턴 타입은 char이 되고, sizeof(char)1 이므로, sizeof(detail::test<T>(0)) == 1 은 통과될 것이다.

반면 T가 클래스가 아니라면

template <class T>
char test(int T::* );  // (1)

위 문법은 그냥 불가능한 문법이다. 이 경우 컴파일 오류가 발생되지는 않고, 오버로딩 후보군에서 제외된다. 따라서 2 번이 유일한 후보군 이므로, detail::test<T>(0) 의 리턴 타입은 two (char[2]) 가 된다. 결과적으로 sizeof 값이 2가 되므로 is_classvaluefalse가 될 것이다.

[4] !std::is_union< T >::value

참고로 c++ 에서 데이터 멤버를 가리키는 포인터가 허용되는 경우는 클래스공용체(Union) 딱 두 가지가 있다. 따라서 sizeof(detail::test<T>(0)) == 1 뿐만 아니라, T 가 공용체 아님을 추가적으로 확인해야 한다.

참고로 is_union 이 어떻게 구현되어 있는지 궁금할 것이다. 안타깝지만 c++ 에서는 클래스와 공용체를 구별할 수 있는 방법이 없다. 따라서 is_union은 컴파일러에 직접 의존하는 방식으로 구현되어 있다고 한다.

SFINAE (Substitution failure is not an error)

c++ 에서 SFINAE 원칙이란 치환 오류는 컴파일 오류가 아니다 를 뜻한다. 바로 위에서도 언급한 내용인데, 템플릿 인자 치환 후에 만들어진 식이 문법적으로 맞지 않는다면, 컴파일 오류를 발생 시키는 대신 함수의 오버로딩 후보군에서 제외만 시키게 된다.

여기서 한 가지 중요한 점은, 컴파일러가 템플릿 인자 치환 시에 함수 내용 전체가 문법적으로 올바른지 확인하는 것이 아니라는 점이다. 컴파일러는 단순히 함수의 인자들과 리턴 타입만을 문법적으로 올바른지 확인한다. 따라서 함수 내부에 문법적으로 올바르지 않은 내용이 있더라도 오버로딩 후보군에 남아 있게 된다.

template <typename T>
void test(typename T::x a) {
  typename T::y b;
}

template <typename T>
void test(typename T::y b) {
  std::cout << "T::y \n";
}

struct A {
  using x = int;
};

int main() { test<A>(11); }

[!error] test2.cc:5:17: error: no type named ‘y’ in ‘struct A’

위 코드의 경우 첫 번째 test 함수는 템플릿 인자 유추를 성공했기 때문에 오버로딩 후보군에 남아 있게 된다. 하지만 해당 함수 내부에 typename T::y 타입이 존재하지 않으므로 컴파일 오류가 발생하게 된다.

이렇게 SFINAE 원칙을 활용하게 된다면 원하지 않는 타입들에 대한 오버로딩을 후보군에서 제외할 수 있다. type_traits에는 해당 작업을 손쉽게 할 수 있는 메타 함수를 제공하는데, 대표적으로 enable_if 가 있다.



맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기