I stumbled upon a surprising behaviour of the new std::pair
constructor, that was introduced with C++11. I observed the issue when using std::pair<int, std::atomic<int>>
, and it occurs, because std::atomic
is neither copyable nor movable. In the following code, I replace std::atomic<int>
with foobar
for simplification.
The following code compiles fine, both with GCC-4.9 and Clang-3.5 (with and without libc++):
struct foobar
{
foobar(int) { } // implicit conversion
// foobar(const foobar&) = delete;
};
std::pair<int, foobar> p{1, 2};
This behaviour is expected. However, when I delete the copy constructor of foobar
, the compilation fails. It works with piecewise construct, but I think that shouldn't be necessary, because of the implicit conversion from int
to foobar
. I am referring to the constructor with the following signature:
template <typename U, typename V>
pair(U&& u, V&& v);
Can you explain, why the pair constructor is so restrictive, and does not allow implicit conversions for noncopyable/nonmovable types?
Testing your code, with the copy constructor deleted, I get
[h:\dev\test\0082] > g++ foo.cpp In file included from h:\bin\mingw\include\c++\4.8.2\utility:70:0, from foo.cpp:1: h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h: In instantiation of 'constexpr std::pair::pair(_U1&&, const _T2&) [with _U1 = int; <template-parameter-2-2> = void; _T1 = int; _T2 = foobar]': foo.cpp:12:34: required from here h:\bin\mingw\include\c++\4.8.2\bits\stl_pair.h:134:45: error: use of deleted function 'foobar::foobar(const foobar&)' : first(std::forward<_U1>(__x)), second(__y) { } ^ foo.cpp:6:5: error: declared here foobar(const foobar&) = delete; ^ [h:\dev\test\0082] > cl foo.cpp foo.cpp [h:\dev\test\0082] > _
The mentioned constructor
pair(_U1&&, const _T2&)
is not specified by the standard.
Addendum: as shown below the code works just fine with only the standard's constructors defined for the pair class:
#include <utility>
struct foobar
{
foobar(int) { } // implicit conversion
foobar(const foobar&) = delete;
};
namespace bah {
using std::forward;
using std::move;
struct Piecewise_construct_t {};
template <class T1, class T2>
struct Pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
//Pair(const Pair&) = default;
//Pair(Pair&&) = default;
/*constexpr*/ Pair(): first(), second() {}
Pair(const T1& x, const T2& y)
: first( x ), second( y )
{}
template<class U, class V> Pair(U&& x, V&& y)
: first( forward<U>( x ) ), second( forward<V>( y ) )
{}
template<class U, class V> Pair(const Pair<U, V>& p)
: first( p.first ), second( p.second )
{}
template<class U, class V> Pair(Pair<U, V>&& p)
: first( move( p.first ) ), second( move( p.second ) )
{}
//template <class... Args1, class... Args2>
//Pair(Piecewise_construct_t,
//tuple<Args1...> first_args, tuple<Args2...> second_args);
//
//Pair& operator=(const Pair& p);
//template<class U, class V> Pair& operator=(const Pair<U, V>& p);
//Pair& operator=(Pair&& p) noexcept(see below);
//template<class U, class V> Pair& operator=(Pair<U, V>&& p);
//void swap(Pair& p) noexcept(see below);
};
}
auto main()
-> int
{
bah::Pair<int, foobar> p{1, 2};
};
[h:\dev\test\0082] > g++ bar.cpp [h:\dev\test\0082] > _
IMPORTANT ERRATA.
As @dyb points out in comments, while the standard's “requires” clause refers to std::is_constructible
(the pair's items must be constructible from the arguments), the “remarks” clause, following the resolution of Defect Report 811, refers to convertibility:
C++11 §20.3.2/8:
“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.”
And so, while this is arguably now a defect in the standard, from a formal point of view the code should not compile.
It's a defect in the Standard (I didn't found it at first since it's formulated for tuple
).
https://wg21.link/lwg2051
Further discussion and a proposed resolution (voted into C++1z at Lenexa in May 2015):
https://wg21.link/n4387
The underlying problem is that the converting constructors of pair
and tuple
check for is_convertible
which requires an accessible copy/move constructor.
En detail: The converting constructor templates of std::pair<T1, T2>
and std::tuple
look like this:
template<class U, class V>
constexpr pair(U&&, V&&);
But this is too greedy: It produces a hard error when you try to use it with incompatible types, and std::is_constructible<pair<T1, T2>, U, V>::value
will always be true
because the declaration of this constructor template can be instantiated for any types U
and V
. Hence, we need to restrict this constructor template:
template<class U, class V,
enable_if_t<check_that_we_can_construct_from<U, V>::value>
>
constexpr pair(U&& u, V&& v)
: t1( forward<U>(u) ), t2( forward<V>(v) )
{}
Note that the tx( forward<A>(a) )
can call explicit
constructors. Because this constructor template of pair
is not marked as explicit, we must restrict it to not perform explicit conversions internally while initializing its data members. Therefore, we use is_convertible
:
template<class U, class V,
std::enable_if_t<std::is_convertible<U&&, T1>::value &&
std::is_convertible<V&&, T2>::value>
>
constexpr pair(U&& u, V&& v)
: t1( forward<U>(u) ), t2( forward<V>(v) )
{}
In the case of the OP, there is no implicit conversion: the type is noncopyable, and this renders the test that defines implicit convertibility ill-formed:
// v is any expression of type `int`
foobar f = v; // definition of implicit convertibility
This copy-initialization form according to the Standard produces a temporary on the right hand side, initialized with v
:
foobar f = foobar(v);
Where the right hand side shall be understood as an implicit conversion (so no explicit
constructors can be called). However, this requires to copy or move the temporary on the right hand side into f
(until C++1z, see p0135r0).
To sum up: int
is not implicitly convertible to foobar
because of the way implicit convertibility is defined, which requires moveability because RVO is not mandatory. pair<int, foobar>
cannot be constructed from {1, 2}
because this pair
constructor template is not explicit
and hence requires implicit conversions.
A better solution to the explicit
vs implicit conversion problem as presented in Improvements on pair
and tuple
is to have explicit
magic:
The constructor is
explicit
if and only ifis_convertible<U&&, first_type>::value
isfalse
oris_convertible<V&&, second_type>::value
isfalse
.
With this change, we can loosen the restriction of implicit convertibility (is_convertible
) to "explicit convertibility" (is_constructible
). Effectively, we get the following constructor template in this case:
template<class U, class V,
std::enable_if_t<std::is_constructible<U&&, int>::value &&
std::is_constructible<V&&, foobar>::value>
>
explicit constexpr pair(U&&, V&&);
Which is unrestricted enough to make std::pair<int, foobar> p{1, 2};
valid.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With