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

Advantages of pass-by-value and std::move over pass-by-reference

本文关键字:引用 std      更新时间:2023-10-16

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

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

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

这导致clang-time建议我应该传递值而不是引用,并使用std::move。如果我这样做了,我会得到建议,将name作为参考(以确保它不会每次都被复制),并警告std::move不会有任何效果,因为nameconst,所以我应该删除它。

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

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

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

尽管注意,在标准库中,移动意味着移动的from对象处于有效但未指定的状态。这意味着,在这样的操作之后,移动的对象的值只应被销毁或分配一个新的值;否则访问它会产生一个未指定的值。

现在想象一下这个代码:

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

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

所以有两个问题:

  1. 我是否正确理解了这里发生的事情
  2. 与通过引用传递和仅调用m_name{name}相比,使用std::move有什么好处吗
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • 传递的左值绑定到name,然后被复制m_name

  • 传递的右值name结合,然后被复制m_name中。


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • 传递的左值复制name中,然后被移动到m_name中。

  • 传递的右值移动name,然后被移动到m_name


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • 传递的左值绑定到name,然后被复制m_name

  • 传递的右值rname结合,然后移动m_name中。


由于移动操作通常比复制快,如果您通过大量临时操作,则(1)优于(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旨在通过Concepts来简化这一过程。


在C++17中,prvalues受到保证的副本省略的影响,如果适用,这将减少向函数传递参数时的副本/移动数量。

  1. 我是否正确理解了这里发生的事情

是。

  1. 与通过引用传递和仅调用m_name{name}相比,使用std::move有什么好处吗

一个易于掌握的函数签名,无需任何额外的重载。签名会立即显示参数将被复制-这使调用方不必怀疑const std::string&引用是否可能被存储为数据成员,以后可能会成为悬空引用。并且在向函数传递右值时,不需要重载std::string&& nameconst std::string&参数以避免不必要的复制。传递左值

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

对于按值获取参数的函数,将导致一次复制和一次移动构造。将右值传递给相同的函数

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

导致两个移动结构。相反,当函数参数为const std::string&时,将始终存在副本,即使在传递右值参数时也是如此。这显然是一个优势,只要参数类型可以廉价地移动构造(std::string就是这样)。

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

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

将导致m_name所引用的资源在重新分配之前被解除分配。我建议阅读有效的现代C++中的第41项以及这个问题。

你如何传递并不是这里唯一的变量,你传递的内容决定了两者之间的巨大差异。

在C++中,我们有各种类型的值类别,这种"习惯用法"适用于传递右值(如"Alex-string-literal-that-constructs-temporary-std::string"std::move(nameString))的情况,这会导致生成0个副本std::string(对于右值参数,该类型甚至不必是可复制构造的),并且只使用std::string的移动构造函数。

一些相关的问答;A.

传递值和移动方法相对于传递-(rv)参考有几个缺点:

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