通过值传递和 std: : move 超过通过引用传递的优点

我现在正在学习 C + + ,尽量避免养成坏习惯。 据我所知,clang-clean 包含许多“最佳实践”,我尽可能地坚持它们(尽管我不一定理解 为什么,但它们被认为是好的) ,但我不确定我是否理解这里推荐的做法。

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

class Creature
{
private:
std::string m_name;


public:
Creature(const std::string &name)
:  m_name{name}
{
}
};

这导致一个来自 clang-clean 的建议,即我应该通过值而不是引用传递,并使用 std::move。 如果我这样做,我得到的建议,使 name一个参考(以确保它不会被复制每次)和警告,std::move将不会有任何影响,因为 name是一个 const,所以我应该删除它。

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

Creature(std::string name)
:  m_name{std::move(name)}
{
}

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

但是请注意,在标准库中,move 意味着 move-from 对象处于有效但未指定的状态。这意味着,在执行这样的操作之后,只能销毁或分配移出对象的值; 否则访问该对象将产生一个未指定的值。

现在想象一下这个代码:

std::string nameString("Alex");
Creature c(nameString);

因为 nameString是通过值传递的,所以 std::move只会使构造函数中的 name失效,而不会触及原始字符串。但这样做有什么好处呢?似乎内容只被复制了一次——如果我在调用 m_name{name}时通过引用传递,如果我通过值传递(然后它被移动)。我知道这比通过值传递和不使用 std::move要好(因为它被复制了两次)。

有两个问题:

  1. 我没理解错吧?
  2. 通过引用传递 std::move并只调用 m_name{name}有什么好处吗?
53549 次浏览
  1. 我没理解错吧?

是的。

  1. 通过引用传递 std::move并只调用 m_name{name}有什么好处吗?

容易掌握的函数签名没有任何额外的重载。签名立即显示参数将被复制——这避免了调用者想知道 const std::string&引用是否可以作为数据成员存储,以后可能成为悬空引用。而且在向函数传递 rvalue 时,不需要重载 std::string&& nameconst std::string&参数以避免不必要的副本。传递左值

std::string nameString("Alex");
Creature c(nameString);

到通过值获取其参数的函数,导致一个副本和一个移动构造。向同一个函数传递一个右值

std::string nameString("Alex");
Creature c(std::move(nameString));

造成两个移动结构。相反,当函数参数为 const std::string&时,即使在传递 rvalue 参数时,也始终存在一个副本。只要参数类型便于移动构造(std::string就是这种情况) ,这显然是一个优势。

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

void setName(std::string name)
{
m_name = std::move(name);
}

将导致重新分配 m_name引用的资源的释放。我推荐阅读《有效的现代 C + + 》中的第41条,也推荐阅读 这个问题

How you pass is not the only variable here,you pass make the big different between the two.

在 C + + 中,我们有 各种价值范畴,这个“习惯用法”存在于传入 价值(例如 "Alex-string-literal-that-constructs-temporary-std::string"std::move(nameString))的情况下,这导致 std::string0份被生成(对于 rvalue 参数,这种类型甚至不必是可复制构造的) ,并且只使用 std::string的 move 构造函数。

有些相关的问与答。

/* (0) */
Creature(const std::string &name) : m_name{name} { }
  • 传递的 Lvalue绑定到 name,然后 复制绑定到 m_name

  • 传递的 价值绑定到 name,然后 复制绑定到 m_name


/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
  • 一个通过的 Lvalue复制name,然后是 感动m_name

  • 一个通过的 价值感动name,然后是 感动m_name


/* (2) */
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • 传递的 Lvalue绑定到 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)} { }

您可以选择约束 T,以便限制此构造函数可以实例化的类型的域(如上所示)。C + + 20的目标是用 概念简化这个过程。


在 C + + 17中,价值受到 保证副本省略的影响,在适用的情况下,保证副本省略将减少向函数传递参数时的拷贝/移动次数。

与 pass-by-(rv)引用相比,pass-by-value-and-move 方法有几个缺点:

  • 它会产生3个对象,而不是2个;
  • 通过值传递对象可能导致额外的堆栈开销,因为即使是常规的字符串类通常也至少比指针大3或4倍;
  • 参数对象的构造将在调用方完成,从而导致代码膨胀;