Why Is There No Piecewise Tuple Construction

Why is there no piecewise tuple construction?

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.

C++11 use-case for piecewise_construct of pair and tuple?

Not all types can be moved more efficiently than copied, and for some types it may make sense to even explicitly disable both copying and moving. Consider std::array<int, BIGNUM> as an an example of the former kind of a type.

The point with the emplace functions and piecewise_construct is that such a class can be constructed in place, without needing to create temporary instances to be moved or copied.

struct big {
int data[100];
big(int first, int second) : data{first, second} {
// the rest of the array is presumably filled somehow as well
}
};

std::pair<big, big> pair(piecewise_construct, {1,2}, {3,4});

Compare the above to pair(big(1,2), big(3,4)) where two temporary big objects would have to be created and then copied - and moving does not help here at all! Similarly:

std::vector<big> vec;
vec.emplace_back(1,2);

The main use case for piecewise constructing a pair is emplacing elements into a map or an unordered_map:

std::map<int, big> map;
map.emplace(std::piecewise_construct, /*key*/1, /*value*/{2,3});

Why do we need piecewise_construct in map emplace in C++20?

weirdo classes that take std::tuple as argument to constructor

Yes, exactly.

If you decided that m.emplace(x, y) automatically did piecewise_construct functionality for tuples x and y, then it would be impossible for me to emplace construct this:

struct Key { Key(std::tuple<int, int>); bool operator<(Key const&) const; };
struct Value { Value(std::tuple<char>); };
map<Key, Value> m;
m.emplace(std::tuple(1, 2), std::tuple('3'));

I need to provide tuples for both types, but instead I'd always get two ints for Key and a char for Value and that doesn't work. I would necessarily have to either add an extra constructor for Key or just not do any emplacing.

Now, if you follow up and say, okay, m.emplace(x, y) will automatically do piecewise_construct if x and y are tuples and also the piecewise construction is actually valid... then , first, that's getting fairly complicated. But also, what if both constructions are valid?

struct Key2 {
Key2(std::tuple<int, int>);
Key2(int, int);
bool operator<(Key2 const&) const;
};
map<Key2, Value> m2;
m2.emplace(std::tuple(1, 2), std::tuple('3'));

Now this always uses the (int, int) constructor. Is that what the user intended here?

Basically, having them separate means the user can do what they need and there's no ambiguity.

Where is piecewise_construct for boost::container::map::emplace()?

piecewise_construct support for both C++03 and C++11 capable compilers was added in commit:

https://github.com/boostorg/container/commit/79a75f470e75f35f5f2a91e10fcc67d03b0a2160

and will be officially released in Boost 1.62. The following code compiles fine:

#include <boost/tuple/tuple.hpp>
#include <tuple>
#include <boost/container/map.hpp>

class A {
public:
/**/ A( int ) { }
bool operator<( const A & ) const { return false; }
} ;

class B {
public:
/**/ B( int, const char * ) { }
} ;

int main( int, char *[] )
{
A a( 100 );
B b( 200, "foo" );

boost::container::map<A,B> m;

//1) Both Boost.Tuple and std tuple supported
//2) std:: or boost::container::piecewise_construct supported
m.emplace( boost::container::piecewise_construct,
boost::make_tuple( 300 ),
boost::make_tuple( 400, "World" ) );

m.emplace( std::piecewise_construct,
std::make_tuple( 400 ),
std::make_tuple( 500, "World2" ) );
}

Initialize a tuple of non-copyable and non-movable classes

std::tuple has few facilities for constructing its members in-situ. The generally expected way to initialize a tuple is via copy/move from its component objects.

It does permit implicit conversion of constructor parameters to their respective arguments. But that is only a 1:1 relationship between parameters and tuple members. There is no way to have a many-to-one relationship between constructor parameters and tuple members.

You can allow your type to be implicitly constructible from a tuple itself:

class A
{
public:
A(float, int) {}
A(const std::tuple<float, int> &tpl)
: A(std::get<0>(tpl), std::get<1>(tpl)) {}

private:
A() = delete;
A(const A&) = delete;
A(A&&) = delete;
};

Thus, you can construct a tuple like this:

class B
{
public:
B() : ta(std::tuple<float, int>(0.0f, 1)) {}

private:
std::tuple<A> ta;
};

If you want to employ deeper metaprogramming, you may yet be able to make a set of constructors that allow your type to be constructed from a tuple whose types match any available constructors:

class A
{
public:
A(float, int) {}

template<typename ...Args>
A(const std::tuple<Args...> &args) : A(args, std::index_sequence_for<Args...>{}) {}

private:
A() = delete;
A(const A&) = delete;
A(A&&) = delete;

template<typename ...Args, std::size_t ...Is>
A(const std::tuple<Args...> &args, std::index_sequence<Is...>)
: A(std::get<Is>(args)...) {}
};

Why does unordered_map's emplace with piecewise_construct argument needs default constructor?

std::unordered_map needs default constructor is because of operator[]. map[key] will construct new element using default constructor if key is not exist in map.

You can totally use map without default constructor. E.g. following program will compile with no error.

struct A {
int x;
A(int x) : x(x) {}
};

...

std::unordered_map<int, A> b;

b.emplace(std::piecewise_construct,
std::forward_as_tuple(1),
std::forward_as_tuple(2));
b.at(1).x = 2;

How to initialize a tuple of non-default-constructible not-copyable objects?

Can you just add a piecewise constructor for your types? If so, you can create a horrible macro that unpacks and delegates a tuple:

#define CONSTRUCT_FROM_TUPLE(CLS)                      \
template <class... Ts> \
CLS(std::tuple<Ts...> const& tup) \
: CLS(tup, std::index_sequence_for<Ts...>{}) \
{ } \
\
template <class Tuple, size_t... Is> \
CLS(Tuple const& tup, std::index_sequence<Is...> ) \
: CLS(std::get<Is>(tup)...) \
{ }

And just add it to your types:

struct A {
A(bool, int ) { }
A(const A& ) = delete;
CONSTRUCT_FROM_TUPLE(A)
};

struct B {
B(char, double ) { }
B(const B& ) = delete;
CONSTRUCT_FROM_TUPLE(B)
};

And pass in tuples:

std::tuple<A, B> tup(
std::forward_as_tuple(true, 42),
std::forward_as_tuple('x', 3.14));

Pre-C++11, I don't know that this is possible - you don't have delegating constructors at all. You'd have to either:

  1. Write your own tuple-like class that accepts tuples in its constructor
  2. Add tuple constructors to your types that explicitly initialize the same thing as the non-tuple versions did
  3. Have a tuple of types that are single-argument constructible, like boost::tuple<boost::scoped_ptr<A>, boost::scoped_ptr<B>>(new A(...), new B(...))

(1) is a lot of work, (2) is code duplication and error prone, and (3) involves now having to do allocation all of a sudden.

Eliminate copies when constructing members of a class

This is already optimal:

Comp(T&& t, U&& u)
: t_m(std::move(t))
, u_m(std::move(u))
{ }

If T has a move constructor, this'll be a move. If it doesn't, this'll be a copy - but then there's no way around making a copy somewhere. And it isn't copyable, then the whole question is moot.

Of course this only works for rvalues, so you'd want something for lvalues too. That unfortunately gets a little complicated:

template <class Tx, class Ux, 
class = std::enable_if_t<std::is_convertible<std::decay_t<Tx>*, T*>::value &&
std::is_convertible<std::decay_t<Ux>*, U*>::value>>
Comp(Tx&& t, Ux&& u)
: t_m(std::forward<Tx>(t))
, u_m(std::forward<Ux>(u))
{ }

Here we want to allow deduction of Tx such that it's either T, T&, or D, D& where D derives from T. std::decay drops the reference and is_convertible for pointers checks if it's derived.

Okay, can we do better? Not really. This is either going to do 1 move or 1 copy for each member. But what if we want to construct them in place? That we should be able to allow for:

template <class... TArgs, class... UArgs,
class = std::enable_if_t<std::is_constructible<T, TArgs...>::value &&
std::is_constructible<U, UArgs...>::value>>
Comp(std::piecewise_construct_t pc, std::tuple<TArgs...> const& t, std::tuple<UArgs...> const& u)
: Comp(t, u, std::index_sequence_for<TArgs...>{}, std::index_sequence_for<UArgs...>{})
{ }

private:
template <class TTuple, class UTuple, size_t... Is, size_t... Js>
Comp(TTuple const& t, UTuple const& u, std::index_sequence<Is...>, std::index_sequence<Js...> )
: t_m(std::get<Is>(t)...)
, u_m(std::get<Js>(u)...)
{ }

With this potentially we can avoid any kind of copy or move at all by just constructing in-place. Whether or not this is beneficial depends on your usage of Comp.



Related Topics



Leave a reply



Submit