31

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;
}
dyp
  • 38,334
  • 13
  • 112
  • 177
oli_obk
  • 28,729
  • 6
  • 82
  • 98
  • 1
    Seems like it's not the fault of `map`, but of `pair`: http://coliru.stacked-crooked.com/a/e5232a33731cf220 – dyp Jan 28 '14 at 12:45
  • 1
    @dyp See a related question: [std::unordered_map::emplace issue with private/deleted copy constructor](https://stackoverflow.com/questions/14808663/stdunordered-mapemplace-issue-with-private-deleted-copy-constructor?rq=1) –  Jan 28 '14 at 13:19
  • 1
    @remyabel Great! It's a dup/related, I came to a similar conclusion as jogojapan. However, IMHO the problem is `pair`, not `map`. – dyp Jan 28 '14 at 13:38

1 Answers1

26

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.

dyp
  • 38,334
  • 13
  • 112
  • 177
  • 1
    What a weird issue :( – dyp Jan 28 '14 at 13:40
  • You can also try `static_assert(std::is_convertible::value, "int is not convertible to A");` and `static_assert(std::is_constructible::value, "A is not constructible from an int");` to illustrate the issue. – dyp Jan 28 '14 at 13:44
  • thanks, i assumed it was an overload issue of the templated constructors of std::pair. although on the right track your answer cleared everything up. thanks for the amount of detail in your anwser – oli_obk Jan 28 '14 at 14:13
  • @Oliver'ker'Schneider You might consider filing a bug report. To me, it seems to be an oversight to disallow this form of constructing a pair. – dyp Jan 28 '14 at 14:22
  • 1
    @ker Thanks. Although I think if that nonstandard ctor wasn't there, the ctor taking two `const&` should be used, which would also fail. So I think the issue is rejecting the `U1&&, U2&&` ctor even though that seems like a reasonable choice. I'll also guess that the libstdc++ implementers will "blame" the Standard, but maybe that'll lead to a discussion if `is_convertible` (or the corresponding wording in the Standard) is intended. – dyp Jan 28 '14 at 15:08
  • 2
    Using `is_convertible` there is intended, as clarified by [N4064](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4064.html) which is very explicit about the use of `is_convertible`, but also improves things in this area to pick a better constructor for non-copyable, non-movable types. – Jonathan Wakely Apr 08 '15 at 16:14
  • @JonathanWakely Thank you; I found N3680 four months after answering this question. As anticipated, you "blame the Standard" ;) -- I like the ideas of N3680/N4064, was it rejected? (libc++ implements something similar, but I cannot find the spec in recent drafts) – dyp Apr 08 '15 at 17:38
  • 3
    @dyp, there will be a new revision of N4064 in the next mailing, and it will probably be accepted at the meeting next month. As for blaming the standard, removing the `is_convertible` constraints would break far more important programs than ones that try to emplace immovable objects in maps. The constraints were added to the standard for good reasons, and both libstdc++ and libc++ obey them for good reasons, not just to pedantically follow the standard. I'm not really sure what you expect implementations to do if not follow the standard. – Jonathan Wakely Apr 09 '15 at 00:29
  • 1
    @JonathanWakely After reading N3680, I understood the reasons why `is_convertible` is used. Please don't take my "blame the Standard" too seriously. Thanks for the info on the status of N4064. – dyp Apr 09 '15 at 02:27