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

January 23, 2015

tags: ,
7 comments

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 of tuple_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_impl and 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_impl(OtherTypes&&... elements)
  : tuple_element<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 takes tuple<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 a tuple<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 for tuple_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 satisfies Op<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 lvalue tuple_impl, then OtherTypes may contain a tuple_impl&, which will be rejected by is_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.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

7 comments

  1. Pingback: Implementing std::tuple - Part 4: Getting Tuple Elements

  2. Ivaylo ValchevJanuary 21, 2016 ב 7:50 PM

    I’m confused at this piece of the code:

    template
    explicit tuple_element(OtherTypes&&… elements)
    : tuple_impl(std::forward(elements))…
    {
    }

    How do we get access to Indices, which is a tupl_impl template parameter, from tuple_element? Did you perhaps switch tuple_impl and tuple_element by mistake?

    Reply
    1. Sasha Goldshtein
      Sasha GoldshteinJanuary 22, 2016 ב 3:10 PM

      Yes, thank you for spotting this. I updated the post. The tuple_impl constructor should call the tuple_element constructors, not the other way around.

      Reply
      1. Andrey PugachevMarch 29, 2017 ב 6:09 PM

        Hello Sasha!
        Can you explain me one interesting thing?

        I didn’t understand one moment.

        You said:


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

        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 to support copy construction from tuple, which is a different type:

        You add the copy ctor which takes another object of tuple type by const reference.

        This moment confused me, why it should to help us?

        I wrote simple example emulates this situation.

        template
        class TestCopyCtor
        {
        public:
        template
        TestCopyCtor(T&&…)
        {
        std::cout << "template\n";
        }

        template
        TestCopyCtor(TestCopyCtor const&)
        {
        std::cout << "copy ctor\n";
        }
        };

        And used it like this:

        int main()
        {
        TestCopyCtor t(1,2);
        TestCopyCtor t1(t);

        std::cin.get();
        }

        And I seen two strings (template template).
        Maybe we need to add two versions of copy ctor?

        Reply
        1. Andrey PugachevMarch 29, 2017 ב 6:15 PM

          I’m sorry, I think that this form for comments filters text.
          And it deleted part of template in triangular brackets.

          Therefore I suggest to look here to my example: http://rextester.com/FXFQX6618

          Reply
          1. Sasha Goldshtein
            Sasha GoldshteinMarch 30, 2017 ב 2:45 PM

            Sorry, not sure what you’re referring to. The way to avoid the problem is by adding enable_if to disable the T&&… constructor when one of the types T is tuple_impl.

  3. kosmoApril 28, 2016 ב 4:53 AM

    It may be a stupid question, but the variadic template constructor clashes with default constructor, and compiler will generate error message for the simple class below.

    template
    class T
    {
    public:
    T() = default;
    T(const T&) = default;
    T& operator=(const T&) = default;

    template
    explicit T(Types&&... values)
    {
    int temp = 0;
    }
    };
    T tt;

    multiple versions of a defaulted special member functions are not allowed.

    Reply