3
\$\begingroup\$

I hate working with std::integer_sequence. Each time I want to write a function foo I have to write an another function foo_impl where the actual stuff is happening. Therefore I'm polluting the space with additional function and the code is kind of scattered around. I came up with an alternative and I would love to know your opinion.


Let's define integral_sequence<N>. Here we use the damned std::integer_sequence for the last time!

template <std::size_t... I>
constexpr auto integral_sequence_impl(std::index_sequence<I...>) {
  return std::make_tuple(std::integral_constant<std::size_t, I>{}...);
}

template <int N>
auto integral_sequence = integral_sequence_impl(std::make_index_sequence<N>{});

To demonstrate its use, lets redo the example code for std::integral_sequence:

// Convert array into a tuple
template <typename T, std::size_t N>
decltype(auto) a2t(const std::array<T, N> &a) {
  auto impl = [&](auto... I) { return std::make_tuple(a[I]...); };

  return std::apply(impl, integral_sequence<N>);
}

// pretty-print a tuple
template <class Ch, class Tr, class... Args>
decltype(auto) operator<<(std::basic_ostream<Ch, Tr> &os,
                          const std::tuple<Args...> & t) {

  auto impl = [&](auto... I) {
    ((os << (I == 0 ? "" : ", ") << std::get<I>(t)), ...);
  };

  os << "(";
  std::apply(impl, integral_sequence<sizeof...(Args)>);
  return os << ")";
}

int main() {
  std::array<int, 4> array = {1, 2, 3, 4};

  // convert an array into a tuple
  auto tuple = a2t(array);
  static_assert(
      std::is_same<decltype(tuple), std::tuple<int, int, int, int>>::value, "");

  // print it to cout
  std::cout << tuple << '\n';
}

The trick with integral_sequence is kind of the same as with integer_sequence. However, because it can be used with generic lambda you do not have to define an extra function, all code can be neatly together and it reduces visual clutter.

So what do you think? Are there any disadvantages?

The only problems I can see now is that it is only c++17 and integral_sequence<400> takes really long time to compile.

\$\endgroup\$
5
\$\begingroup\$

Looks good to me! I mean, I'm not likely to convert to it because of its heavyweightness — you noted its use of std::apply, but it also pulls in all of <tuple> every time you use it, too. Also, it's arguably coincidental that your expressions a[I] and std::get<I>(t) work in this context, since what's actually happening in both cases is an implicit conversion from std::integral_constant<size_t, K> to size_t. In some contexts, the user might need to explicitly cast int(I) or whatever.

You could remove the std::apply dependency and simplify the user's job at the same time by writing a helper function apply_to_sequence<N>(lambda), as in:

// Convert array into a tuple
template <typename T, std::size_t N>
auto a2t(const std::array<T, N>& a) {
  return apply_to_sequence<N>([&](auto... Is) {
      return std::make_tuple(a[Is]...);
  });
}

(Oh yeah, I think your use of decltype(auto) in this function was bogus and could result in dangling references. Certainly auto would be fine, so I recommend it.)

// pretty-print a tuple
template <class Ch, class Tr, class... Args>
auto& operator<<(std::basic_ostream<Ch, Tr>& os,
                          const std::tuple<Args...>& t)
{
  os << "(";
  apply_to_sequence<sizeof...(Args)>([&](auto... Is) {
    ((os << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
  });
  return os << ")";
}

Writing apply_to_sequence from scratch is left as an exercise for the reader, but the trivial C++17 implementation is just this:

template<size_t N, class F>
decltype(auto) apply_to_sequence(const F& fn) {
    return std::apply(fn, integral_sequence<N>);
}

Having hidden the implementation details apply and integral_sequence from the reader, we are now free — if we so desire — to replace them with things that are

  • faster
  • C++14-compatible
  • <tuple>-free

One stylistic note: I prefer to name my parameter packs with plurals, so, singular auto I but plural auto... Is. That's the logic behind my renaming above.

\$\endgroup\$
  • \$\begingroup\$ About the decltype(auto), I took the code straight from cppreference.com is the use there bogus too? Or is it specific to my code? \$\endgroup\$ – tom Apr 4 '18 at 6:25
  • \$\begingroup\$ @tom about the decltype(auto): Hmm, looks like it's non-bogus specifically because your return statement returns a prvalue, so decltype(auto) is actually equivalent to auto in this case. But I still strongly recommend that you never use decltype(auto) in C++ until you can explain the behavior of this little snippet. (Which, in practice, means: never use it, ever. ;)) — P.S. I adjusted the cppreference page. ;) \$\endgroup\$ – Quuxplusone Apr 4 '18 at 6:46
  • \$\begingroup\$ @Quuxplusone, or one can grab Effective Modern C++ :) I got a little bit confused about your comment on decltype(auto), but now I see that it applies only to your code. \$\endgroup\$ – Incomputable Apr 6 '18 at 6:45
  • \$\begingroup\$ @Incomputable: I'm sure we're both being too vague. :) When I said "your return statement returns a prvalue, so decltype(auto) is actually equivalent to auto in this case", I was talking about Tom's code i.e. cppreference's code, and the first Wandbox link was an example demonstrating that concept in isolation. The word "this" in "this little snippet" was indeed intended to refer to the second Wandbox link itself. \$\endgroup\$ – Quuxplusone Apr 6 '18 at 19:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.