拷贝列表初始化与直接列表初始化临时对象的区别

5

给定以下结构:

struct ABC
{
    ABC(){cout << "ABC" << endl;}
    ~ABC() noexcept {cout << "~ABC" << endl;}
    ABC(ABC const&) {cout << "copy" << endl;}
    ABC(ABC&&) noexcept {cout << "move" << endl;}
    ABC& operator=(ABC const&){cout << "copy=" << endl;}
    ABC& operator=(ABC&&) noexcept {cout << "move=" << endl;}
};

输出为:
std::pair<std::string, ABC> myPair{{}, {}};

是:

ABC
copy
~ABC
~ABC

当执行以下代码时:

std::pair<std::string, ABC> myPair{{}, ABC{}};

is:

ABC
move
~ABC
~ABC

在试图理解两者之间的区别时,我认为我已经确定了第一个案例使用的是复制列表初始化(copy-list-initialization),而第二个案例则使用了未命名临时对象的直接列表初始化(direct-list-initialization)(分别在这里的数字7和2:http://en.cppreference.com/w/cpp/language/list_initialization)。
寻找类似问题的答案,我发现了这个:为什么标准区分直接列表初始化和复制列表初始化?和这个:复制列表初始化在概念上调用复制构造函数吗?
这些问题中的答案讨论了对于复制列表初始化,使用显式构造函数会使代码不合法。实际上,如果我将ABC的默认构造函数设为显式,我的第一个示例将无法编译,但这可能是另一回事。
因此,问题是:为什么在第一个案例中临时对象被复制,而在第二个案例中被移动?是什么阻止了它在复制列表初始化的情况下被移动?
作为说明,以下代码:
std::pair<std::string, ABC> myPair = std::make_pair<string, ABC>({}, {});

这也会导致调用ABC的移动构造函数(而不是复制构造函数),但可能涉及不同的机制。

您可以尝试在 C++14 模式下使用 gcc-4.9.2 在以下网址测试代码:https://ideone.com/Kc8xIn

1个回答

8
通常,像{}这样的初始化列表不是表达式,也没有类型。如果你有一个函数模板。
template<typename T> void f(T);

如果您调用f( {} ),则不会为T推导类型,类型推导将失败。

另一方面,ABC{}是一个类型为ABC的prvalue表达式(“函数符号中的显式类型转换”)。对于像f( ABC{} )这样的调用,函数模板可以从此表达式中推导出类型ABC


在C++14和C++11中,std::pair有以下构造函数[pairs.pair];T1T2std::pair类模板的模板参数的名称:
pair(const pair&) = default;
pair(pair&&) = default;
constexpr pair();
constexpr pair(const T1& x, const T2& y);
template<class U, class V> constexpr pair(U&& x, V&& y);
template<class U, class V> constexpr pair(const pair<U, V>& p);
template<class U, class V> constexpr pair(pair<U, V>&& p);
template <class... Args1, class... Args2>
pair(piecewise_construct_t, tuple<Args1...>, tuple<Args2...>);
注意,有一个构造函数。
constexpr pair(const T1& x, const T2& y); // (C)
但是不。
constexpr pair(T1&& x, T2&& y);
相反,有一个完全的转发。
template<class U, class V> constexpr pair(U&& x, V&& y); // (P)
如果您尝试使用至少一个初始化器为花括号初始化列表的std::pair进行初始化,则构造函数(P)不可行,因为它无法推断其模板参数。
(C)不是构造函数模板。其参数类型T1 const&T2 const&由类模板参数固定。对于常量类型的引用可以从空的花括号初始化列表初始化。这将创建一个绑定到引用的临时对象。由于所引用的类型是const,因此(C)构造函数将其参数复制到类的数据成员中。
当你通过std::pair<T,U>{ T{}, U{} }初始化一对时,T{}U{}是prvalue表达式。构造函数模板(P)可以推断它们的类型并且是可行的。在类型推导后产生的实例化比(C)构造函数更匹配,因为(P)将产生rvalue-reference参数并将prvalue参数绑定到它们上面。另一方面,(C)将prvalue参数绑定到lvalue-references上。

那么,为什么通过std::pair<T,U>{ {}, U{} }调用的实时示例会移动第二个参数呢?

libstdc++定义了额外的构造函数。以下是其从78536ab78e开始的std::pair实现的摘录,省略了函数定义、一些注释和SFINAE。 _T1_T2std::pair类模板的模板参数的名称。

  _GLIBCXX_CONSTEXPR pair();

  _GLIBCXX_CONSTEXPR pair(const _T1& __a, const _T2& __b); // (C)

  template<class _U1, class _U2>
constexpr pair(const pair<_U1, _U2>& __p);

  constexpr pair(const pair&) = default;
  constexpr pair(pair&&) = default;

  // DR 811.
  template<class _U1>
constexpr pair(_U1&& __x, const _T2& __y); // (X)

  template<class _U2>
constexpr pair(const _T1& __x, _U2&& __y); // (E) <=====================

  template<class _U1, class _U2>
constexpr pair(_U1&& __x, _U2&& __y);      // (P)

  template<class _U1, class _U2>
constexpr pair(pair<_U1, _U2>&& __p);

  template<typename... _Args1, typename... _Args2>
    pair(piecewise_construct_t, tuple<_Args1...>, tuple<_Args2...>);

请注意(E)构造函数模板:它将复制第一个参数并完美地转发第二个参数。对于像std::pair<T,U>{ {}, U{} }这样的初始化,它是可行的,因为它只需要从第二个参数中推导出一种类型。与第二个参数相比,它也更匹配(C),因此总体上更好。
“DR 811”注释在libstdc++源代码中。它指的是LWG DR 811,它添加了一些SFINAE,但没有新的构造函数。
构造函数(E)和(X)是libstdc++的扩展。虽然我不确定它是否符合标准。
另一方面,libc++没有这些额外的构造函数。对于例子std::pair<T,U>{ {}, U{} },它将复制第二个参数

使用两个库实现的在线演示


1
如果无法从 {} 推断出 std::string,那么如何为 myPair{{}, ABC{}}; 调用选择完美转发构造函数呢? - Marc Andreson
@MarcAndreson 抱歉,是我看错了。如果你正在使用libstdc++,这可能是一个扩展。让我试着查一下。 - dyp
1
这些重载的原因主要是为了支持像 std::pair<std::unique_ptr<int>, int *> p(std::unique_ptr<int>(), 0); 这样的东西。请参见 PR 40925 - T.C.
@T.C. PR 40925似乎包含与LWG 811类似的示例,但您的评论末尾有一个逗号。是否有遗漏的内容? - dyp
@dyp 我链接到第8条评论,这是添加了那些重载的提交,并带有注释*Add,以便在存在“空指针”的情况下正确处理仅移动类型。 - T.C.
@T.C. 哦,谢谢。我没有想到要查看那个评论的提交/更改消息。我已经将其和示例添加到我的std-discussion电子邮件中。 - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接