get<N>(tuple) considered harmful

I have been toying around with a custom implementation of tuple and came across this pitfall. Suppose you need a tool that would provide a tuple that would be the tail of a given tuple. What’s a tuple? It can be std::tuple or it can be mytuple, so the tool should best be dependent on the tuple’s template template together with the tuple’s type pack:

    template<template <class...> class Tuple, class ArgHead, class... ArgTail>
auto tuple_tail(Tuple<ArgHead,ArgTail...>& t) -> Tuple<ArgTail...>;

The implementation must construct a tuple with pack expansion involving only the tail arguments. Types are easy, but to get the values from t we need an index sequence going from 1 to sizeof...(T)-1. It is straightforward to get a sequence like that with the use of std::index_sequence_for, but then it starts at 0, that we need to drop. We then send the index sequence together with the tuple t to a helper function that does the actual job. All of that should be fairly straightforward c++ for anybody with a couple of years of experience (did I say a couple? I meant a couple of dozen, of course…):

    template<class I>
struct drop_first;

    template<std::size_t IdxHead, std::size_t... IdxTail>
struct drop_first<std::integer_sequence<std::size_t, IdxHead, IdxTail...>>
  {
  using type = std::integer_sequence<std::size_t, IdxTail...>;
  };

    template<class I>
using drop_first_t = typename drop_first<I>::type;

    template
      <
      std::size_t... Idx, 
      template <class...> class Tuple, 
      class ArgHead, 
      class... ArgTail
      >
auto tuple_tail_impl
    (
    std::integer_sequence<std::size_t,Idx...>,
    Tuple<ArgHead,ArgTail...>& t
    ) -> auto
  {
  return Tuple<ArgTail...>(std::forward<ArgTail>(get<Idx>(t))...);
  }

    template<template <class...> class Tuple, class ArgHead, class... ArgTail>
auto tuple_tail(Tuple<ArgHead,ArgTail...>& t) -> Tuple<ArgTail...>
{
    return tuple_tail_impl
             (
             drop_first_t<std::index_sequence_for<ArgHead,ArgTail...>>{},
             t
             );
    }

Straightforward, uh? Doesn’t compile? Unhelpful compiler messages? “error: 'get' was not declared in this scope” says gcc, “error: use of undeclared identifier 'get'” do we get from clang. Sure, it’s a dependent template function to be looked up via ADL at instantiation time. Or is it?

I managed to get more descriptive error messages by inserting more template arguments: get<Idx,Tuple<ArgHead,ArgTail...>>(t) should be more or less equivalent, but now clang says that after Tuple it “expected ‘(‘ for function-style cast or type construction”, and gcc complains about no primary-expression before ‘>’. Gcc’s message is more understandable (!), and means that the compiler was not expecting a type here, but a value, and this can only happen if the comma after Idx is an actual comma operator.

As it stands, the compiler has no reason to believe that get is a dependent template function. To it, get looks like an autonomous identifier, so the line gets parsed as get < Idx, that is, get less than Idx, and there is no non-dependent get around to have it compared.

Is all hope lost in this case? Fortunately not, but at the cost of an ugly hack. When the compiler sees our line using get, it must know, that it is a template, then it will consider it dependent. But we’re writing generic code, so we do not have get ready for use. And including the tuple header and writing using std::get does not seem to be a viable option, first, it is not generic, second, if by any chance std::get is selected as your overload at instantiation time, you’re in for trouble, as it has no chance of performing, contrary to, say, std::swap. In fact, you should be pretty sure that the template declaration for get is so blatantly wrong for the purpose, that does not get selected in any case. It’s necessary only to turn get into a dependent symbol. So, how about that:

    template<class I>
struct drop_first;
    template<std::size_t IdxHead, std::size_t... IdxTail>
struct drop_first<std::integer_sequence<std::size_t, IdxHead, IdxTail...>>
  {
  using type = std::integer_sequence<std::size_t, IdxTail...>;
  };
    template<class I>
using drop_first_t = typename drop_first<I>::type;

namespace detail
  {
      template<class>
  auto get(void)->void;

      template
        <
        std::size_t... Idx, 
        template <class...> class Tuple, 
        class ArgHead, 
        class... ArgTail
        >
  auto tuple_tail_impl
      (
      std::integer_sequence<std::size_t,Idx...>,
      Tuple<ArgHead,ArgTail...>& t
      ) -> auto
    {
    return Tuple<ArgTail...>(std::forward<ArgTail>(get<Idx>(t))...);
    }
  }

    template<template <class...> class Tuple, class ArgHead, class... ArgTail>
auto tuple_tail(Tuple<ArgHead,ArgTail...>& t) -> Tuple<ArgTail...>
  {
  return detail::tuple_tail_impl
           (
           drop_first_t<std::index_sequence_for<ArgHead,ArgTail...>>{},
           t
           );
  }

I never liked the idea of having standalone functions performing private tasks, now I have one more concrete reason. I believe the above complication is a high price to pay for the coolness of the syntax get<0>(t) as compared to t.get<0>(), which, in our template use, would in addition need to be decorated with the template keyword: t.template get<Idx>().

If you asked, there is more to come about tuples and pairs, one of the things is observable above, with the use of a non-const reference to the tuple. Const with std::tuple is messy, but that’s for another post, so is the interoperability between std::tuple and std::pair.

Advertisements

2 Responses to get<N>(tuple) considered harmful

  1. Antoniy says:

    It should be std::get in
    return Tuple(std::forward(get(t))…);

    • ljwo says:

      Thanks for the comment, but I cannot agree. If I do std::get then it will not work for the custom mytuple. Also, until c++17, you need the <ArgTail...> spec after Tuple.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: