본문 바로가기
Computer Science/C++

[C++] TMP로 컨테이너 내부 타입 정보 얻어오기 [템플릿 메타프로그래밍]

by invrtd.h 2022. 10. 22.

 

 Motivation

 

 제네릭 프로그래밍을 하고 싶다고 하자. std::vector<T>와 같은 컨테이너 내부의 타입이 몇 바이트인지 확인할 일이 생겼다고 가정해 보자. 컨테이너로 std::vector를 사용한다는 사실을 미리 알고 있다면, 다음과 같이 짜는 것으로 충분하다.

 

sizeof(typename std::vector<T>::value_type)

 사실은 이렇게 할 필요도 없으며, 이걸로도 충분하다.

 

sizeof(T)

 

 문제는 우리가 컨테이너로 std::vector가 들어온다는 사실을 모를 때 발생한다. 이렇게 가정해보자. "어떤 타입 X가 들어오는데, 우리는 X가 std::vector<T> 꼴인지 (some custom container)<T> 꼴인지도 모르고 사실 컨테이너인지도 모른다." 이럴 때는 다음의 해결 방법이 우리에게 도움을 주는 것이 별로 없다. 위에서 std::vector<T>::value_type 구문이 가능했던 이유는 C++ 표준에서 std::vector 내부에 typedef T value_type을 적어놓을 것을 강제했기 때문이다. 모든 커스텀 컨테이너가 std::vector처럼 친절하다고 믿을 수는 없다.

 

 1단계

 

 아무 타입이나 개수 제한 없이 받는 어떤 빈 구조체를 생각해 보자.

 

template<unsigned int N, class T = void, class ...Tp>
struct impl {
    using type = /* ??? */
}

 

 이 type이 뭐였으면 좋겠냐면, 받아 놓은 모든 타입들 중 N번째 타입과 같은 타입이었으면 좋겠다.

 

typename impl<2, int, int, double>::type // double
typename impl<0, std::vector<int>, double, char>::type // std::vector<int>
typename impl<1, int, char, std::optional<char>>::type // char

 

 TMP를 할 때에는 for문을 돌리지 못하므로, 문제 상황을 recursive하게 처리할 필요가 있다. 문제 상황을 점화식으로 적으면,

 

 impl<0, T, Tp...>::type == T // base case

 impl<N, T, Tp...>::type == impl<N-1, Tp...>::type // recursive step

 

 이걸 그대로 코드로 구현해주자.

 

template<unsigned int N, class T = void, class ...Tp>
struct impl {
    using type = impl<N - 1, Tp...>::type;
}

template<class T = void, class ...Tp>
struct impl<0, T, Tp...> {
    using type = T;
}

template<unsigned int N, class T = void, class ...Tp>
using nth_among = typename impl<N, T, Tp...>::type;

 직접 해 보면 잘 작동한다는 사실을 알 수 있다. 이제 우리는 주어진 typename 파라미터들 중에서 정확히 N번째 원소에 접근할 수 있게 되었다! 그러나 우리가 원하는 것은 이게 아니다. 임의의 컨테이너 T에 대해서 T의 value_type에 접근하는 것이다. 사실 지금까지 우리가 한 게 여기에 도움이 되는지도 잘 모르겠다... 하지만 이제 곧 이것을 사용하게 될 때가 올 것이다.

 

 2단계

 

 목적을 달성하기 위해, 함수가 자동으로 템플릿 파라미터를 추론해 주는 기능을 사용할 것이다. 우리는 대개 이 기능을 사용해 본 적이 있다.

 

std::abs(1) // 1 (int)
std::abs(-1.7) // -1.7 (double)

 임의의 std::vector을 받아서 출력해주는 함수를 생각해보자. 템플릿을 사용해 다음과 같이 해결할 수 있을 것이다.

 

template<typename T>
void print(const std::vector<T> &vec) noexcept;

 임의의 std::vector<T>를 입력으로 받는 함수를 템플릿으로 짤 수 있다면, 임의의 container<int>를 받는 함수도 짤 수 있을까? 그렇게 할 수 있다.

 

template<template<class, class...> class C>
void print(const C<int> &cont) noexcept;

 

 그리고 이걸 조합하면? 놀랍게도 모든 컨테이너를 입력으로 받는 함수를 템플릿으로 짤 수 있다는 뜻이다.

 

template<template<class, class...> class C, typename ...Tp>
void print(const C<Tp...> &cont) noexcept;

 

 마지막이다. 만약 print의 리턴 타입이 void가 아니라 T라면? print 함수를 호출한 뒤 그 값을 decltype 처리하면 그 타입이 정확히 T가 된다는 뜻이다! 즉 다음과 같이 조합하면 임의의 컨테이너의 N번째 타입에 접근할 수 있다.

 

template<unsigned int N, class T = void, class ...Tp>
struct impl {
    using type = impl<N - 1, Tp...>::type;
}

template<class T = void, class ...Tp>
struct impl<0, T, Tp...> {
    using type = T;
}

template<unsigned int N, class T = void, class ...Tp>
using nth_among = typename impl<N, T, Tp...>::type;

template<unsigned int N>
struct impl2 {
    template<template<class, class...> class C, typename ...Tp>
    static auto print(const C<Tp...> &cont) noexcept -> nth_among<N, Tp...>;
}

template<unsigned int N, class X>
using nth_value_type = decltype(impl2<N>::print(std::declval<X>()));

 

nth_value_type<1, std::pair<int, double>> == double
nth_value_type<0, std::vector<int>> == int

댓글