Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why do i need to use piecewise_construct in map::emplace for single arg constructors of noncopyable objects?

The following code will not compile on gcc 4.8.2. The problem is that this code will attempt to copy construct an std::pair<int, A> which can't happen due to struct A missing copy and move constructors.

Is gcc failing here or am I missing something?

#include <map> struct A {   int bla;   A(int blub):bla(blub){}   A(A&&) = delete;   A(const A&) = delete;   A& operator=(A&&) = delete;   A& operator=(const A&) = delete; }; int main() {   std::map<int, A> map;   map.emplace(1, 2); // doesn't work   map.emplace(std::piecewise_construct,           std::forward_as_tuple(1),           std::forward_as_tuple(2)   ); // works like a charm   return 0; } 
like image 294
oli_obk Avatar asked Jan 28 '14 12:01

oli_obk


People also ask

What is Piecewise_construct?

inline constexpr std::piecewise_construct_t piecewise_construct{}; (since C++17) std::piecewise_construct_t is an empty class tag type used to disambiguate between different functions that take two tuple arguments.

What is MAP emplace?

The map::emplace() is a built-in function in C++ STL which inserts the key and its element in the map container. It effectively increases the container size by one.


1 Answers

As far as I can tell, the issue isn't caused by map::emplace, but by pair's constructors:

#include <map>  struct A {     A(int) {}     A(A&&) = delete;     A(A const&) = delete; };  int main() {     std::pair<int, A> x(1, 4); // error } 

This code example doesn't compile, neither with coliru's g++4.8.1 nor with clang++3.5, which are both using libstdc++, as far as I can tell.

The issue is rooted in the fact that although we can construct

A t(4); 

that is, std::is_constructible<A, int>::value == true, we cannot implicitly convert an int to an A [conv]/3

An expression e can be implicitly converted to a type T if and only if the declaration T t=e; is well-formed, for some invented temporary variable t.

Note the copy-initialization (the =). This creates a temporary A and initializes t from this temporary, [dcl.init]/17. This initialization from a temporary tries to call the deleted move ctor of A, which makes the conversion ill-formed.


As we cannot convert from an int to an A, the constructor of pair that one would expect to be called is rejected by SFINAE. This behaviour is surprising, N4387 - Improving pair and tuple analyses and tries to improve the situation, by making the constructor explicit instead of rejecting it. N4387 has been voted into C++1z at the Lenexa meeting.

The following describes the C++11 rules.

The constructor I had expected to be called is described in [pairs.pair]/7-9

template<class U, class V> constexpr pair(U&& x, V&& y); 

7    Requires: is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true.

8    Effects: The constructor initializes first with std::forward<U>(x) and second with std::forward<V>(y).

9    Remarks: If U is not implicitly convertible to first_type or V is not implicitly convertible to second_type this constructor shall not participate in overload resolution.

Note the difference between is_constructible in the Requires section, and "is not implicitly convertible" in the Remarks section. The requirements are fulfilled to call this constructor, but it may not participate in overload resolution (= has to be rejected via SFINAE).

Therefore, overload resolution needs to select a "worse match", namely one whose second parameter is a A const&. A temporary is created from the int argument and bound to this reference, and the reference is used to initialize the pair data member (.second). The initialization tries to call the deleted copy ctor of A, and the construction of the pair is ill-formed.


libstdc++ has (as an extension) some nonstandard ctors. In the latest doxygen (and in 4.8.2), the constructor of pair that I had expected to be called (being surprised by the rules required by the Standard) is:

template<class _U1, class _U2,          class = typename enable_if<__and_<is_convertible<_U1, _T1>,                                            is_convertible<_U2, _T2>                                           >::value                                    >::type> constexpr pair(_U1&& __x, _U2&& __y) : first(std::forward<_U1>(__x)), second(std::forward<_U2>(__y)) { } 

and the one that is actually called is the non-standard:

// DR 811. template<class _U1,          class = typename enable_if<is_convertible<_U1, _T1>::value>::type> constexpr pair(_U1&& __x, const _T2& __y) : first(std::forward<_U1>(__x)), second(__y) { } 

The program is ill-formed according to the Standard, it is not merely rejected by this non-standard ctor.


As a final remark, here's the specification of is_constructible and is_convertible.

is_constructible [meta.rel]/4

Given the following function prototype:

template <class T> typename add_rvalue_reference<T>::type create(); 

the predicate condition for a template specialization is_constructible<T, Args...> shall be satisfied if and only if the following variable definition would be well-formed for some invented variable t:

T t(create<Args>()...); 

[Note: These tokens are never interpreted as a function declaration. — end note] Access checking is performed as if in a context unrelated to T and any of the Args. Only the validity of the immediate context of the variable initialization is considered.

is_convertible [meta.unary.prop]/6:

Given the following function prototype:

template <class T> typename add_rvalue_reference<T>::type create(); 

the predicate condition for a template specialization is_convertible<From, To> shall be satisfied if and only if the return expression in the following code would be well-formed, including any implicit conversions to the return type of the function:

To test() {   return create<From>(); } 

[Note: This requirement gives well defined results for reference types, void types, array types, and function types. — end note] Access checking is performed as if in a context unrelated to To and From. Only the validity of the immediate context of the expression of the return-statement (including conversions to the return type) is considered.


For your type A,

A t(create<int>()); 

is well-formed; however

A test() {   return create<int>(); } 

creates a temporary of type A and tries to move that into the return-value (copy-initialization). That selects the deleted ctor A(A&&) and is therefore ill-formed.

like image 56
dyp Avatar answered Oct 05 '22 14:10

dyp