Why is there no piecewise tuple construction?
Asked Answered
C

3

43

The standard templates std::pair and std::array are special cases of std::tuple, and it stands to reason that they should have a very similar set of capabilities.

However, uniquely among the three, std::pair allows for piecewise construction. That is, if the types T1 and T2 can be constructed from a set of arguments a1, a2, ... and b1, b2, ..., then morally speaking we can make a pair

"pair<T1, T2> p(a1, a2, ..., b1, b2, ...)"

directly. Practically, this is spelt out as something like this:

std::pair<T1, T2> p(std::piecewise_construct,
                    std::forward_as_tuple(a1, a2, ...),
                    std::forward_as_tuple(b1, b2, ...));

Question: Why doesn't the same piecewise constructibility exist for arrays and tuples? Is there a profound reason, or is this a plain omission? For example, it would be nice to have:

std::tuple<T1, T2, T3> t(std::piecewise_construct,
                         std::forward_as_tuple(a1, a2, ...),
                         std::forward_as_tuple(b1, b2, ...),
                         std::forward_as_tuple(c1, c2, ...));

Is there a reason this cannot be done? [Edit: Or am I misunderstanding the purpose of piecewise construction entirely?]

(I do really have a situation in which I would like to initialize a vector of tuples with a defaulted element value which I would prefer to construct directly from the arguments, without spelling out each tuple element type again.)

Cosmogony answered 7/8, 2012 at 13:1 Comment(17)
It's there in N3059 and it is gone again in N3140. Now, if a human could just find out which report was actually integrated, I would know what happened to it...Bivalve
So just so I'm clear, you have a vector of tuples of types with multi-argument constructors?Cotillion
@MarkB: Not quite. I have a single tuple type, and I want to construct such a tuple from a the arguments of the constructors of the constituent types directly, without intermediate copy-initialization. This is part of the general idiom-shift from C++03 to C++11 which prefers direct-initialization over copy-initialization whenever possible.Cosmogony
in what sense is std::array a special case of std::tuple? does std::tuple have contiguous memory constraints?Sledge
@rhalbersma std::array supports the "tuple-like access" interface but there the similarity ends. There is also no piecewise construction there, nor can there be as it's an aggregate and can't have explicit constructors.Mold
The best I can come up with is passing n boost::in_place's into emplace_back.Cotillion
@Potatoswatter: Hm, maybe my example isn't good then? Do you have a better example of how to use piecewise construction? Me, I mainly want to avoid having to say the type names repeatedly.Cosmogony
@rhalbersma: They're similar in terms of interface: they have tuple_size and element_type traits; they have iterators, and get accessors. It's true that array has additional details (contiguous memory, aggregate, and []-accessor), though.Cosmogony
@Bivalve N3140 focuses on reference members of pairs and tuples. But as far as I can tell, std::pair also supports reference members, and will misbehave, binding a member reference to a temporary when you try to piecewise construct a const lvalue or rvalue reference member. EDIT: Maybe that's not a problem, since the user might want that when the resulting tuple is also a temporary.Mold
@Potatoswatter: Thanks for your edit -- I only noticed now that you installed forward_as_tuples. I never used that one before! :-)Cosmogony
@rhalbersma: Another similarity: There are type-deducing maker functions std::make_pair and std::make_tuple, as well as the much-missed make_array that should really exist...Cosmogony
Where did you get the first syntax from? I am not seeing a variadic template constructor in pair that takes a single sequence of arguments. The one present in the standard is the second one, taking a tag and two tuples. I have the feeling that your premise does not really holdThroes
The only guess I have is the shear uglyness that would result in the signature, since you would need tuple_size argument packs. In theory, you might be able to have it accept a variable number of arguments itself, and do something to delegate it. As for std::array, it has no constructors, and defining one would prevent it from being an aggregate.Purusha
@DaveS: You actually have a point... such a constructor would require a variable number of variadic template argument packs. A variadic variadic template if you may, for which there is no appropriate syntax. You should write that as an answer. On the other hand, having a make_array does not require array to have constructors --I don't think make_array is needed, but the lack of constructors would not be a reason not to have make_arrayThroes
@DavidRodríguez-dribeas I agree with make_array, but I was more thinking about a piecewise_construct constructor for std::arrayPurusha
@DavidRodríguez-dribeas: The first piece of code is pseudo-code in quotation marks. I agree that array can't get a constructor, but conversely, the make_ functions don't make sense with piecewise arguments, since the whole point of them is to deduce the element type. make and piecewise are mutually exclusive concepts.Cosmogony
"morally speaking" ??Chimpanzee
R
19

Question: Why doesn't the same piecewise constructibility exist for arrays and tuples?

My recollection is that piecewise construction was added to std::pair for one reason only: to support uses-allocator construction of the pair elements, i.e. to allow an allocator to be provided and conditionally passed to the elements if they support construction with an allocator (see [allocator.uses] in the standard).

At one point during the C++0x process std::pair had twice as many constructors as it does now, with every constructor having a corresponding "allocator-extended" version taking a std::allocator_arg_t and an allocator argument e.g.

template<class T, class U>
  struct pair {
    pair();
    pair(allocator_arg_t, const Alloc&);
    template<class TT, class UU>
      pair(TT&&, UU&&);
    template<class Alloc, class TT, class UU>
      pair(allocator_arg_t, const Alloc&, TT&&, UU&&);
    // etc.

There was something of a running joke (haha, only serious) about the insane complexity of std::pair. The support for passing allocators to the elements was removed from std::pair and moved into std::scoped_allocator_adaptor, which is responsible for detecting whether the elements should be constructed with an allocator (see the construct overloads taking a pointer to std::pair in [allocator.adaptor.members]).

A nice consequence of the piecewise construction is that you can do "emplace" style initialization of pair elements, allowing pairs of non-movable, non-copyable types, but as far as I know that was not the goal of the design.

So the reason tuple doesn't support it is that the feature was invented to simplify pair which had ballooned from a very simple type in C++03 to a laughing stock in C++0x, but doing the same for tuple was not considered as important (it was new for C++11 anyway). Also, extending scoped_allocator_adaptor to handle tuples of arbitrary numbers of elements would have made that adaptor much more complicated.

As for std::array, that's an aggregate type (because reasons) so adding a constructor taking piecewise_construct_t is not possible without making it a non-aggregate.

Reine answered 22/7, 2014 at 13:2 Comment(1)
I would very much need piecewise construction for tuples! Its useful.Heritor
P
13

I'm not sure why it's not there. Previously, I thought that the implementation wouldn't be possible, given the current varadic template syntax, but I realized that it can be done if it's broken into pieces.

If they had defined an interface like this:

template<typename... T>
tuple(piecewise_construct, T&&... t);

And made it a requirement that the arguments are something that you can use std::get<N> to access the arguments (basically, tuples, pairs, arrays). There would have to be extra checks to verify there isn't a mismatch between the number of arguments given and the number of elements in the tuple.

Edit: This problem has been bothering me since I read it. And I've created the following class, it is derived from std::tuple, and has no data members, so you can assign it to the tuple and the slicing is harmless. The current version requires that the elements be moveable or copyable, as it creates a temporary and then inserts that into the tuple. If you were a tuple implementer, it should be possible to eliminate even that move.

namespace detail
{
template<int ... N>
struct index {
    typedef index<N..., sizeof...(N)> next;
};
template<int N>
struct build_index {
    typedef typename build_index<N - 1>::type::next type;
};

template<>
struct build_index<0> {
    typedef index<> type;
};

template<typename T>
struct tuple_index {
    typedef typename build_index<
            std::tuple_size<typename std::remove_reference<T>::type>::value>::type type;

};
}
template<typename ... Elements>
class piecewise_tuple: public std::tuple<Elements...>
{
    typedef std::tuple<Elements...> base_type;

    template<int Index, typename ... Args, int ... N>
    static typename std::tuple_element<Index, base_type>::type 
    construct(std::tuple<Args...>&& args, detail::index<N...>)
    {
        typedef typename std::tuple_element<Index, base_type>::type result_type;
        return result_type(std::get<N>(std::move(args))...);
    }

    template<int ...N, typename ArgTuple>
    piecewise_tuple(detail::index<N...>, ArgTuple&& element_args)
    : base_type( construct<N>( std::get<N>(std::forward<ArgTuple>(element_args)),
                 typename detail::tuple_index< typename std::tuple_element<N, typename std::remove_reference<ArgTuple>::type >::type >::type() )...)
    {

    }

public:

    piecewise_tuple() = default;

    // For non-piecewise constructors, forward them
    template<typename... Args>
    piecewise_tuple(Args&&... args) : base_type(std::forward<Args>(args)...) {}


    template<typename... T>
    piecewise_tuple(std::piecewise_construct_t, T&&... args) :
    piecewise_tuple(typename detail::tuple_index<base_type>::type(),    
                    std::forward_as_tuple(std::forward<T>(args)...))
    {

    }


};

// Usage example
int main()
{
   int i = 5;
   std::unique_ptr<int> up(new int(0));

   piecewise_tuple<std::pair<int, int>, double, std::unique_ptr<int>, int& >
   p(std::piecewise_construct,
    std::forward_as_tuple(1,2),
    std::forward_as_tuple(4.3),
    std::forward_as_tuple(std::move(up)),
    std::forward_as_tuple(i));
   return 0;
}
Purusha answered 7/8, 2012 at 14:18 Comment(8)
+1 Although I don't quite agree on the array part, the main point (why is there no piecewise constructor for tuple) is good. On the std::array case, a make_array could be provided without adding constructors to std::array, and the implementation would actually be simple. Although I don't think it would provide much value.Throes
I don't see this as a problem: template<class... Tuples> tuple(piecewise_construct_t, Tuples&&...). You could possibly add a trait that checks if all Tuples are really of type std::tuple.Heptateuch
@Xeo: Hrm... you're right, and you could use a static_assert to verify the length of the arguments. And you wouldn't even have to use only std::tuple (though that's the most likely use).Purusha
One other solution would be to allow an implementation-defined limit to the number of arguments separate from the usual pack length limit. Solve the double-variadic problem the same way the single-variadic problem was solved in C++03.Mold
@Heptateuch Sounds like a job for a Concept. Maybe the mysterious removal was related to that major feature drop?Mold
@Potatoswatter: I don't think so. A concept would be neat, but in fact you don't even need the trait. When you try to access the "tuple", you get an error anyways if it doesn't support the tuple interface.Heptateuch
For the record one of the selling points of std::piecewise_construct is that it allows in-place construction of non-movable types (when it takes more than one argument for such a type). The big catch however is that there's only one way to implement piecewise construction, via delegating constructors -- which makes it very intrusive. Piecewise constructors must be in std::tuple to get the real boon.Yanez
I have heard this claim before that piecewise construction requires delegating constructors, but you can also implement it with a base class that has the constructor you need (essentially delegating constructors, but without the language feature).Baeyer
S
1

Here is my implementation of tuple piecewise (it also allow to omit values with omit "keyword"). Zero overhead (no copy/move - direct construction):

http://coliru.stacked-crooked.com/a/6b3f9a5f843362e3

#include <tuple>
#include <utility>
#include <typeinfo>


struct Omit{} omit;


template <class Field, class ...Fields>
struct TupleHolder{
    using fieldT = Field;
    using nextT = TupleHolder<Fields...>;

    Field field;
    TupleHolder<Fields...> next;

    TupleHolder(){}

    template <class ...ValuesRef>
    TupleHolder(Omit, ValuesRef&& ... values)
            : next( std::forward<ValuesRef>(values)... )
    {}

    template <std::size_t ...ids, class FieldValue, class ...ValuesRef>
    TupleHolder(std::index_sequence<ids...>, FieldValue&& field, ValuesRef&& ... values)
            :
            field( std::get<ids>(std::forward<FieldValue>(field))... ),
            next( std::forward<ValuesRef>(values)... )

    {};


    template <class FieldValue, class ...ValuesRef>
    TupleHolder(FieldValue&& field, ValuesRef&& ... values)
            : TupleHolder(
            std::make_index_sequence<
                    std::tuple_size< std::decay_t<FieldValue> >::value
            >(),
            std::forward<FieldValue>(field),
            std::forward<ValuesRef>(values)...
    )
    {}

};


template <class Field>
struct TupleHolder<Field>{
    using fieldT = Field;
    Field field;    // actually last

    TupleHolder(){}
    TupleHolder(Omit){}

    template <std::size_t ...ids, class FieldValue>
    TupleHolder(std::index_sequence<ids...>, FieldValue&& field)
            :
            field( std::get<ids>(std::forward<FieldValue>(field))... )
    {}


    template <class FieldValue>
    TupleHolder(FieldValue&& field)
            : TupleHolder(
            std::make_index_sequence<
                    std::tuple_size< std::decay_t<FieldValue> >::value
            >(),
            std::forward<FieldValue>(field)
    )
    {}
};



template <int index, int target_index, class T>
struct GetLoop{
    using type = typename T::nextT;

    constexpr static decltype(auto) get(T& data) noexcept{
        return GetLoop<index+1, target_index, typename T::nextT>::get(
                data.next
        );
    }

    constexpr static decltype(auto) get(const T& data) noexcept{
        return GetLoop<index+1, target_index, typename T::nextT>::get(
                data.next
        );
    }


    constexpr static decltype(auto) get(T&& data) noexcept{
        return GetLoop<index+1, target_index, typename T::nextT>::get(
                std::forward<type>(data.next)
        );
    }
};

template <int target_index, class T>
struct GetLoop<target_index, target_index, T>{
    using type = typename T::fieldT;

    constexpr static type& get(T& data) noexcept{
        return data.field;
    }

    constexpr static const type& get(const T& data) noexcept{
        return data.field;
    }

    constexpr static type&& get(T&& data) noexcept{
        return std::forward<type>(data.field);
    }
};


// ----------------------------------------------------------------------------------
//                          F R O N T E N D
// ----------------------------------------------------------------------------------

template<class ...FieldTypes>
struct TuplePiecewise{
    using fieldsT = TupleHolder<FieldTypes...>;
    TupleHolder<FieldTypes...> data;

    TuplePiecewise(){}

   // allow copy constructor
   TuplePiecewise(TuplePiecewise& other)
            : TuplePiecewise(static_cast<const TuplePiecewise&>(other)) {}


    template <class ...ValuesRef>
    explicit constexpr TuplePiecewise(ValuesRef&& ... values) noexcept
            : data( std::forward<ValuesRef>(values)... ){}

    TuplePiecewise( const TuplePiecewise& other ) = default;
    TuplePiecewise( TuplePiecewise&& other ) = default;


    static constexpr const std::size_t size = sizeof...(FieldTypes);
};


template<int index, class ...FieldTypes>
constexpr decltype(auto) get(TuplePiecewise<FieldTypes...> &&list) noexcept {
    return GetLoop<0, index, typename TuplePiecewise<FieldTypes...>::fieldsT >::get(  std::move(list.data) );
}

template<int index, class ...FieldTypes>
constexpr decltype(auto) get(TuplePiecewise<FieldTypes...> &list) noexcept {
    return GetLoop<0, index, typename TuplePiecewise<FieldTypes...>::fieldsT >::get(  list.data );
}

template<int index, class ...FieldTypes>
constexpr decltype(auto) get(const TuplePiecewise<FieldTypes...> &list) noexcept {
    return GetLoop<0, index, typename TuplePiecewise<FieldTypes...>::fieldsT >::get(  list.data );
}

Usage:

TuplePiecewise< CopyTest, int&, string, int >
list (forward_as_tuple(45,63), forward_as_tuple(i), forward_as_tuple("hghhh"), omit );
decltype(auto) o = get<2>(list);
cout << o;

Tuple inside tuple (zero overhead):

TuplePiecewise< string, TuplePiecewise<int,int> > list4(forward_as_tuple("RRR"), forward_as_tuple(forward_as_tuple(10), forward_as_tuple(20)));
Saprolite answered 20/8, 2015 at 16:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.