【问题标题】:Move constructor in initializator list with a vector使用向量在初始化器列表中移动构造函数
【发布时间】:2012-12-14 12:35:27
【问题描述】:

使用此示例:

// test.cpp

#include <iostream>
#include <vector>
#include <utility>

using namespace std;

class mystring : public string { public:
    mystring() = default;

    mystring(const char* c) : string(c) {}

    mystring(mystring& m) : string(m) { cout << "Reference" << endl; }
    mystring(mystring const & m) : string(m) { cout << "Const reference" << endl; }
    mystring(mystring&& m) : string(move(m)) { cout << "Move" << endl; }
};

int main() {
    mystring a;

    vector<mystring> v{ a };
}

输出是:

$ g++ --version | grep "g++"
g++ (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2
$ g++ -std=c++11 -fno-elide-constructors test.cpp
$ ./a.out
Reference
Move
Const reference

但是,如果我用 r 值初始化 v:

vector<mystring> v{"hello"};

输出是:

$ g++ -std=c++11 -fno-elide-constructors test.cpp
$ ./a.out
Const reference

这意味着,没有副本。使用 r 值:

vector <mystring> v{mystring()};

输出:

$ g++ -std=c++11 -fno-elide-constructors test.cpp
$ ./a.out
Move
Move
Const reference

我不明白两件事:

  • 为什么使用非原始字符串执行第二次移动(在第一次复制/移动之前)?
  • 为什么对于原始字符串,不执行“mystring”的副本?

【问题讨论】:

  • "但是,如果我用右值初始化 v:" 我不认为你的意思是右值
  • @JonathanWakely 这是一个“值”,一个“未命名”对象,“非左值”,一个“prvalue”。我想我的意思是右值:)如果我错了,我必须从一开始就学习 C++:P
  • 好的,但是您将"hello"mystring() 都称为“具有 r 值”,这有助于区分这些情况
  • 但两者都是右值,不是吗?第一个是恒定的 r 值,第二个是可修改的 r 值,不是吗?
  • 我的意思是,您可以将它们称为“字符串文字”和“右值 mystring”,以区分它们是两个非常不同的事物这一事实

标签: c++ c++11 copy move initializer-list


【解决方案1】:

注意原始字符串在 C++11 中的含义有所不同,如果您在谈论 "hello"

,我认为您的意思是字符串文字
  • 为什么对于原始字符串,不执行“mystring”的副本?

诶?有一份,所以打印Const Reference

vector<mystring> v{"hello"};

braced-init-list 使用单个元素初始化 std::initializer_list&lt;mystring&gt; 构造函数参数,该元素使用 mystring(const char* c) 构造函数构造,然后使用 mystring(const mystring&amp;) 构造函数将该元素复制到向量中,该构造函数打印 @987654327 @。很简单。

  • 为什么使用非原始字符串执行第二次移动(在第一次复制/移动之前)?

第二步怎么会在第一步之前?! :)

这是initializer_list如何构造的实现细节,这通常无关紧要,因为多余的动作会被省略。

vector <mystring> v{mystring()};

这会使用默认构造函数创建一个临时的mystring,然后使用单个元素创建std::initializer_list&lt;mystring&gt;,该元素是使用mystring(mystring&amp;&amp;) 构造函数构造的,并将临时作为其参数。该移动构造打印Move。内部还有另一个动作,再次打印Move。然后像以前一样将initializer_list元素复制到向量中,打印Const Reference

回答您的评论:

您看到的额外移动也发生在第一种情况下,但在这种情况下,被移动的类型是const char*,因此它不会打印Move,即语句std::vector&lt;mystring&gt;{ expr };可以被认为是:

auto tmp = expr;
mystring tmparray[] = { std::move(tmp) };
std::initializer_list<mystring> init_list( tmparray, 1 };
std::vector<mystring> v(init_list);

expra(这是一个非常量左值)创建tmp 打印Reference 然后创建tmparray 打印Move 然后将tmparray[0] 复制到向量打印Const Reference

expr"hello" 时,创建tmp 的元素不打印任何内容,创建tmparray 调用不打印任何内容的mystring(const char*),然后将tmparray[0] 复制到矢量打印中@ 987654355@.

exprmystring() 创建tmp 打印Move 然后创建tmparray 打印Move 然后将tmparray[0] 复制到矢量打印Const Reference

使用像 { expr1, expr2 } 这样的两个元素支撑初始化列表,它可以是:

auto tmp1 = expr1;
auto tmp2 = expr2;
mystring tmparray[] = { std::move(tmp1), std::move(tmp2) };
std::initializer_list<mystring> init_list( tmparray, 2 };
std::vector<mystring> v(init_list);

因此,如果您使用{ "hello", mystring() },您将得到Move 打印一次,tmp2Move 打印一次,由tmparrayConst Reference 打印两次,由v 的元素打印。

当然,通常所有这些都会被省略掉,因此没有不必要的复制或移动。 -fno-elide-constructors 除了找出会发生的事情之外并没有真正的用处,但实际上并没有发生!

【讨论】:

  • 为什么用 {mystring()} 复制临时文件,而用 {"hello"} 不复制临时文件 (mystring("hello"))?
  • 但是如果我有一个像 {"hello", mystring()} 这样的列表呢? auto 将是“mystring”类型,因此“hello”将是代码第一行中的“mystring”,并移至第二行。它不匹配。但我可以看到是实现定义的。也许,前两行是针对列表中的每个元素执行的,但不像数组。在这种形式中,描述与“经验”相匹配。
  • 是的,实际上tmparray 不应该是一个数组,它更像tuple&lt;T...&gt;,所以每个元素可以是不同的类型。给定{ "hello", mystring },它将是tuple&lt;const char*, mystring&gt;,因此将打印一次Move,然后tmparray2 也将打印一次Move。我会更新答案。
【解决方案2】:

您没有正确地将移动构造函数标记为noexcept

【讨论】:

  • noexcept 和我的问题有什么关系?更改后,结果相同。
  • 将它标记为noexcept 是一个非常好的主意,但实际上并不是必需的,并且将其省略也不是不正确的(尽管它确实阻止了容器可以对不可移动元素进行的某些重要优化-可构造)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-06-13
  • 2014-11-01
  • 1970-01-01
  • 2020-12-27
  • 1970-01-01
  • 1970-01-01
  • 2019-03-04
相关资源
最近更新 更多