我是否经常使用 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 不管理任何堆内存或其他资源
我终于觉得我理解了现代 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 不管理任何堆内存或其他资源