42

最近我从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";
    }

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

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

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

作为黄金法则,无论何时通过右值引用获取某些内容,都需要在 std::move 中使用它,而无论何时通过通用引用获取某些内容(即使用 && 推导出的模板类型),都需要在 std:: 中使用它向前

但我不确定:按值传递不是普遍参考?

[更新]

为了让我的问题更清楚。可以将构造函数参数视为 XValue - 我的意思是过期值吗?

在此示例中,我们不使用 AFAIK std::move

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

那么,这里是否需要:

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

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

4

4 回答 4

19

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

通常,当您想将左值转换为右值时,是的,您需要一个std::move(). 另请参阅C++11 编译器在代码优化期间是否可以将局部变量转换为右值?

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

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

在这里,我希望优化器消除多余的localALTOGETHER;不幸的是,实际情况并非如此。当堆内存发挥作用时,编译器优化变得棘手,请参阅BoostCon 2013 主题演讲:Chandler Carruth:优化 C++ 的紧急结构。我从 Chandler 的演讲中得出的一个结论是,当涉及到堆分配的内存时,优化器往往会放弃。

请参阅下面的代码以获取令人失望的示例。我没有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;
}

显然,根据“as-if”规则,生成的代码可以优化为:

int main() {
   std::printf("string literal\n");
}

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

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

#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);
}

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

因此,我期望local可以消除示例代码中的多余部分并不是完全不受支持的:正如我的后一个带有堆栈分配数组的示例所示,它可以完成。

想象一下数百个用 C++03 编写的 KLOC 项目——我们应该到处添加std::move吗?
[...]
但我不确定:按值传递不是普遍参考?

“想要速度?测量。” 霍华德·欣南特

您很容易发现自己处于进行优化只是为了发现您的优化使代码变慢的情况。:( 我的建议与 Howard Hinnant 的建议相同:衡量。

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

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

于 2014-05-29T11:07:33.703 回答
15

由DR1579修订的当前规则是,当 NRVOable 局部或参数或引用局部变量或参数的id 表达式是语句的参数时,将发生 xvalue 转换return

这很有效,因为很明显,在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;
   }
}

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

可以更改标准以指定局部变量的“最后”使用受xvalue转换的影响,这并不是完全不可能的。问题是定义“最后”用法什么。另一个问题是这在函数内具有非局部效应。在下方添加例如调试语句可能意味着您所依赖的 xvalue 转换不再发生。即使是单次出现的规则也行不通,因为一条语句可以执行多次。

也许您有兴趣在标准提案邮件列表上提交提案以供讨论?

于 2014-05-29T11:27:54.280 回答
14

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

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

这不仅仅是语言所说的——如果类型是这样的:

struct Foo {
    Foo() { cout << "ctor"; }
    Foo(const Foo &) { cout << "copy ctor"; }
    Foo(Foo &&) { cout << "move ctor"; }
};

copy ctor如果您省略移动,则必须打印的语言要求。这里没有选项。编译器无法做到这一点。

是的,复制省略仍然适用。但不是您的情况(初始化列表),请参阅评论。


或者您的问题是否涉及我们为什么要使用这种模式?

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

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

struct Foo {
     Foo(string s1, string s2)
         : m_s1{s1}, m_s2{s2} {}
private:
     string m_s1, m_s2;
};

那么让我们看看在各种情况下会发生什么。

拿 1

string s1, s2; 
Foo f{s1, s2}; // 2 copies for passing by value + 2 copies in the ctor

啊,这很糟糕。这里发生了 4 个副本,而真正需要的只有 2 个。在 C++03 中,我们会立即将 Foo() 参数转换为 const-refs。

拿 2

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}

现在我们有

Foo f{s1, s2}; // 2 copies in the ctor

那好多了!

但是动作呢?例如,来自临时工:

string function();
Foo f{function(), function()}; // still 2 copies in the ctor

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

Foo f{std::move(s1), std::move(s2)}; // still 2 copies in the ctor

那不是那么好。我们可以使用string'move 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{std::move(s1)}, m_s2{s2} {}
Foo(const string &s1, string &s2) : m_s1{s1}, m_s2{std::move(s2)} {}
Foo(string &&s1, string &&s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}

所以,好的,现在我们有了

Foo f{function(), function()}; // 2 moves
Foo f2{s1, function()}; // 1 copy + 1 move

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

拿4(好的)

让我们来看看:

Foo(string s1, string s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}

使用此版本:

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

由于移动非常便宜,因此这种模式可以充分受益避免组合爆炸。我们确实可以一路向下移动

我什至没有触及异常安全的表面。


作为更一般性讨论的一部分,现在让我们考虑以下代码段,其中所有涉及的类通过值传递复制 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
}

如果我的理解正确,您真的会希望编译器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!
}

我告诉过你为什么编译器不能这样做,但我明白你的意思。从某种角度来看,这是有道理的——让slive 比它最后一次使用的时间更长是无稽之谈。我想 C++2x 值得深思。

于 2014-05-29T12:58:45.387 回答
1

我做了一些进一步的调查并查询了网上的另一个论坛。

不幸的是,这似乎std::move是必要的,不仅因为 C++ 标准这么说,而且否则它会很危险:

(((来自comp.std.c++的Kalle Olavi Niemitalo的信用-他的回答在这里))

#include <memory>
#include <mutex>
std::mutex m;
int i;
void f1(std::shared_ptr<std::lock_guard<std::mutex> > p);
void f2()
{
    auto p = std::make_shared<std::lock_guard<std::mutex> >(m);
    ++i;
    f1(p);
    ++i;
}

如果 f1(p) 自动更改为 f1(std::move(p)),则互斥体将在第二个 ++i 之前解锁;陈述。

下面的例子似乎更现实:

#include <cstdio>
#include <string>
void f1(std::string s) {}
int main()
{
    std::string s("hello");
    const char *p = s.c_str();
    f1(s);
    std::puts(p);
}

如果 f1(s) 自动变为 f1(std::move(s)),那么在 f1 返回后指针 p 将不再有效。

于 2014-06-16T11:54:51.197 回答