提问人:huzzm 提问时间:6/24/2023 最后编辑:huzzm 更新时间:6/25/2023 访问量:214
移动和复制语义的构造函数实现首选项 C++
Constructor implementation preference for move and copy semantics C++
问:
每当我想实现一个需要移动构造函数和某种形式的复制构造函数的类时,我都会发现自己想知道以下几点:
对于存储 的示例类,我是否应该更喜欢:C
std::vector<std::string> _s
C(std::vector<std::string> s) : _s(std::move(s)) {}
C(std::vector<std::string>&& s) : _s(std::move(s)) {}
或
C(const std::vector<std::string>& s) : _s(s) {}
C(std::vector<std::string>&& s) : _s(std::move(s)) {}
?
move 构造函数是直截了当的,但对于另一个构造函数,则略有不同:第一个选项将在调用构造函数时复制,然后将副本移动到 .对于第二个选项,我通过指定参数来避免复制,只接受引用,然后自己复制内容。因此,无论如何,我最终都会为我的“复制构造函数”(真实的和虚假的)进行一个副本。也许对内部发生的搬家成本的解释会有所帮助,但我也觉得不知道正确的问题是什么。_s
C(std::vector<std::string> s) : _s(std::move(s)) {}
根据我的理解,我真的看不出更好的选择是什么。
答:
Howard Hinnant 在一次演讲中强烈建议单独实现每个特殊成员函数,而不是根据另一个函数实现一个。为了提高性能,他还不鼓励在赋值运算符中使用复制和交换习语,这类似于问题()中显示的第一个选项。这种统一的赋值运算符可以接受右值和左值。对于此构造函数也是如此。C(std::vector<std::string> s) : _s(std::move(s)) {}
他展示了基于包含向量的类的经验证据(就像问题中一样)。特别是,如果目标中的向量具有足够的容量,则基于复制和交换的赋值可能比专用赋值运算符慢八倍。因此,在接受右值的情况下,将不必要地将右值移动到参数(即左值),这绝对是不可取的。C(std::vector<std::string> s) : _s(std::move(s)) {}
s
因此,需要 lvalue 和 rvalue 参数的单独构造函数。在传递左值的情况下,可以分析并找出复制是否应该在构造函数参数阶段或初始化列表中进行。但是,将左值参数作为对常量左值的引用传递是惯用的,类似于复制构造函数的操作。
更新:HolyBlackCat 已经解决了设计的可扩展性问题,这非常重要。实现它的另一种方法是通过基于参数包的转发引用。但是,应该注意此类构造函数取代其他构造函数的陷阱,并且应使用 SFINAE 或概念 (C++20) 约束模板。Scott Meyers 的《有效的现代 C++》第 25、26 和 27 项讨论了这些方面。
更新:在下面的评论中,Brian Bi 提出了“您试图挽救的举动成本有多高?
C(const std::vector<std::string> &s) : _s(s) {}
C(std::vector<std::string>&& s) : _s(std::move(s)) {}
自
C(std::vector<std::string> s) : _s(std::move(s)) {}
事实上,按值传递产生的额外移动对于几种类型来说不是问题。因此,注意所涉及的类型很重要。上文第41项讨论了两种选择之间的争论,更一般地说,不仅仅是构造者。该书中的一些准则:
...使用重载或通用引用,而不是按值传递 除非已经证明价值传递的收益率是可以接受的 所需参数类型的高效代码。
...对于必须尽可能快的软件,按值传递可能会 不是一个可行的策略......
最后一点在 HolyBlackCat 的回答中也有所提及。
评论
C(std::vector<std::string> s) : _s(std::move(s)) {}
C(const std::vector<std::string> &s) : _s(s) {}
C(std::vector<std::string>&& s) : _s(std::move(s)) {}
C(std::vector<std::string> s) : _s(std::move(s)) {}
对于初学者来说,这里没有移动构造函数。那将是.C(C &&)
默认情况下,首选 lone
C(std::vector<std::string> s) : _s(std::move(s)) {}
这很好地扩展到 >1 参数。
如果您正在编写需要优化的代码(也称为低级库类),请执行以下操作:
C(const std::vector<std::string>& s) : _s(s) {}
C(std::vector<std::string>&& s) : _s(std::move(s)) {}
与上一个选项相比,这每次调用只节省一个移动。
这不能很好地扩展到 N>1 参数,需要 2N 重载或完美转发模板。
澄清一下,可扩展性并不是我推荐一个而不是另一个的原因。啰嗦是。
一般来说,我认为低级语言功能是针对低级库代码的。高级业务逻辑不应该关心如何更好地将参数传递给构造函数,它应该使用最简单的语法(选项 1)。这是大多数程序员大多数时候应该做的事情。
一些高级库代码将使用适度冗长的语法来提高效率(选项 2)。
硬核低级库代码可能会使用更复杂的东西(一组转发引用,带有适当的 、 、 也许 ,也许是第二个重载作为第一个参数)。requires
noexcept
explicit(bool)
std::initializer_list<T>
评论
C(std::vector<std::string> s) : _s{std::move(s)} {}
std::vector<std::string>&&
&&