std :: pair:限制性过强的构造函数?

时间:2014-05-18 15:41:42

标签: c++ c++11

我偶然发现了新的std::pair构造函数的令人惊讶的行为,这是C ++ 11引入的。我在使用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的复制构造函数时,编译失败。它适用于分段构造,但我认为不应该这样做,因为从int隐式转换为foobar。我指的是具有以下签名的构造函数:

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

你能解释为什么对构造函数是如此限制,并且不允许对noncopyable / nonmovable类型进行隐式转换?

2 个答案:

答案 0 :(得分:9)

这是标准中的一个缺陷(我最初没有找到它,因为它是为tuple制定的。)

https://wg21.link/lwg2051

进一步讨论和提议的决议(2015年5月在Lenexa投票选入C ++ 1z):

https://wg21.link/n4387


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

详细信息: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,因为这个构造函数模板的声明可以实例化为< em>任何类型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,因此需要隐式转换。


Improvements on pair and tuple中提到的explicit vs隐式转换问题的更好解决方案是explicit魔术:

  

当且仅当explicitis_convertible<U&&, first_type>::valuefalse时,构造函数为is_convertible<V&&, second_type>::value   是false

通过此更改,我们可以放宽隐式可兑换性(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};有效。

答案 1 :(得分:1)

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

[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]
> _

提到的构造函数

pair(_U1&&, const _T2&)

未由标准指定。


附录:如下所示,代码只适用于为对类定义的标准构造函数:

#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]
> _

重要错误
正如@dyb在注释中指出的那样,而标准的“requires”子句引用std::is_constructible(该对的项必须可以从参数构造),“remarks”子句遵循Defect Report 811的解析,指可兑换性:

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

所以,虽然这可能是标准中的缺陷,但从形式上看,代码不应该编译。