此刻我正在学习C ++,并尝试避免养成不良习惯。 据我了解,clang-tidy包含许多“最佳实践”,并且我会尽力做到最好(即使我不一定理解为什么都被认为是好的),但是我不确定我是否了解这里的建议。
我在教程中使用了此类:
class Creature
{
private:
std::string m_name;
public:
Creature(const std::string &name)
: m_name{name}
{
}
};
这导致clang-tidy建议我应该按值传递而不是引用并使用std::move
。
如果可以,我得到建议将name
用作参考(以确保每次都不会被复制),并且警告std::move
无效,因为name
是const
,所以我应该将其删除。
我没有收到警告的唯一方法是完全删除const
:
Creature(std::string name)
: m_name{std::move(name)}
{
}
这似乎合乎逻辑,因为const
的唯一好处是可以防止与原始字符串混淆(因为我按值传递,所以不会发生)。
但我读了CPlusPlus.com:
尽管请注意-在标准库中-移动意味着从中移出的对象处于有效但未指定的状态。这意味着在执行此操作之后,仅应销毁移出对象的值或为其分配新值;否则将获得未指定的值。
现在想象一下这段代码:
std::string nameString("Alex");
Creature c(nameString);
由于nameString
被值传递,std::move
将仅使构造函数内部的name
无效,而不会触摸原始字符串。但是,这样做的好处是什么?内容似乎只能复制一次-如果我在调用m_name{name}
时通过引用传递,如果在传递时按值传递(然后它被移动)。我知道这比按值传递而不使用std::move
更好(因为它被复制了两次)。
有两个问题:
std::move
代替通过引用传递并仅调用m_name{name}
有什么好处吗?答案 0 :(得分:55)
/* (0) */
Creature(const std::string &name) : m_name{name} { }
传递的左值绑定到name
,然后复制到m_name
。
传递的 rvalue 绑定到name
,然后复制到m_name
。
/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
将传递的 lvalue 复制到name
,然后移动到m_name
。< / p>
通过的 rvalue 被移动到name
,然后被移动到m_name
。< / p>
/* (2) */
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
传递的左值绑定到name
,然后复制到m_name
。
传递的 rvalue 绑定到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旨在通过Concepts简化此过程。
在C ++ 17中, prvalues 受guaranteed copy elision的影响,在适用时,它将在将参数传递给函数时减少复制/移动的次数。
答案 1 :(得分:17)
- 我是否正确理解这里发生的事情?
是的
- 使用
std::move
代替通过引用传递并仅调用m_name{name}
有什么好处吗?
易于掌握的函数签名,没有任何其他重载。签名立即显示该参数将被复制-这使调用者不必怀疑const std::string&
引用是否可以作为数据成员存储,以后可能成为悬挂的引用。并且无需在std::string&& name
和const std::string&
参数上重载,以免在将右值传递给函数时避免不必要的复制。传递左值
std::string nameString("Alex");
Creature c(nameString);
对于按值接受参数的函数,将导致一复制和一移动构造。将右值传递给相同的函数
std::string nameString("Alex");
Creature c(std::move(nameString));
导致两个移动结构。相反,当函数参数为const std::string&
时,即使传递右值参数,也始终会有一个副本。只要参数类型对move-construct便宜(std::string
就是这种情况),这显然是一个优势。
但是要考虑一个缺点:对于将函数参数分配给另一个变量(而不是初始化变量)的函数,推理不起作用:
void setName(std::string name)
{
m_name = std::move(name);
}
将导致m_name
所引用资源的重新分配。我建议阅读《有效的现代C ++》中的第41项,也请阅读this question。
答案 2 :(得分:1)
传递方式并不是这里唯一的变量,传递的内容使两者之间有很大的区别。
在C ++中,我们有all kinds of value categories,并且当您传入 rvalue (例如"Alex-string-literal-that-constructs-temporary-std::string"
或std::move(nameString)
)的情况下,存在此“惯用语”,这会导致产生{strong> 0个副本的std::string
(对于rvalue参数,该类型甚至不必是可复制构造的),并且仅使用std::string
的move构造函数。
答案 3 :(得分:1)
“按值传递”方法相对于“(rv)传递”引用有几个缺点:
答案 4 :(得分:0)
在我的情况下,切换为按值传递然后执行std:move会导致Address Sanitizer中出现堆后释放错误。
https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165
所以,我已经关闭了它,以及clang-tidy中的建议。
https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332
答案 5 :(得分:-2)
我遇到了同样的问题,并用一个很酷的包装类解决了它。
https://stackoverflow.com/a/65357007/4037515
max-width
然后你可以这样使用它:
#pragma pack(push, 1)
template<class T>
class CopyOrMove{
public:
CopyOrMove(T&&t):m_move(&t),m_isMove(true){}
CopyOrMove(const T&t):m_reference(&t),m_isMove(false){}
bool hasInstance()const{ return m_isMove; }
const T& getConstReference() const {
return *m_reference;
}
T extract() && {
if (hasInstance())
return std::move(*m_move);
else
return *m_reference;
}
void fastExtract(T* out) && {
if (hasInstance())
*out = std::move(*m_move);
else
*out = *m_reference;
}
private:
union
{
T* m_move;
const T* m_reference;
};
bool m_isMove;
};
#pragma pack(pop)