pass-by-value和std :: move优于pass-by-reference

Bla*_*bot 81 c++

我正在学习C++,并试着避免养成坏习惯.根据我的理解,clang-tidy包含许多"最佳实践",我尽可能地尽可能地坚持它们(即使我不一定理解为什么它们被认为是好的),但我不确定我是否了解这里推荐的内容.

我在教程中使用了这个类:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};
Run Code Online (Sandbox Code Playgroud)

这导致了一个讽刺的建议,即我应该通过值而不是引用和使用std::move.如果我这样做,我得到的建议做出name的引用(以确保它不会被复制每次),并警告说std::move将不会有任何影响,因为nameconst这样,我应该将其删除.

我没有得到警告的唯一方法是const完全删除:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}
Run Code Online (Sandbox Code Playgroud)

这似乎是合乎逻辑的,因为唯一的好处const是防止弄乱原始字符串(这不会因为我通过值传递而发生).但我在CPlusPlus.com看到了:

虽然注意到-in标准库移动意味着移动的对象保持有效但未指定的状态.这意味着,在这样的操作之后,移动对象的值应该只被销毁或分配一个新值; 访问它否则会产生一个未指定的值.

现在想象一下这段代码

std::string nameString("Alex");
Creature c(nameString);
Run Code Online (Sandbox Code Playgroud)

因为nameString通过值传递,std::move只会name在构造函数内部无效而不会触及原始字符串.但这有什么好处?看起来无论如何内容只被复制一次 - 如果我在调用时通过引用传递m_name{name},如果我在传递它时传递值(然后它被移动).我知道这比通过值而不是使用std::move(因为它被复制两次)更好.

所以有两个问题:

  1. 我是否正确理解这里发生了什么?
  2. 使用std::move过度引用和调用是否有任何好处m_name{name}

Vit*_*meo 68

/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
Run Code Online (Sandbox Code Playgroud)
  • 传递的左值绑定name,然后复制m_name.

  • 传递的右值绑定name,然后复制m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
Run Code Online (Sandbox Code Playgroud)
  • 一个通过左值复制name,然后移动m_name.

  • 一个通过右值移动name,然后移动m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
Run Code Online (Sandbox Code Playgroud)
  • 传递的左值绑定name,然后复制m_name.

  • 甲传递右值结合rname,然后被移动m_name.


由于移动操作通常比复制更快,(1 )如果你传递了很多临时数,则优于(0).(2)在复制/移动方面是最佳的,但需要重复代码.

完美转发可以避免代码重复:

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }
Run Code Online (Sandbox Code Playgroud)

您可以选择约束T以限制此构造函数可以实例化的类型的域(如上所示).C++ 20旨在通过Concepts简化这一过程.


在C++ 17中,prvalues受到保证副本省略的影响,在适用时,它会减少将参数传递给函数时的副本/移动次数.

  • @skytree:您不能从 const 对象移动,因为移动会改变源。这将编译,但它会复制。 (9认同)
  • @skytree:可以移动非 const 左值引用,但它具有潜在的危险和误导性,因为移动的对象不是临时的,并且函数的调用者可能希望重用它。不幸的是,我不知道有什么简单的方法来检测“不动的移动”。当心 :) (3认同)
  • 请注意,在这种情况下,您不需要* SFINAE 来完美前进。只需要消除歧义。当传递错误的参数时,它对潜在的错误消息很有帮助 (2认同)
  • 我们可以在 **(2)** 中写出:`Creature(const std::string &amp;name) : m_name{std::move(name)} { }` 吗? (2认同)

lub*_*bgr 22

  1. 我是否正确理解这里发生了什么?

是.

  1. 使用std::move过度引用和调用是否有任何好处m_name{name}

一个易于掌握的功能签名,没有任何额外的重载.签名会立即显示该参数将被复制 - 这样可以避免调用者想知道const std::string&引用是否可以存储为数据成员,以后可能成为悬空引用.当rvalues传递给函数时,不需要重载std::string&& nameconst std::string&参数来避免不必要的副本.通过左值

std::string nameString("Alex");
Creature c(nameString);
Run Code Online (Sandbox Code Playgroud)

按值获取其参数的函数会导致一个副本和一个移动构造.将右值传递给相同的函数

std::string nameString("Alex");
Creature c(std::move(nameString));
Run Code Online (Sandbox Code Playgroud)

导致两个移动结构.相反,当函数参数const std::string&为时,即使传递rvalue参数,也总会有副本.这显然是一个优势,只要参数类型移动构造便宜(这是这种情况std::string).

但是有一个缺点需要考虑:推理不适用于将函数参数分配给另一个变量的函数(而不是初始化它):

void setName(std::string name)
{
    m_name = std::move(name);
}
Run Code Online (Sandbox Code Playgroud)

将导致m_name在重新分配之前引用的资源的重新分配.我建议阅读Effective Modern C++中的第41项以及这个问题.