【问题标题】:why do i need to use piecewise_construct in map::emplace for single arg constructors of noncopyable objects?为什么我需要在 map::emplace 中为不可复制对象的单个 arg 构造函数使用分段构造?
【发布时间】:2014-02-19 18:09:05
【问题描述】:

以下代码无法在 gcc 4.8.2 上编译。 问题是此代码将尝试复制构造 std::pair<int, A>,由于 struct A 缺少复制和移动构造函数而无法发生。

gcc 在这里失败还是我遗漏了什么?

#include <map>
struct A
{
  int bla;
  A(int blub):bla(blub){}
  A(A&&) = delete;
  A(const A&) = delete;
  A& operator=(A&&) = delete;
  A& operator=(const A&) = delete;
};
int main()
{
  std::map<int, A> map;
  map.emplace(1, 2); // doesn't work
  map.emplace(std::piecewise_construct,
          std::forward_as_tuple(1),
          std::forward_as_tuple(2)
  ); // works like a charm
  return 0;
}

【问题讨论】:

标签: c++ c++11 map std-pair emplace


【解决方案1】:

据我所知,问题不是由map::emplace 引起的,而是由pair 的构造函数引起的:

#include <map>

struct A
{
    A(int) {}
    A(A&&) = delete;
    A(A const&) = delete;
};

int main()
{
    std::pair<int, A> x(1, 4); // error
}

据我所知,无论是使用 coliru 的 g++4.8.1 还是 clang++3.5,此代码示例都无法编译,它们都使用 libstdc++。

问题的根源在于,虽然我们可以构造

A t(4);

也就是说,std::is_constructible&lt;A, int&gt;::value == true,我们不能隐式转换 intA [conv]/3

表达式 e 可以隐式转换为类型 T 当且仅当声明 T t=e; 格式正确, 对于一些发明的临时变量t

注意复制初始化(=)。这会创建一个临时的 A 并从这个临时的 [dcl.init]/17 初始化 t。这种临时初始化尝试调用 A 的已删除移动 ctor,这使得转换格式不正确。


由于我们无法将 int 转换为 A,因此我们希望调用的 pair 的构造函数被 SFINAE 拒绝。这种行为令人惊讶,N4387 - Improving pair and tuple 分析并尝试通过使构造函数 explicit 而不是拒绝它来改善这种情况。 N4387 在 Lenexa 会议上被选为 C++1z。

下面介绍C++11规则。

[pairs.pair]/7-9 中描述了我期望调用的构造函数

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

7    要求: is_constructible&lt;first_type, U&amp;&amp;&gt;::valuetrueis_constructible&lt;second_type, V&amp;&amp;&gt;::valuetrue

8    效果: 构造函数首先使用 std::forward&lt;U&gt;(x) 初始化,然后使用 std::forward&lt;V&gt;(y).

9    备注: 如果 U 不能隐式转换为 first_typeV 不能隐式转换为 second_type this 构造函数不应参与重载决议。

注意 Requires 部分中的 is_constructibleRemarks 部分中的“不可隐式转换”之间的区别。满足调用此构造函数的要求,但它可能不参与重载决议(= 必须通过 SFINAE 拒绝)。

因此,重载解析需要选择一个“更差的匹配”,即第二个参数是A const&amp;。从int 参数创建一个临时对象并绑定到该引用,该引用用于初始化pair 数据成员(.second)。初始化尝试调用 A 的已删除副本 ctor,并且该对的构造不正确。


libstdc++ 有(作为扩展)一些非标准的ctors。在latest doxygen(和4.8.2)中,我期望被调用的pair的构造函数(对标准要求的规则感到惊讶)是:

template<class _U1, class _U2,
         class = typename enable_if<__and_<is_convertible<_U1, _T1>,
                                           is_convertible<_U2, _T2>
                                          >::value
                                   >::type>
constexpr pair(_U1&& __x, _U2&& __y)
: first(std::forward<_U1>(__x)), second(std::forward<_U2>(__y)) { }

实际上是调用的是非标准的:

// DR 811.
template<class _U1,
         class = typename enable_if<is_convertible<_U1, _T1>::value>::type>
constexpr pair(_U1&& __x, const _T2& __y)
: first(std::forward<_U1>(__x)), second(__y) { }

根据标准,程序格式不正确,它仅仅被这个非标准的ctor拒绝。


最后,这里是is_constructibleis_convertible 的规范。

is_constructible[meta.rel]/4

给定以下函数原型:

template <class T>
typename add_rvalue_reference<T>::type create();

模板特化is_constructible&lt;T, Args...&gt; 的谓词条件当且仅当以下变量定义对于某个发明变量t 的格式正确:

T t(create<Args>()...);

[注意:这些标记永远不会被解释为函数声明。 — 结束说明] 访问检查就像在与T 和任何Args 无关的上下文中执行。只考虑变量初始化的直接上下文的有效性。

is_convertible[meta.unary.prop]/6:

给定以下函数原型:

template <class T>
typename add_rvalue_reference<T>::type create();

模板特化is_convertible&lt;From, To&gt;的谓词条件必须满足,如果并且 仅当以下代码中的返回表达式格式正确时,包括任何隐式转换 到函数的返回类型:

To test() {
  return create<From>();
}

[注意:此要求为引用类型、void 类型、数组类型和函数类型提供了明确定义的结果。 — 尾注] 访问检查就像在与ToFrom 无关的上下文中执行。仅有的 return-statement 表达式的直接上下文的有效性(包括转换为 返回类型)被考虑。


对于您的类型A

A t(create<int>());

格式正确;然而

A test() {
  return create<int>();
}

创建一个A 类型的临时变量,并尝试将其移动 到返回值中(复制初始化)。这选择了已删除的 ctor A(A&amp;&amp;),因此格式错误。

【讨论】:

  • 多么奇怪的问题:(
  • @ker 谢谢。尽管我认为如果不存在那个非标准的ctor,那么应该使用带两个const&amp; 的ctor,这也会失败。所以我认为问题在于拒绝U1&amp;&amp;, U2&amp;&amp; ctor,尽管这似乎是一个合理的选择。我也猜想 libstdc++ 的实现者会“责备”标准,但如果is_convertible(或标准中的相应措辞)是有意的,这可能会引发讨论。
  • 有意使用is_convertible,正如N4064 所阐明的那样,is_convertible 的使用非常明确,但也改进了这方面的内容,以便为不可复制选择更好的构造函数, 不可移动的类型。
  • @dyp,下次发N4064会有新的修改,下个月的会议大概会接受。至于指责标准,删除is_convertible 约束将破坏比试图在地图中放置不可移动对象的程序更重要的程序。这些约束被添加到标准中是有充分理由的,libstdc++ 和 libc++ 都出于充分的理由遵守它们,而不仅仅是为了学究气地遵循标准。如果不遵循标准,我不确定您希望实现做什么。
  • @JonathanWakely 看了N3680,明白了为什么使用is_convertible。请不要把我的“责备标准”看得太认真。感谢您提供有关 N4064 状态的信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-13
  • 2018-01-29
  • 1970-01-01
  • 2018-01-23
相关资源
最近更新 更多