광고


[C++14] std::integer_sequence TR1/C++11/C++14/C++17


1. 기본적인 syntax 및 설명

std::integer_sequence는 C++14에 추가된 traits 템플릿으로써, 컴파일 타임에 정수 시퀀스를 표현할 수 있게 해준다.
특히, tuple 또는 tuple을 핸들링 하는 코드들에 자주 등장하는데, 인수로 함수에 전달되는 variadic 형식의 parameter pack을 추론하고 확장하는데 사용된다.

  1. template <typename T, T... Vals>
  2. class integer_sequence;

T는 bool, char, char16_t, char32_t, wchar_t 그리고, signed or unsigned 정수 등 정수 계열 형식이어야 한다.
... Vals는 (정수 계열 형식) T 값의 시퀀스(0, 1, 2, 3, ... N)를 나타내는 parameter pack이다.


2. Implementation

VS2017 (15.5.2)에 구현되어 있는 std::integer_sequence는 다음과 같다.

  1. // STRUCT TEMPLATE integer_sequence
  2. template <typename T, T... Vals>
  3. struct integer_sequence
  4. {
  5.     // T는 반드시 정수 계열 형식이어야 한다
  6.     static_assert(
  7.         is_integral_v<T>,
  8.         "integer_sequence<T, I...> requires T to be an integral type"
  9.     );
  10.  
  11.     using value_type = T;
  12.  
  13.     // get length of parameter list
  14.     static constexpr size_t size() noexcept
  15.     {  
  16.         return (sizeof...(Vals));
  17.     }
  18. };

구현에서도 보듯이 T는 정수 계열 형식이어야 한다. 아닐 경우 static_assert에 걸려 컴파일 에러가 발생한다.
그리고, 유일한 멤버 함수인 size()는 Vals parameter pack의 parameter 개수를 반환한다.


3. Helper templates

STL이나, 여타 traits 템플릿 들을 살펴보면, std::integer_sequence를 그대로 쓰는 경우는 잘 없다.
대신 미리 만들어진 Helper template들을 많이 사용하는데 하나씩 살펴보도록 하자.

1) std::index_sequence

  1. template <size_t... Vals>
  2. using index_sequence = integer_sequence<size_t, Vals...>;

T = size_t로 한정한 std::integer_sequence의 alias template이다.

앞서, std::integer_sequence가 tuple 핸들링에 자주 쓰인다고 했는데, 그 때의 sequence 의미는 index에 가깝다.
이런 경우가 많다보니, 아예 별도로 std::index_sequence라는 alias를 만들어 둔 것 같다.

...Vals의 parameter 개수가 4인 경우 (0, 1, 2, 3)의 size_t sequence가 인자로 넘어간다.
이 sequence를 인자로 받는 함수는 다음의 형태를 가져야 한다.

  1. // template argument와 function parameter 형식 참고
  2. template <std::size_t... Idx>
  3. return-type function(std::index_sequence<Idx...>)
  4. {
  5.     return return-type;
  6. }

2) std::make_integer_sequence

  1. // ALIAS TEMPLATE make_integer_sequence
  2. template <typename T, T Size>
  3. using make_integer_sequence = __make_integer_seq<integer_sequence, T, Size>;

std::make_integer_sequence는 std::integer_sequence를 좀 더 편하게 생성시키기 위해 만들어진 alias template 이다.
__make_integer_seq의 내부 코드를 들여다 볼 수 없지만, 그것의 반환값이 std::integer_sequence임은 틀림 없다.

참고)
Size가 음수이면, ill-formed 예외가 발생한다. 
또한 Size가 0이면, 반환값은 std:;integer_sequence가 아닌 std::index_sequence가 된다.

3) std::make_index_sequence

  1. template <size_t Size>
  2. using make_index_sequence = make_integer_sequence<size_t, Size>;

T = size_t로 한정한 std::make_integer_sequence의 alias template이다.
반환값은 std::index_sequence 이다.

참고 : Size가 음수이면, ill-formed 예외가 발생한다.

4) std::index_sequence_for

  1. template <typename... Vals>
  2. using index_sequence_for = make_index_sequence<sizeof...(Vals)>;

특정 정수 계열의 parameter pack을, std::index_sequence로 변환하기 위한 alias template이다.
(당연하게도 parameter pack의 size는 동일하다)


4. Examples #1

다음은 cppreference.com의 예제 중 #1을 그대로 가져온 것이다.

  1. #include <tuple>
  2. #include <array>
  3.  
  4. /////////////////////////////////////////////////////////////////////////////////////////
  5. // Convert array into a tuple
  6. // std::make_index_sequence로 넘어온 인자를 받기 위해,
  7. //  template argument로 <std::size_t... Idx>
  8. //  function parameter로 (std::index_sequence<Idx...>)를 받은 부분 주목
  9. /////////////////////////////////////////////////////////////////////////////////////////
  10. template <typename Array, std::size_t... Idx>
  11. decltype(auto) array_to_tuple_impl(const Array& a, std::index_sequence<Idx...>)
  12. {
  13.     return std::make_tuple(a[Idx]...);
  14. }
  15.  
  16. template <typename T, std::size_t N>
  17. decltype(auto) array_to_tuple(const std::array<T, N>& a)
  18. {
  19.     return array_to_tuple_impl(a, std::make_index_sequence<N>());
  20. }
  21.  
  22. /////////////////////////////////////////////////////////////////////////////////////////
  23. int main()
  24. {
  25.     // convert an array into a tuple    
  26.     std::array<int4> array = { 1234 };

  27.     auto tuple = array_to_tuple(array);

  28.     // check type of converted to tuple
  29.     static_assert(std::is_same<decltype(tuple), std::tuple<intintintint>>::value);
  30.  
  31.     return 0;
  32. }

위 예제를 실행하고 13라인에 중단점을 찍어보면, 13라인에서의 콜스택은 다음과 같다.

  1. /*
  2.     template argument:
  3.     std::size_t... Idx ==> 0, 1, 2, 3으로 확장(unpack)
  4.  
  5.     function parameter:
  6.     std::index_sequence<Idx...> ==> std::integer<size_t, 0, 1, 2, 3>으로 unpack
  7. */
  8. array_to_tuple_impl<std::array<int4>0123>(
  9.     const std::array<int4>& a, std::integer_sequence<size_T ,0123> __formal
  10. )

최종적으로 13라인은 다음과 같이 std::make_tuple을 실행하는 것과 동일한 의미를 가지게 된다.

  1. decltype(auto) array_to_tuple_impl<std::array<int4>0123>(
  2.     const std::array<int4>& a, std::integer_sequence<size_T ,0123> __formal
  3. )
  4. {
  5.     // Line 13
  6.     return std::make_tuple(a[0], a[1], a[2], a[3]);
  7. }
  8.  
  9. // 위 함수 호출의 결과는 다음과 같이 컴파일러에 의해 해석된다.
  10. std::make_tuple<int const& __ptr64, int const& __ptr64, int const& __ptr64, int const& __ptr64>(
  11.     const int& <_Args_0>const int& <_Args_1>const int& <_Args_2>const int& <_Args_3>
  12. )
  13. {
  14.     // ... tuple 생성 로직 ...
  15. }
  16.  
  17. /*
  18.     a[0] == <_Args_0>,
  19.     a[1] == <_Args_1>,
  20.     a[2] == <_Args_2>,
  21.     a[3] == <_Args_3>,
  22. */


5. Examples #2

다음은 cppreference.com의 예제 중 #2을 그대로 가져온 것이다.

  1. #include <tuple>
  2. #include <iostream>
  3.  
  4. /////////////////////////////////////////////////////////////////////////////////////////
  5. // pretty-print a tuple
  6. // std::index_sequence_for -> std::make_index_sequence로 넘어온 인자를 받기 위해,
  7. //  template argument로 <std::size_t... Idx>
  8. //  function parameter로 (std::index_sequence<Idx...>)를 받은 부분 주목
  9. /////////////////////////////////////////////////////////////////////////////////////////
  10. template <typename Ch, typename Tr, typename Tuple, std::size_t... Idx>
  11. decltype(auto) print_tuple_impl(
  12.     std::basic_ostream<Ch, Tr>& os, const Tuple& t, std::index_sequence<Idx...>
  13. )
  14. {
  15.     return ((os << (Idx == 0 ? "" : ", ") << std::get<Idx>(t)), ...);
  16. }
  17.  
  18. template <typename Ch, typename Tr, typename... Args>
  19. decltype(auto) operator << (
  20.     std::basic_ostream<Ch, Tr>& os, const std::tuple<Args...>& t
  21. )
  22. {
  23.     return print_tuple_impl(os, t, std::index_sequence_for<Args...>());
  24. }
  25.  
  26. /////////////////////////////////////////////////////////////////////////////////////////
  27. int main()
  28. {
  29.     // print it to cout
  30.     auto tuple = std::make_tuple(1234);
  31.     std::cout << tuple << '\n';
  32.  
  33.     return 0;
  34. }

예제 1을 이해했다면, 이것도 이해하는 데 크게 무리는 없다.
std::index_sequence_for<Args...>()std::make_index_sequence<sizeof...(Args)>()동일 문장이므로...

다만, 15라인에서 std::get<Idx>(t)), ... 이 부분이 어떻게 동작하는지는 다음의 콜스택을 살펴보면 이해가 쉬울 것이다.
(모든 것을 다 적진 않았으니, 개략 흐름을 이해하는 차원에서 보길 바란다)

  1. // _Val = ","
  2. std::operator << <std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>>& _Ostr, const char* _Val)
  3. // Idx = 3, std::get<3>(_Tuple) = 4
  4. std::get<3,int,int,int,int>(const std::tuple<int,int,int,int>& _Tuple)
  5.  
  6. // _Val = ","
  7. std::operator << <std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>>& _Ostr, const char* _Val)
  8. // Idx = 2, std::get<2>(_Tuple) = 3
  9. std::get<2,int,int,int,int>(const std::tuple<int,int,int,int>& _Tuple)
  10.  
  11. // _Val = ","
  12. std::operator << <std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>>& _Ostr, const char* _Val)
  13. // Idx = 1, std::get<1>(_Tuple) = 2
  14. std::get<1,int,int,int,int>(const std::tuple<int,int,int,int>& _Tuple)
  15.  
  16. // _Val = ""
  17. std::operator << <std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>>& _Ostr, const char* _Val)
  18. // Idx = 0, std::get<0>(_Tuple) = 1
  19. std::get<0,int,int,int,int>(const std::tuple<int,int,int,int>& _Tuple)

헌데, 아직도 15라인의 문법이 생소할 수 있다.
아마도 여기에 C++17의 "Fold expressions"가 사용되었기 때문일 것이다.
다시 한번 15라인 부근만 가져와서 살펴보자.

  1. // ...
  2. {
  3.     // 조금 더 이해를 쉽게 하기 위해, 억지로 행을 나누어 보았다.
  4.     // 즉, 아래 반환문은 comma(,) operator 를 통한 parameter unpack을 수행한다.
  5.     return (
  6.         (os << (Idx == 0 ? "" : ", ") << std::get<Idx>(t))...
  7.     );
  8. }

Fold expressions은 특정 operator를 통한 parameter unpack을 지원하며, 위에서는 comma(,) operator가 사용되었다.

이 글은 std::integer_sequence에 대한 글이므로, 이 부분에 대해 상세하게 작성하긴 어렵다.
대신 fold expressions에 대해 조금 더 자세히 알고 싶다면 다음 글을 참고하기 바란다.


1 2 3 4 5 6 7 8 9 10 다음