移动和复制语义的构造函数实现首选项 C++

Constructor implementation preference for move and copy semantics C++

提问人:huzzm 提问时间:6/24/2023 最后编辑:huzzm 更新时间:6/25/2023 访问量:214

问:

每当我想实现一个需要移动构造函数和某种形式的复制构造函数的类时,我都会发现自己想知道以下几点:

对于存储 的示例类,我是否应该更喜欢:Cstd::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 构造函数是直截了当的,但对于另一个构造函数,则略有不同:第一个选项将在调用构造函数时复制,然后将副本移动到 .对于第二个选项,我通过指定参数来避免复制,只接受引用,然后自己复制内容。因此,无论如何,我最终都会为我的“复制构造函数”(真实的和虚假的)进行一个副本。也许对内部发生的搬家成本的解释会有所帮助,但我也觉得不知道正确的问题是什么。_sC(std::vector<std::string> s) : _s(std::move(s)) {}

根据我的理解,我真的看不出更好的选择是什么。

C++ 构造函数 移动语义

评论

1赞 Eljay 6/24/2023
有一些强烈的思想流派有不同的意见。对于接收器参数,我倾向于作为模式。无需变体。C++23 将提供更好的选择,但我被困在 C++17 领域。相反的思想流派将只有版本,并强制调用站点明确地执行所需的操作。C(std::vector<std::string> s) : _s{std::move(s)} {}std::vector<std::string>&&&&
2赞 Jarod42 6/24/2023
pass-by-value-vs-pass-by-rvalue-referencetaking-sink-parameters-by-rvalue-reference-instead-of-by-value-to-enforce-performance 相关

答:

3赞 Hari 6/24/2023 #1

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 的回答中也有所提及。

评论

1赞 Brian Bi 6/24/2023
对于赋值运算符来说,是的,按值获取参数可能是一种悲观,因为当你这样做时,你会强制对参数进行复制构造,而复制构造可能比最佳复制赋值慢得多。但是,当你实现构造函数本身时,按值获取参数不太可能是悲观的。无论如何,您都必须构造成员。
0赞 Hari 6/24/2023
与赋值运算符的类比是比较只有 ,而不是 和。就像基于复制和交换的统一赋值运算符在传递右值时导致参数的浪费移动构造一样,也会做同样的事情。此外,该演讲对当前的讨论很有见地,但它通过特殊的成员函数和复制和交换习语传达了其信息。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)) {}
1赞 Brian Bi 6/24/2023
但额外的移动结构很少重要。您会注意到 Hinnant 实际上并不建议对构造函数使用这种模式。
0赞 Hari 6/25/2023
还行。点指出。我会更好地调查和理解这方面。
3赞 HolyBlackCat 6/24/2023 #2

对于初学者来说,这里没有移动构造函数。那将是.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)。

硬核低级库代码可能会使用更复杂的东西(一组转发引用,带有适当的 、 、 也许 ,也许是第二个重载作为第一个参数)。requiresnoexceptexplicit(bool)std::initializer_list<T>