14

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?

nosid
  • 48,932
  • 13
  • 112
  • 139
  • @KerrekSB: I don't get your comment. In my example, both member variables are directly initialized from `int&&`. No copy constructor for _foobar_ or _pair_ involved. – nosid May 18 '14 at 17:31
  • Hm, I probably got something wrong. I'm investigating. – Kerrek SB May 18 '14 at 17:45
  • So, I tried reproducing this with my self-made pair class and didn't manage. I looked at the error, which points to the GCC implementation and mentions [DR 811](http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#811); curiously, the overload is `template pair(U1 &&, const T2 &)`, which does end up making a copy (since it constructs a temporary to bind to the second parameter). But it shouldn't! – Kerrek SB May 18 '14 at 17:48
  • I think this is a dup :) let me see... the problem was that the Standard requires `is_convertible` which surprisingly requires movability. http://stackoverflow.com/q/21405674/420683 – dyp May 18 '14 at 17:49
  • @dyp: It's bad that GCC invents an overload that is not specified by the standard, isn't it? – Kerrek SB May 18 '14 at 17:50
  • @KerrekSB I don't think that's the problem. See my (now linked) answer – dyp May 18 '14 at 17:51
  • What I found weird about the specification in the Standard that it *requires* constructibility and SFINAE-checks for convertability. I wanted to ask about this on the isocpp/Standard discussion list some time ago.. – dyp May 18 '14 at 17:53
  • @nosid *"because of the implicit conversion from `int` to `foobar`"* And that's the problem. `int` is not implicitly convertible to `foobar` in the strict sense, because `foobar f = 5;` is not well-formed. – dyp May 18 '14 at 17:56
  • @dyp: there's no implicit conversion involved. – Cheers and hth. - Alf May 18 '14 at 17:56
  • @Cheersandhth.-Alf Indeed, and that's why I think it's a defect in the Standard. – dyp May 18 '14 at 17:56
  • @dyp: the standard doesn't have the constructor that g++ invokes here. – Cheers and hth. - Alf May 18 '14 at 17:57
  • @Cheersandhth.-Alf Yes, but it wouldn't work even w/o this ctor. See [my answer here](http://stackoverflow.com/a/21407037/420683). – dyp May 18 '14 at 17:59
  • @dyp: I wonder if it's OK to have those partial template overloads. The standard says that only the full `(U&&, V&&)` overload should exist, if and only if both `U` and `V` are implicitly convertible, not those intermediate forms. – Kerrek SB May 18 '14 at 18:08
  • @dyp: on the contrary, it [works just fine](http://coliru.stacked-crooked.com/a/c1172c5e1a6ff3b5). your answer therefore appears to be **fundamentally incorrect**. – Cheers and hth. - Alf May 18 '14 at 18:10
  • @KerrekSB Hmm I think if it's `copy_constructible`, then it is also `convertible` (for the same type). – dyp May 18 '14 at 18:10
  • @Cheersandhth.-Alf You're missing the SFINAE checks that are required by the Standard. Let me check this.. – dyp May 18 '14 at 18:11
  • @dyp: But "requires" is different from "shall not participate", isn't it? – Kerrek SB May 18 '14 at 18:12
  • 1
    @dyp: Either way, I think it's not a problem: The OP's code *should* fail, and it does. The fact that GCC passes the first argument through a direct initialization rather than a copy does not diminish this. – Kerrek SB May 18 '14 at 18:13
  • @KerrekSB Exactly. The *requires* part says when there's UB, whereas the "shall not participate" mandates a SFINAE-check. It's weird that those two have different requirements for the type. – dyp May 18 '14 at 18:13
  • @Cheersandhth.-Alf See http://coliru.stacked-crooked.com/a/8d9a21ee6d4f732b – dyp May 18 '14 at 18:14
  • @dyp: that's not [what the standard requires](http://coliru.stacked-crooked.com/a/22e51fa2d716f89d); it requires constructible, not convertible. so again, your answer elswhere, that you linked to, appears to **fundamentally incorrect**. "fundamentally": the foundations are wrong. – Cheers and hth. - Alf May 18 '14 at 18:19
  • @Cheersandhth.-Alf: No, it does require implicit convertibility for the constructor to participate - it's in the "Remarks", not the "Requires". Weirdly enough, the Standard only uses English, not the `std::is_convertible` trait (which is used in the implementation in GCC). – Kerrek SB May 18 '14 at 18:19
  • @KerrekSB: you mean "yes" it does not rquire convertibility. *Oh*. that's an inconsistency in the standard. – Cheers and hth. - Alf May 18 '14 at 18:20
  • @Cheersandhth.-Alf The remarks section requires a SFINAE-check for *convertibility*, and I agree that's unintuitive and probably a defect. The *requires* section AFAIK lists when the function has UB. – dyp May 18 '14 at 18:21
  • Oh dang. I'll update my answer. Is there a defect report? – Cheers and hth. - Alf May 18 '14 at 18:23
  • @Cheersandhth.-Alf I don't think so. I wanted to post that on the isocpp mailing list a long time ago but never got around doing it. – dyp May 18 '14 at 18:24
  • @Cheersandhth.-Alf Ok, there *is* already a DR + proposed resolution, see my answer. – dyp May 18 '14 at 18:42

2 Answers2

11

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 if is_convertible<U&&, first_type>::value is false or is_convertible<V&&, second_type>::value is false.

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.

dyp
  • 38,334
  • 13
  • 112
  • 177
  • Interesting. The document referenced by the second link discusses exactly the problem I have observed. But I don't understand the first link. I see that it is related to the second link, but is it also related to my question? – nosid May 18 '14 at 19:58
  • @nosid Kind of. `is_convertible` is also `false` for explicit constructors, so it has the same underlying problem. – dyp May 18 '14 at 20:04
  • Can you elaborate some more about the issue described there, and how it was resolved (it was supposedly resolved recently, so in C++17 this should work I guess.) – einpoklum Dec 21 '15 at 23:31
  • @einpoklum I've elaborated "slightly" ;) – dyp Dec 22 '15 at 00:15
  • [N4528](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4528.html) misses this change. It is in [this commit](https://github.com/cplusplus/draft/commit/c8347e3f3caa2ecef1b1dd869ce15b9095134a1a). Is it expected? – FrankHB Jan 15 '19 at 06:23
  • @FrankHB I don't know much about editor reports. Maybe it only lists *notable* changes? http://wg21.link/lwg2051 says *"Resolved by the adoption of N4387."* in May 2015... – dyp Jan 27 '19 at 14:32
  • This is strange because even the git log (with more minor editorial changes) has been there. Perhaps it was never published in an editor's report just for some occasional reasons. – FrankHB Jan 29 '19 at 21:23
1

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.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Note that the OP used G++ V4.9, not V4.8.2 . 4.9 is closer to C++11 than 4.8, maybe that's the reason why your testing failed on his code. – András Aszódi May 18 '14 at 17:51
  • @user465139: judging from the question comments the issue with 4.9 is the same. it has introduced a constructor that isn't there in the standard. – Cheers and hth. - Alf May 18 '14 at 17:57