Why is there no piecewise tuple construction?
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.)
Solution 1:
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.
Solution 2:
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;
}
Solution 3:
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)));