我是否经常使用 std::move() ?

Am I using std::move() too often?

我终于觉得我理​​解了现代 C++ 中的移动语义,并且它在我编写代码的方式上发生了巨大的变化。现在,我正在开发一个使用依赖注入的应用程序,我正在结合我新发现的移动语义知识,但我最终使用 std::move() 太多以至于我担心我使用不正确。

以前,如果我想在我的对象中注入一个我需要一个副本的依赖项,我会这样编写我的构造函数:

class NeedsCopyOfFoo
{
public:
  NeedsCopyOfFoo(const Foo& foo)
    : m_myFoo{foo} {}
private:
  Foo m_myFoo;
};

现在,我的 class 看起来像这样:

class NeedsCopyOfFoo
{
public:
  NeedsCopyOfFoo(Foo foo)
    : m_myFoo{std::move(foo)} {}
private:
  Foo m_myFoo;
};

在我的设计中有 classes 需要多达三个或四个 class 类型的依赖项,我最终将它们全部移动。显然,如果我的构造函数的调用者无法使用右值调用构造函数,而且在构造 NeedsCopyOfFoo 对象后也不打算使用依赖项,我还需要在那里使用 std::move() , 以避免完全不必要的复制。

现代 C++ 应该是这样的吗? Bob 大叔是否提到 "Uses std::move() too often" 的代码味道?我是不是反应过度了,因为我还不习惯用这种新风格写作?

您的代码对较新的 C++ 标准反应过度。

将这些优化留给编译器;任何体面的副本都会消除多余的值副本,并且在某些情况下需要从 C++17 开始这样做。

有效的现代 C++ 就是编写更少的代码,而不是更多的代码。

如果您有任何疑问您的代码是 运行 缓慢的,因为大量的价值副本被盗用,那么请分析它。分析是您应该不时进行的事情。

TL;DR:如果您不关心完美的性能,那么

Class(const Foo& foo, const Bar& bar, ...) : m_myFoo{foo}, m_myBar{bar}, ...{...} {}

是您的构造函数。它需要 rvalues/lvalues 并且要花费你一份副本。它是您所能得到的最好的东西,让生活变得轻松,拥有轻松的生活还有很多话要说。


对于一个变量,我会设置一个重载,如

NeedsCopyOfFoo(Foo&& foo) : m_myFoo{std::move(foo)} {}
NeedsCopyOfFoo(const Foo& foo) : m_myFoo{foo} {}

这最多花费一次复制或一次移动操作,具体取决于传递给构造函数的对象类型。这是你能得到的最完美的。

不幸的是,这不能很好地扩展。当您开始添加更多您想要以相同方式处理的参数时,过载集会呈二次方增长。这一点都不好玩,因为 4 参数构造函数需要 16 次重载才能完美。为了解决这个问题,我们可以使用转发构造函数并使用 SFINAE 对其进行限制,以便它只采用您想要的类型。那会给你一个像

这样的构造函数
template<typename T, 
         typename U, 
         std::enable_if_t<std::is_convertible_v<T, Foo> &&
                          std::is_convertible_v<U, Bar>, bool> = true>
Class(T&& foo, U&& bar) : 
    m_myFoo{std::forward<T>(foo)}, 
    m_myBar{std::forward<U>(bar)} {}

这为您提供最佳性能,但如您所见,它非常冗长,需要您了解更多有关 C++ 的知识才能使用。

一方面,我认为您做的一切都是正确的。

但是,当然,如果复制你的类型和移动它一样快1,那么 std::move 就变得不必要了,这样的类型应该按值传递或通过 const 参考,具体取决于复制它的成本。

1 — 例如如果它是 class 不管理任何堆内存或其他资源