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

Is std::move really needed on initialization list of constructor for heavy members passed by value?

本文关键字:真的 std move 是否 按值传递 成员 构造函数 列表 初始化      更新时间:2023-10-16

最近我读了一个来自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::forward

内使用

但我不确定:通过值传递是不是通用引用?

(更新)

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

在这个例子中,我们没有使用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?
}

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

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

一般来说,当你想把左值转换成右值时,那么是的,你需要一个std::move() 。参见c++ 11编译器会在代码优化期间将局部变量转换为右值吗?

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

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

传递的参数

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

请参阅下面的代码,以获得一个令人失望的示例。在这个例子中我没有使用std::string,因为这是一个使用内联汇编代码进行了大量优化的类,经常产生违反直觉的生成代码。更糟糕的是,std::string至少在gcc 4.7.2中是一个引用计数的共享指针(写时复制优化,现在被std::string的2011标准所禁止)。所以没有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("%sn", buffer);
   delete[] buffer;
}
显然,根据"as-if"规则,生成的代码可以被优化为:
int main() {
   std::printf("string literaln");
}

我在启用了链接时间优化(LTO)的GCC 4.9.0和Clang 3.5中尝试过,它们都无法将代码优化到这个级别。我查看了生成的汇编代码:它们都在堆上分配了内存并进行了复制。是啊,真让人失望。

Stack分配的内存不同:

#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("%sn", buffer);
}

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

因此,我期望可以消除示例代码中多余的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
}

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

DR1579修订的当前规则是,当NRVOable局部变量或参数或引用局部变量或参数的id-expressionreturn语句的参数时,将发生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;
   }
}

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

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

也许你有兴趣提交一个提案,以便在std提案邮件列表中讨论?

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

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

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

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

如果省略了移动,语言要求必须打印copy ctor这里没有选项。编译器不能这样做。

是的,复制省略仍然适用。但不是在你的情况下(初始化列表),见注释


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

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

考虑这个类,它包含两个字符串(即两个"heavy"对象复制)。

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

啊,这太糟糕了。其实只需要两份,却复印了四份。在c++ 03中,我们会立即将Foo()参数转换为const-ref。

2

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

现在我们有

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

那好多了!

但是动作呢?例如,from temporary:

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

或者当显式地将左值移动到元素中时:

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

这不是很好。我们可以使用string的move函数直接初始化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个向量吗?

取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!
}

我告诉过你为什么编译器不能这样做,但是我明白你的意思。从某种角度来看,这是有意义的——让s比它的最后一次使用寿命更长是无稽之谈。我想这是c++ 2x的思想食粮。

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

不幸的是,这个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)),则指针p在f1返回后将不再有效。