Std::pair:过于严格的构造函数
std::pair: too restrictive constructor?
我偶然发现了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
的复制构造函数时,编译失败了。它与分段构造一起工作,但我认为这不应该是必要的,因为从int
到foobar
的隐式转换。我引用的构造函数具有以下签名:
template <typename U, typename V>
pair(U&& u, V&& v);
你能解释一下,为什么pair构造函数如此严格,不允许对不可复制/不可移动类型进行隐式转换吗?
这是标准中的一个缺陷(我一开始没有发现它,因为它是为tuple
制定的)。
进一步的讨论和提议的决议(2015年5月在Lenexa上投票通过c++ 1z):
https://wg21.link/n4387潜在的问题是pair
和tuple
的转换构造函数检查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
,因为这个构造函数模板的声明可以为任何类型U
和V
实例化。因此,我们需要限制这个构造函数模板:
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
,因此需要隐式转换。
在pair
和tuple
的改进中提出的explicit
vs隐式转换问题的更好解决方案是让explicit
魔术:
当且仅当
is_convertible<U&&, first_type>::value
为false
或is_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;上面提到的构造函数
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_type
或V
不能隐式转换为second_type
,此构造函数将不参与重载解析。”
因此,虽然这是标准中的一个有争议的缺陷,但从正式的角度来看,代码不应该编译。
- 应用于运算符而不是构造函数的显式关键字
- 复制构造函数优先于移动构造函数
- 相对于继承的构造函数,gcc 编译器是否还有一个错误?
- STD ::在模板函数中应用于构造函数
- 对C++所做的更改使复制初始化适用于具有显式构造函数的类
- 为什么我的类只适用于两个构造函数 C++
- 可以将属性应用于构造函数参数
- 如何使用户定义的空默认构造函数的行为类似于编译器定义的空构造函数
- C++是否可以创建依赖于单个构造函数参数的派生类而不是bass类
- 可变参数构造函数优先于用户提供的移动构造函数,除非默认
- SWIG 多参数类型映射适用于函数,但如果有多个构造函数,则不适用于构造函数
- 构造函数效率低下,适用于按值从函数返回对象的情况
- 类似于类中的 std::map 或 std::vector 的构造函数
- 创建独立于构造函数的对象指针
- 在本地将 'using std::foo' 指令应用于构造函数初始值设定项列表 (C++)
- C++ 在超类构造函数中运行依赖于子类覆盖的大量变量的代码的正确方法是什么?
- 移动构造函数相对于复制构造函数的优势是什么?复制构造函数使用bool来表示是复制还是移动
- 当大括号初始值设定项列表中是否有序列点应用于构造函数时
- C等价于新对象(构造函数)
- 依赖于尚未构造的对象的构造函数