std :: move是否真的需要在构造函数的初始化列表中通过值传递的重成员?

Pio*_*ycz 34 c++ language-lawyer initialization-list move-semantics c++11

最近我从cppreference中读到了一个例子.../vector/emplace_back:

struct President
{
    std::string name;
    std::string country;
    int year;

    President(std::string p_name, std::string p_country, int p_year)
        : name(std::move(p_name)), country(std::move(p_country)), year(p_year)
    {
        std::cout << "I am being constructed.\n";
    }
Run Code Online (Sandbox Code Playgroud)

我的问题:这std::move真的需要吗?我的观点是,这p_name不是在构造函数体中使用,所以,也许,语言中有一些规则默认使用移动语义?

将std :: move初始化列表添加到每个重要成员(例如std::string,std::vector)会非常烦人.想象一下用C++ 03编写的数百个KLOC项目 - 我们是否应该添加到处std::move

这个问题:move-constructor-and-initialization-list答案说:

作为一个黄金法则,无论何时你通过右值引用来获取内容,你需要在std :: move中使用它,并且每当你通过通用引用(即用&&推导出模板化类型)时,你需要在std ::里面使用它向前

但我不确定:通过价值而不是普遍的参考?

[UPDATE]

使我的问题更清楚.构造函数参数可以被视为XValue - 我的意思是到期值?

在这个例子AFAIK我们不使用std::move:

std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}
Run Code Online (Sandbox Code Playgroud)

那么,这需要它:

void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}
Run Code Online (Sandbox Code Playgroud)

对我来说,这个局部变量是到期变量 - 所以可以应用移动语义......这类似于通过值传递的参数....

Ali*_*Ali 16

我的问题:这个std :: move真的需要吗?我的观点是编译器发现这个p_name没有在构造函数体中使用,所以,也许,默认情况下有一些规则可以使用移动语义吗?

一般来说,当你想将左值变成右值时,是的,你需要一个std::move().另请参阅Do C++ 11编译器在代码优化期间可以将局部变量转换为rvalues吗?

void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}
Run Code Online (Sandbox Code Playgroud)

对我来说,这个局部变量是到期变量 - 所以可以应用移动语义......这类似于通过值传递的参数....

在这里,我希望优化器能够消除多余的localALTOGETHER; 不幸的是,实际情况并非如此.当内存进入时,编译器优化会变得棘手,请参阅BoostCon 2013主题演讲:Chandler Carruth:优化C++的紧急结构.我对钱德勒谈话的一个看法是,优化器只是倾向于在堆分配内存时放弃.

有关令人失望的示例,请参阅下面的代码.我没有std::string在这个例子中使用,因为这是一个具有内联汇编代码的高度优化的类,通常会产生违反直觉的生成代码.为了增加对侮辱的伤害,std::string粗略地说,参考计数至少是gcc 4.7.2中的共享指针(写入时复制优化,现在禁止使用2011标准std::string).所以示例代码没有std::string:

#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   int len = sizeof literal;
   char* buffer = new char[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
   delete[] buffer;
}
Run Code Online (Sandbox Code Playgroud)

显然,根据"as-if"规则,生成的代码可以针对此进行优化:

int main() {
   std::printf("string literal\n");
}
Run Code Online (Sandbox Code Playgroud)

我已经尝试使用GCC 4.9.0和Clang 3.5并启用了链接时间优化(LTO),并且没有一个能够将代码优化到这个级别.我查看了生成的汇编代码:它们都在堆上分配内存并执行复制.嗯,是的,那令人失望.

堆栈分配的内存不同但是:

#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   const int len = sizeof literal;
   char buffer[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
}
Run Code Online (Sandbox Code Playgroud)

我检查了汇编代码:在这里,编译器能够将代码减少基本上std::printf("string literal\n");.

所以我对local你的示例代码中多余的东西可以被消除的期望并不是完全不受支持的:正如我的后一个带有堆栈分配数组的例子所示,它可以完成.

想象一下用C++ 03编写的数百个KLOC项目 - 我们是否应该添加到处std::move
[...]
但我不确定:通过价值而不是普遍的参考?

"想要速度?测量." (作者 Howard Hinnant)

您可以轻松地发现自己处于优化状态,只是为了发现优化使代码变慢.:(我的建议与Howard Hinnant的相同:措施.

std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}
Run Code Online (Sandbox Code Playgroud)

是的,但是我们有针对这种特殊情况的规则:它被称为命名返回值优化(NRVO).


eca*_*mur 14

DR1579修改的当前规则是当NRVOable本地或参数或引用局部变量或参数的id表达式return语句的参数时发生xvalue变换.

这是有效的,因为很明显,在return语句之后变量不能再次使用.

除非情况并非如此:

struct S {
    std::string s;
    S(std::string &&s) : s(std::move(s)) { throw std::runtime_error("oops"); }
};

S foo() {
   std::string local = "Hello SO!";
   try {
       return local;
   } catch(std::exception &) {
       assert(local.empty());
       throw;
   }
}
Run Code Online (Sandbox Code Playgroud)

因此,即使对于return语句,实际上也不能保证该语句中出现的局部变量或参数是该变量的最后一次使用.

毫无疑问,可以更改标准以指定局部变量的"最后"用法受xvalue转换的影响; 问题在于定义"最后"用法是什么.另一个问题是它在函数内具有非局部效应; 添加例如较低的调试语句可能意味着您不再依赖于您所依赖的xvalue变换.即使是单次出现的规则也行不通,因为单个语句可以多次执行.

也许您有兴趣提交有关std-proposals邮件列表的讨论提案?


pep*_*ppe 10

我的问题:这个std :: move真的需要吗?我的观点是这个p_name没有在构造函数体中使用,所以,也许,语言中有一些规则默认使用移动语义?

当然需要它.p_name是左值,因此std::move需要将其转换为右值并选择移动构造函数.

这不仅仅是语言所说的 - 如果类型如下:

struct Foo {
    Foo() { cout << "ctor"; }
    Foo(const Foo &) { cout << "copy ctor"; }
    Foo(Foo &&) { cout << "move ctor"; }
};
Run Code Online (Sandbox Code Playgroud)

copy ctor如果省略移动,则必须打印语言.这里没有选择.编译器不能做任何不同的事情.

是的,复制省略仍然适用.但不是在您的情况下(初始化列表),请参阅注释.


或者你的问题涉及我们为什么使用这种模式?

答案是,当我们想要存储传递参数的副本,同时受益于移动,并避免参数的组合爆炸时,它提供了一种安全模式.

考虑这个包含两个字符串的类(即要复制的两个"重"对象).

struct Foo {
     Foo(string s1, string s2)
         : m_s1{s1}, m_s2{s2} {}
private:
     string m_s1, m_s2;
};
Run Code Online (Sandbox Code Playgroud)

那么让我们看看在各种场景中会发生什么.

拿1

string s1, s2; 
Foo f{s1, s2}; // 2 copies for passing by value + 2 copies in the ctor
Run Code Online (Sandbox Code Playgroud)

唉,这很糟糕.这里发生了4个副本,当时只需要2个副本.在C++ 03中,我们立即将Foo()参数转换为const-refs.

拿2

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Run Code Online (Sandbox Code Playgroud)

现在我们有

Foo f{s1, s2}; // 2 copies in the ctor
Run Code Online (Sandbox Code Playgroud)

那好多了!

但是移动怎么样?例如,来自临时工:

string function();
Foo f{function(), function()}; // 2 moves + still 2 copies in the ctor
Run Code Online (Sandbox Code Playgroud)

或者当明确地将左值移动到ctor中时:

Foo f{std::move(s1), std::move(s2)}; // 2 moves + still 2 copies in the ctor
Run Code Online (Sandbox Code Playgroud)

那不是那么好.我们可以使用string移动ctor来直接初始化Foo成员.

拿3

所以,我们可以为Foo的构造函数引入一些重载:

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(const string &s1, string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, string &&s2) : m_s1{s1}, m_s2{s2} {}
Run Code Online (Sandbox Code Playgroud)

好吧,好吧,现在我们有了

Foo f{function(), function()}; // 2 moves
Foo f2{s1, function()}; // 1 copy + 1 move
Run Code Online (Sandbox Code Playgroud)

好.但是,我们得到了一个组合爆炸:现在每个参数必须出现在它的const-ref + rvalue变体中.如果我们得到4个字符串怎么办 我们要写16个ctors吗?

拿4(好的)

让我们来看看:

Foo(string s1, string s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}
Run Code Online (Sandbox Code Playgroud)

有了这个版本:

Foo foo{s1, s2}; // 2 copies + 2 moves
Foo foo2{function(), function()}; // 2 moves in the arguments + 2 moves in the ctor
Foo foo3{std::move(s1), s2}; // 1 copy, 1 move, 2 moves
Run Code Online (Sandbox Code Playgroud)

由于移动非常便宜,这种模式可以充分利用它们避免组合爆炸.我们确实可以一路向下移动.

我甚至没有抓住异常安全的表面.


作为更一般性讨论的一部分,我们现在考虑以下片段,其中涉及的所有类通过值传递s的副本:

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {s};

// s won't be touched any more from here on
}
Run Code Online (Sandbox Code Playgroud)

如果我找到你的话,你真的很喜欢编译器s最后的用法:

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {std::move(s)}; // bye bye s

// s won't be touched any more from here on. 
// hence nobody will notice s is effectively in a "dead" state!
}
Run Code Online (Sandbox Code Playgroud)

我告诉你为什么编译器不能这样做,但我明白你的观点.从某种观点来看这是有道理的 - 让它s比上一次使用更长时间是无意义的.我猜想C++ 2x值得深思.