Skip to main content
  1. Posts/

Piecewise Tuple

·3 mins

Motivation #

Why a piecewise tuple? It may be that some of the types that in our tuple do not have a default constructor, or perhaps share some sort of common constructor and we want to initialise them all the same way.

In addition, std::pair which is really just a tuple of two types, has a piecewise constructor.

Requirements #

We have a few requirements:

  • the existing piecewise constructor for std::pair uses tag-dispatch, we will use the same idea.
  • reuse existing standard library functions, such as std::get<> to access the tuple

Getting started #

We want to write this:

piecewise_tuple<Foo, Bar, Void> t { std::piecewise_construct, 
                                    std::forward_as_tuple{fooArg1, fooArg2}, 
                                    std::forward_as_tuple{barArg1, barArg2}, 
                                    std::tuple{} };

We will inherit from std::tuple, and create a new constructor similar to that of std::pair:

template <typename... Elements>
class piecewise_tuple : public std::tuple<Elements...> { 
    using base_tuple = std::tuple<Elements...>;

public:
    template <typename... TupleArgs>
    explicit constexpr piecewise_tuple(std::piecewise_construct_t, TupleArgs&&... args) { /*elided*/ }

    // elided
 };

How do we proceed from here?

Tuples all the way down. #

A tuple is a list of types. So let’s use more tuples. We need to pass those args so let’s make a tuple of tuples. And since we’re going to need to unpack the tuple, we will pass an integer sequence to assist with look up and unpacking:

    template <typename... TupleArgs>
    explicit constexpr piecewise_tuple(std::piecewise_construct_t, TupleArgs&&... args)
        : piecewise_tuple(
            std::make_index_sequence<sizeof...(TupleArgs)>{},
            std::forward_as_tuple(std::forward<TupleArgs>(args)...)
        ) {
        static_assert(sizeof...(TupleArgs) == sizeof...(Elements), 
            "Number of tuple arguments should equal number of tuple elements");
    }
It is a common practice in template programming to use tuples (lists), and index sequences to create algorithms over types.

Delegate #

Now we delegate to another constructor:

    template <std::size_t... N, typename ArgTuple>
    explicit constexpr piecewise_tuple(std::index_sequence<N...>, ArgTuple&& argTuple)
        : base_tuple(
            construct<N>(
                std::get<N>(std::forward<ArgTuple>(argTuple)),
                std::make_index_sequence<
                    std::tuple_size_v<
                        std::decay_t<std::tuple_element_t<N, std::decay_t<ArgTuple>>>
                    >
                >{}
            )...
        ) {
    }

and use pack expansion to “iterate” over the (piecewise) tuple, using the index sequence we passed in, and construct each type in the piecewise tuple.

One small wrinkle is that we need to generate another index sequence for the arguments used to construct the type.

Since we’re using perfect fowarding that preserves any qualifiers, we need to decay or remove_reference on the type, and thus prevent 'error: incomplete type ... used in nested name specifier' in gcc.

Construction #

Since the arguments for each type in the piecewise tuple are themselves in a tuple, we’ll need to unpack that using std::get<N>(tuple):

    template <std::size_t Index, typename... Args, std::size_t... N>
    static constexpr auto construct(std::tuple<Args...>&& args, std::index_sequence<N...> /*unused*/ ) {
        // Look up the Index-th type in the piecewise tuple
        using result_type = std::tuple_element_t<Index, base_tuple>;

        // finally, pass the args into the type (of the tuple element) for construction
        // pack expansion of the argument tuple

        return result_type(std::get<N>(std::move(args))...);  // Construct the type!!!!
    }

This is notionally doing the equivalent of:

auto foo = Foo(fooArg1, fooArg2);
return foo;

And we’re done #

We now have a piecewise tuple that can be constructed from the given set of arguments.

Where do we go from here? We can, using tag-dispatch, add another constructor to the piecewise tuple that can pass one set of arguments to all types in the tuple if they share the same constructor arguments.

Full version is here.