Std::pair:过于严格的构造函数

std::pair: too restrictive constructor?

本文关键字:构造函数 于严格 pair Std      更新时间:2023-10-16

我偶然发现了c++ 11引入的新的std::pair构造函数的一个令人惊讶的行为。我在使用std::pair<int, std::atomic<int>>时观察到这个问题,它发生了,因为std::atomic既不可复制也不可移动。在下面的代码中,为了简化,我将std::atomic<int>替换为foobar

下面的代码在GCC-4.9和Clang-3.5(使用和不使用libc++)下都可以正常编译:

struct foobar
{
    foobar(int) { } // implicit conversion
    // foobar(const foobar&) = delete;
};
std::pair<int, foobar> p{1, 2};

此行为是预期的。然而,当我删除foobar的复制构造函数时,编译失败了。它与分段构造一起工作,但我认为这不应该是必要的,因为从intfoobar的隐式转换。我引用的构造函数具有以下签名:

template <typename U, typename V>
pair(U&& u, V&& v);

你能解释一下,为什么pair构造函数如此严格,不允许对不可复制/不可移动类型进行隐式转换吗?

这是标准中的一个缺陷(我一开始没有发现它,因为它是为tuple制定的)。

https://wg21.link/lwg2051

进一步的讨论和提议的决议(2015年5月在Lenexa上投票通过c++ 1z):

https://wg21.link/n4387


潜在的问题是pairtuple的转换构造函数检查is_convertible, CC_12需要一个可访问的复制/移动构造函数。

详细说明:std::pair<T1, T2>std::tuple的转换构造函数模板如下:

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

但是这太贪婪了:当你试图将它用于不兼容的类型时,它会产生一个严重的错误,并且std::is_constructible<pair<T1, T2>, U, V>::value将始终是true,因为这个构造函数模板的声明可以为任何类型UV实例化。因此,我们需要限制这个构造函数模板:

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) )
{}

注意tx( forward<A>(a) )可以调用explicit构造函数。由于pair的构造函数模板没有标记为显式,因此必须将其限制为而不是在初始化其数据成员时在内部执行显式转换。因此,我们使用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) )
{}

在OP的情况下,没有隐式转换:类型是不可复制的,这使得定义隐式可转换性的测试是病态的:

// v is any expression of type `int`
foobar f = v; // definition of implicit convertibility

根据标准的复制初始化形式在右侧产生一个临时对象,初始化为v:

foobar f = foobar(v);

右边应该被理解为隐式转换(因此不能调用explicit构造函数)。然而,这需要将右边的临时变量复制或移动到f中(直到c++ 1z,参见p0135r0)。

综上所述:int不能隐式转换为foobar,因为隐式可转换的定义方式,因为RVO不是强制性的,所以需要可移动。pair<int, foobar>不能从{1, 2}构造,因为pair构造函数模板不是explicit,因此需要隐式转换。


pairtuple的改进中提出的explicit vs隐式转换问题的更好解决方案是让explicit魔术:

当且仅当is_convertible<U&&, first_type>::valuefalseis_convertible<V&&, second_type>::value时,构造函数为explicit

通过这个改变,我们可以将隐式可兑换性(is_convertible)的限制放宽到"显式可兑换性"(is_constructible)。实际上,在本例中我们得到了以下构造函数模板:

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&&);

这是不受限制的,足以使std::pair<int, foobar> p{1, 2};有效

测试您的代码,删除复制构造函数,我得到

<>之前[h: 0082年测试 开发]> g++ foo.cppbinmingwincludec++4.8.2utility:70:0从foo.cpp: 1:h:binmingwincludec++4.8.2bitsstl_pair.h:在实例化'constexpr std::pair::pair(_U1&&, const _T2&) [with _U1 = int; = void;_T1 = int;_T2 = foobar]':[00:34]从这里开始H:binmingwincludec++4.8.2bitsstl_pair.h:134:45:错误:使用已删除的函数'foobar::foobar(const foobar&)':第一次(std:: forward<_U1> (__x)),第二个(__y) {}^Foo.cpp:6:5: error:声明在这里Foobar (const Foobar &) = delete;^[h: 0082年测试 开发]> cl foo.cppfoo.cpp[h: 0082年测试 开发]> _之前

上面提到的构造函数

pair(_U1&&, const _T2&)

在标准中没有指定。


附录:如下所示,仅使用为pair类定义的标准构造函数,代码就可以正常工作:

#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: 0082年测试 开发]> g++ bar.cpp[h: 0082年测试 开发]> _之前

重要勘误表
正如@dyb在评论中指出的那样,虽然该标准要求子句引用std::is_constructible (pair的项必须可以从参数中构造),“条款,在缺陷报告811的决议之后,指的是可兑换性:

c++ 11§20.3.2/8:
备注:如果U不能隐式转换为first_typeV不能隐式转换为second_type,此构造函数将不参与重载解析。”

因此,虽然这是标准中的一个有争议的缺陷,但从正式的角度来看,代码不应该编译。