Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Implementing std::tuple From The Ground Up – Part 3: Constructing Tuples

DZone's Guide to

Implementing std::tuple From The Ground Up – Part 3: Constructing Tuples

·
Free Resource

In the previous installment we were finally able to define what tuple derives from. As a reminder, if we have a tuple of n elements, it (indirectly) derives from n instantiations oftuple_element. For example, tuple<int, string> indirectly derives from tuple_element<0, int> and tuple_element<1, string>.

We’ll need to add some operations to tuple_element to make it more useful. At the very least, we need to make it constructible from its value type:

explicit tuple_element(T const& value) : value_(value) {}
explicit tuple_element(T&& value) : value_(std::move(value)) {}

Now let’s start building some fundamental operations for our tuple class so that we can get busy constructing tuples! We need to define a constructor from the tuple’s constituent types. But first, we will need 1) a default constructor, 2) copy constructor, 3) move constructor, 4) copy assignment operator, and 5) move assignment operator.

Exercise 6: Implement the five special member functions.

This is incredibly easy. Here we go:

tuple() = default;
tuple(tuple const&) = default;
tuple(tuple&&) = default;
tuple& operator=(tuple const& rhs) = default;
tuple& operator=(tuple&&) = default;

Yes, that’s right. We can live with the default compiler-generated member functions for most purposes. We will still need to implement a few additional versions of them to be more forgiving in face of type mismatches, but that will do for now. By the way, tuple_impland tuple_element are going to need the exact same declarations as well.

Now, let’s tackle the constructor that, for a tuple of n types, takes n elements and initializes the tuple. Here’s how I’d like to use it:

tuple<int, float> t1(3, 3.14f); // exact match
tuple<long long, double> t2(3, 3.14f); // widening conversions
tuple<string> t3("Hello, World"); // string construction from char const[]

It’s pretty obvious that this constructor is going to be a template. Moreover, it’s going to take universal references — we want it to support rvalues and lvalues and enable perfect forwarding. Let’s go.

using base_t = tuple_impl<typename make_index_sequence<sizeof...(Types)>::type,
                          Types...>;

template <typename... OtherTypes>
explicit tuple(OtherTypes&&... elements)
  : base_t(std::forward<OtherTypes>(elements)...)
{
}

Oh, that’s right. tuple doesn’t know anything about how its elements are stored. Therefore, it’s simply going to forward the parameters to tuple_impl, which in turn needs to forward the parameters to its tuple_element bases. Let’s tackle that first:

template <typename... OtherTypes>
explicit tuple_element(OtherTypes&&... elements)
  : tuple_impl<Indices, Types>(std::forward<OtherTypes>(elements))...
{
}

Note where the pack expansion operator () is. We want the base constructor calls of the form tuple_impl<N, T>(std::forward<U>(e)) to be expanded for each element in the parameter pack. This requires that the …Indices…Types, and …OtherTypes packs all have the same length — or it will not compile. In fact, we should probably throw in some overload management, so that these constructors aren’t even considered if the number of arguments is wrong:

template <typename... OtherTypes,
          typename = typename std::enable_if<
            sizeof...(OtherTypes) == sizeof...(Types)>::type>
explicit tuple(OtherTypes&&... elements)
  : base_t(std::forward<OtherTypes>(elements)...)
{
}
// and the same thing applies to tuple_impl's constructor

There is an incredibly dangerous thing that we have just enabled. For single-element tuples, this constructor can dangerously shadow the copy constructor. Consider the following example:

tuple<int> t1(3);  // good, construct from rvalue int
tuple<int> t2(t1); // which constructor are we calling?

Perhaps surprisingly, the second line calls the template constructor with OtherTypes = tuple<int>&. It is a better match (an exact match!) than the copy constructor, which takestuple<int> const&, and than the move constructor, which takes tuple<int>&&. The result is that we’re trying to call tuple_impl‘s constructor with OtherTypes = tuple<int>&. It in turn tries to call tuple_element<0, int>‘s constructor with tuple<int>&, and that fails splendidly. We could add a universal constructor to tuple_element as well:

template <typename U>
explicit tuple_element(U&& value) : value_(std::forward<U>(value)) {}

…but it wouldn’t really help — we are still trying to initialize tuple_element<0, int> with atuple<int>&, which means we’re trying to initialize an int (tuple_element<0, int>::value_) with a tuple<int>&.

To avoid this shadowing from happening, we need to add some overload management logic again. Specifically, we want tuple‘s universal constructor to reject tuples — but only if we’re not dealing with a tuple of tuples. What’s more, we want tuple<int> to support copy construction from tuple<short>, which is a different type:

template <typename... OtherTypes>
explicit tuple(tuple<OtherTypes...> const& rhs) : base_t(rhs) {}

template <typename... OtherTypes>
explicit tuple(tuple<OtherTypes...>&& rhs) : base_t(std::move(rhs)) {}

This seems to further complicate things, because tuple_impl‘s constructor can now be called with its “home” tuple, tuples of some other types, and naked lists of the tuple’s elements.

Here’s how we can fix this. The key problem lies within tuple_impl. It has to be able to tell other tuple_impl‘s (which represent copy/move construction) from anything else, which should be used to initialize the tuple_element‘s directly.

Exercise 7: Implement a Boolean-returning metafunction is_tuple_impl<T>, which determines whether T is a tuple_impl.

This is a simple exercise in template specialization:

template <typename>
struct is_tuple_impl
  : std::false_type {};

template <size_t... Indices, typename... Types>
struct is_tuple_impl<tuple_impl<index_sequence<Indices...>, Types...>>
  : std::true_type {};

Armed with this metafunction, we can make the universal constructor disappear fortuple_impl‘s. Essentially, we want to remove the constructor from the overload set if there is an element in the type parameter pack whose type qualifies for is_tuple_impl.

Exercise 8: Implement a Boolean-returning metafunction is_any_of<Op, …Types>, which determines whether any of the types T in the type parameter pack …Types satisfiesOp<T>::value == true. Note that Op has to be a template template parameter.

The cool thing about this exercise is that we can implement it using a constexpr function, and not another boring false_type/true_type class:

template <template <class> typename>
constexpr bool is_any_of()
{
  return false;
}

template <template <class> typename Op, typename Head, typename... Tail>
constexpr bool is_any_of()
{
  return Op<Head>::value || is_any_of<Op, Tail...>();
}

Now we can say is_any_of<is_tuple_impl, …>() with a list of types, and get a compile-time Boolean that says whether there is a tuple_impl among these types. And now it’s time for the overload management:

template <typename... OtherTypes,
          typename = typename std::enable_if<
            !is_any_of<is_tuple_impl, typename std::decay<OtherTypes>::type...>()
          >::type
         >
explicit tuple_impl(OtherTypes&&... elements)
  : tuple_impl<Indices, Types>(std::forward<OtherTypes>(elements))...
{
}

Note that we really need the decayed type. For example, if we’re called with an lvaluetuple_impl, then OtherTypes may contain a tuple_impl&, which will be rejected byis_tuple_impl unless we remove the reference qualifier. That’s what std::decay does.

Oh my. Was it worth it? Well, at this point we can construct tuples from their constituent elements *or* from other tuples that have compatible element types. We could even spray some static_assert‘s to make sure the element types are constructible from the parameter types. It’s mostly a technical exercise, so I’m not going to spend any more time on it.

So, what else is there? Well, for starters, we still can’t access the tuple’s elements after constructing it. In the next installment we’re going to implement get<>, and it’s going to be very easy after laying this foundation.

Topics:

Published at DZone with permission of Sasha Goldshtein, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}