[英]Advantages of pass-by-value and std::move over pass-by-reference
I'm learning C++ at the moment and try avoid picking up bad habits.我目前正在学习 C++,并尽量避免养成坏习惯。 From what I understand, clang-tidy contains many "best practices" and I try to stick to them as best as possible (even though I don't necessarily understand why they are considered good yet), but I'm not sure if I understand what's recommended here.
据我了解,clang-tidy 包含许多“最佳实践”,我尽量坚持使用它们(尽管我不一定明白为什么它们被认为是好的),但我不确定我是否了解这里推荐的内容。
I used this class from the tutorial:我使用了教程中的这个类:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
This leads to a suggestion from clang-tidy that I should pass by value instead of reference and use std::move
.这导致 clang-tidy 建议我应该按值而不是引用传递并使用
std::move
。 If I do, I get the suggestion to make name
a reference (to ensure it does not get copied every time) and the warning that std::move
won't have any effect because name
is a const
so I should remove it.如果我这样做了,我会收到将
name
设为引用的建议(以确保它不会每次都被复制)以及std::move
不会有任何效果的警告,因为name
是一个const
所以我应该删除它。
The only way I don't get a warning is by removing const
altogether:我没有收到警告的唯一方法是完全删除
const
:
Creature(std::string name)
: m_name{std::move(name)}
{
}
Which seems logical, as the only benefit of const
was to prevent messing with the original string (which doesn't happen because I passed by value).这似乎是合乎逻辑的,因为
const
的唯一好处是防止弄乱原始字符串(这不会发生,因为我按值传递)。 But I read on CPlusPlus.com :但我在CPlusPlus.com上读到:
Although note that -in the standard library- moving implies that the moved-from object is left in a valid but unspecified state.
尽管请注意 - 在标准库中 - 移动意味着被移动的对象处于有效但未指定的状态。 Which means that, after such an operation, the value of the moved-from object should only be destroyed or assigned a new value;
这意味着,在这样的操作之后,被移动对象的值应该只被销毁或分配一个新值; accessing it otherwise yields an unspecified value.
否则访问它会产生一个未指定的值。
Now imagine this code:现在想象一下这段代码:
std::string nameString("Alex");
Creature c(nameString);
Because nameString
gets passed by value, std::move
will only invalidate name
inside the constructor and not touch the original string.因为
nameString
是按值传递的,所以std::move
只会使构造函数内的name
无效,而不会触及原始字符串。 But what are the advantages of this?但是这样做有什么好处呢? It seems like the content gets copied only once anyhow - if I pass by reference when I call
m_name{name}
, if I pass by value when I pass it (and then it gets moved).无论如何,内容似乎只被复制一次 - 如果我在调用
m_name{name}
时通过引用传递,如果我在传递它时通过值传递(然后它被移动)。 I understand that this is better than passing by value and not using std::move
(because it gets copied twice).我知道这比按值传递而不使用
std::move
更好(因为它被复制了两次)。
So two questions:所以两个问题:
std::move
over passing by reference and just calling m_name{name}
?std::move
通过引用传递和调用m_name{name}
有什么好处吗?/* (0) */
Creature(const std::string &name) : m_name{name} { }
A passed lvalue binds to name
, then is copied into m_name
.传递的左值绑定到
name
,然后复制到m_name
。
A passed rvalue binds to name
, then is copied into m_name
.传递的右值绑定到
name
,然后复制到m_name
中。
/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
A passed lvalue is copied into name
, then is moved into m_name
.传递的左值被复制到
name
中,然后被移动到m_name
中。
A passed rvalue is moved into name
, then is moved into m_name
.传递的右值被移动到
name
中,然后被移动到m_name
中。
/* (2) */
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
A passed lvalue binds to name
, then is copied into m_name
.传递的左值绑定到
name
,然后复制到m_name
。
A passed rvalue binds to rname
, then is moved into m_name
.传递的右值绑定到
rname
,然后移动到m_name
。
As move operations are usually faster than copies, (1) is better than (0) if you pass a lot of temporaries.由于移动操作通常比副本快,如果您通过大量临时对象, (1)优于(0) 。 (2) is optimal in terms of copies/moves, but requires code repetition.
(2)在复制/移动方面是最佳的,但需要代码重复。
The code repetition can be avoided with perfect forwarding :完美转发可以避免代码重复:
/* (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)} { }
You might optionally want to constrain T
in order to restrict the domain of types that this constructor can be instantiated with (as shown above).您可能希望约束
T
以限制可以实例化此构造函数的类型域(如上所示)。 C++20 aims to simplify this with Concepts . C++20 旨在通过概念简化这一点。
In C++17, prvalues are affected by guaranteed copy elision , which - when applicable - will reduce the number of copies/moves when passing arguments to functions.在 C++17 中, prvalues受保证复制省略的影响,当适用时,这将减少向函数传递参数时的复制/移动次数。
- Did I understand correctly what is happening here?
我是否正确理解了这里发生的事情?
Yes.是的。
- Is there any upside of using
std::move
over passing by reference and just callingm_name{name}
?使用
std::move
通过引用传递和调用m_name{name}
有什么好处吗?
An easy to grasp function signature without any additional overloads.一个易于掌握的函数签名,没有任何额外的重载。 The signature immediately reveals that the argument will be copied - this saves callers from wondering whether a
const std::string&
reference might be stored as a data member, possibly becoming a dangling reference later on.签名立即显示该参数将被复制 - 这使调用者不必怀疑
const std::string&
引用是否可能存储为数据成员,以后可能会成为悬空引用。 And there is no need to overload on std::string&& name
and const std::string&
arguments to avoid unnecessary copies when rvalues are passed to the function.并且不需要重载
std::string&& name
和const std::string&
参数以避免在将右值传递给函数时不必要的复制。 Passing an lvalue传递左值
std::string nameString("Alex");
Creature c(nameString);
to the function that takes its argument by value causes one copy and one move construction.通过值获取参数的函数会导致一个副本和一个移动构造。 Passing an rvalue to the same function
将右值传递给同一个函数
std::string nameString("Alex");
Creature c(std::move(nameString));
causes two move constructions.导致两个移动结构。 In contrast, when the function parameter is
const std::string&
, there will always be a copy, even when passing an rvalue argument.相反,当函数参数为
const std::string&
时,即使传递右值参数,也总会有一个副本。 This is clearly an advantage as long as the argument type is cheap to move-construct (this is the case for std::string
).只要参数类型对移动构造便宜(这是
std::string
的情况),这显然是一个优势。
But there is a downside to consider: the reasoning doesn't work for functions that assign the function argument to another variable (instead of initializing it):但是有一个缺点需要考虑:推理不适用于将函数参数分配给另一个变量(而不是初始化它)的函数:
void setName(std::string name)
{
m_name = std::move(name);
}
will cause a deallocation of the resource that m_name
refers to before it's reassigned.将导致
m_name
在重新分配之前引用的资源重新分配。 I recommend reading Item 41 in Effective Modern C++ and also this question .我建议阅读 Effective Modern C++ 中的第 41 条以及这个问题。
How you pass is not the only variable here, what you pass makes the big difference between the two.你如何通过不是这里唯一的变量,你通过什么使两者之间有很大的不同。
In C++, we have all kinds of value categories and this "idiom" exists for cases where you pass in an rvalue (such as "Alex-string-literal-that-constructs-temporary-std::string"
or std::move(nameString)
), which results in 0 copies of std::string
being made (the type does not even have to be copy-constructible for rvalue arguments), and only uses std::string
's move constructor.在 C++ 中,我们有各种各样的值类别,并且这种“惯用语”适用于传入右值的情况(例如
"Alex-string-literal-that-constructs-temporary-std::string"
或std::move(nameString)
),这会导致生成0 个std::string
副本(对于右值参数,该类型甚至不必是可复制构造的),并且只使用std::string
的移动构造函数。
There are several disadvantages of pass-by-value-and-move approach over pass-by-(rv)reference:与 pass-by-(rv)reference 相比,pass-by-value-and-move 方法有几个缺点:
In my case, switching to pass by value and then doing a std:move caused a heap-use-after-free error in Address Sanitizer.就我而言,切换到按值传递然后执行 std:move 会导致 Address Sanitizer 中出现 heap-use-after-free 错误。
https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165 https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165
So, I've turned it off, as well as the suggestion in clang-tidy.所以,我把它关掉了,还有clang-tidy中的建议。
https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332 https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332
I'm learning C++ at the moment and try avoid picking up bad habits.我目前正在学习C ++,并尝试避免养成不良习惯。 From what I understand, clang-tidy contains many "best practices" and I try to stick to them as best as possible (even though I don't necessarily understand why they are considered good yet), but I'm not sure if I understand what's recommended here.
据我了解,clang-tidy包含许多“最佳实践”,并且我会尽力做到最好(即使我不一定理解为什么它们仍然被认为是好的),但是我不确定我是否了解这里的建议。
I used this class from the tutorial:我从教程中使用了此类:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
This leads to a suggestion from clang-tidy that I should pass by value instead of reference and use std::move
.这导致clang-tidy建议我应该按值传递而不是引用,并使用
std::move
。 If I do, I get the suggestion to make name
a reference (to ensure it does not get copied every time) and the warning that std::move
won't have any effect because name
is a const
so I should remove it.如果这样做,我得到建议将
name
为引用(以确保每次都不会被复制),并且警告说std::move
不会起作用,因为name
是const
所以我应该将其删除。
The only way I don't get a warning is by removing const
altogether:我没有收到警告的唯一方法是完全删除
const
:
Creature(std::string name)
: m_name{std::move(name)}
{
}
Which seems logical, as the only benefit of const
was to prevent messing with the original string (which doesn't happen because I passed by value).这似乎是合乎逻辑的,因为
const
的唯一好处是可以防止与原始字符串混淆(因为我按值传递,所以不会发生)。 But I read on CPlusPlus.com :但是我在CPlusPlus.com上阅读:
Although note that -in the standard library- moving implies that the moved-from object is left in a valid but unspecified state.
尽管要注意-在标准库中-移动意味着从中移出的对象处于有效但未指定的状态。 Which means that, after such an operation, the value of the moved-from object should only be destroyed or assigned a new value;
这意味着,在执行此操作后,仅应销毁移出对象的值或为其分配新值; accessing it otherwise yields an unspecified value.
否则访问它会产生未指定的值。
Now imagine this code:现在想象一下这段代码:
std::string nameString("Alex");
Creature c(nameString);
Because nameString
gets passed by value, std::move
will only invalidate name
inside the constructor and not touch the original string.因为
nameString
是通过值传递的,所以std::move
将仅使构造函数中的name
无效,而不接触原始字符串。 But what are the advantages of this?但是,这样做的好处是什么? It seems like the content gets copied only once anyhow - if I pass by reference when I call
m_name{name}
, if I pass by value when I pass it (and then it gets moved).似乎该内容仅被复制一次-如果我在调用
m_name{name}
时通过引用传递,如果在传递它时按值传递(然后它被移动)。 I understand that this is better than passing by value and not using std::move
(because it gets copied twice).我知道这比按值传递而不使用
std::move
更好(因为它被复制了两次)。
So two questions:有两个问题:
std::move
over passing by reference and just calling m_name{name}
?std::move
通过引用传递和仅调用m_name{name}
什么m_name{name}
吗?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.