为什么在 Herb Sutter 的 CppCon 2014 演讲(回归基础:现代 C++ 风格)中不推荐使用 setter 成员函数? [英] Why is value taking setter member functions not recommended in Herb Sutter's CppCon 2014 talk (Back to Basics: Modern C++ Style)?

查看:20
本文介绍了为什么在 Herb Sutter 的 CppCon 2014 演讲(回归基础:现代 C++ 风格)中不推荐使用 setter 成员函数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 Herb Sutter 的 CppCon 2014 演讲中,回归基础:现代 C++ 风格,他在幻灯片 28(此处提供幻灯片的网络副本) 到此模式:

班级员工{std::string name_;上市:void set_name(std::string name) noexcept { name_ = std::move(name);}};

他说这是有问题的,因为当使用临时变量调用 set_name() 时,noexcept-ness 不强(他使用短语noexcept-ish").

现在,我在自己最近的 C++ 代码中大量使用了上述模式,主要是因为它节省了我每次输入 set_name() 的两个副本 - 是的,我知道通过强制复制构造可能会有点低效每次,但嘿,我是一个懒惰的打字员.然而 Herb 的短语This noexcept is problematic"让我担心,因为我在这里没有遇到问题:std::string 的移动赋值运算符是 noexcept,它的析构函数也是如此,所以上面的 set_name() 似乎是我保证没有例外.我确实看到编译器 before set_name() 在准备参数时抛出了一个潜在的异常,但我很难将其视为有问题.

稍后在幻灯片 32 Herb 明确指出上述是一种反模式.有人可以向我解释为什么我因为懒惰而写出糟糕的代码吗?

解决方案

其他人已经涵盖了上面的 noexcept 推理.

Herb 在效率方面的演讲中花费了更多时间.问题不在于分配,而在于不必要的解除分配.当您将一个 std::string 复制到另一个时,如果有足够的空间来保存正在复制的数据,则复制例程将重用目标字符串的分配存储空间.在进行移动分配时,目标字符串的现有存储空间必须被释放,因为它会从源字符串中接管存储空间.复制和移动"习语强制解除分配总是发生,即使您没有传递临时文件.这就是后面演讲中所展示的可怕表现的根源.他的建议是改用 const 引用,如果您确定需要它,则对 r 值引用进行重载.这将为您提供两全其美的好处:复制到现有存储中的非临时数据避免解除分配并移动临时数据,您将以一种或另一种方式为解除分配付费(目标在移动之前解除分配或源在复制后解除分配).

以上不适用于构造函数,因为成员变量中没有要释放的存储空间.这很好,因为构造函数通常采用多个参数,如果您需要为每个参数执行 const ref/r-value ref 重载,最终会导致构造函数重载的组合爆炸.

现在的问题变成:有多少类在复制时重用像 std::string 这样的存储?我猜 std::vector 确实如此,但除此之外我不确定.我知道我从来没有写过一个像这样重用存储的类,但我写了很多包含字符串和向量的类.对于不重用存储的类,遵循 Herb 的建议不会伤害您,您将首先使用 sink 函数的复制版本进行复制,如果您确定复制对性能的影响太大,那么您将进行 r 值引用重载以避免复制(就像对 std::string 所做的那样).另一方面,使用复制和移动"确实对 std::string 和其他重用存储的类型有性能影响,并且这些类型可能在大多数人的代码中得到了大量使用.我现在正在遵循 Herb 的建议,但在我认为问题完全解决之前需要再仔细考虑其中的一些(可能有一篇我没有时间写的博文潜伏在这一切中).

In Herb Sutter's CppCon 2014 talk Back to Basics: Modern C++ Style he refers on slide 28 (a web copy of the slides are here) to this pattern:

class employee {
  std::string name_;
public:
  void set_name(std::string name) noexcept { name_ = std::move(name); }
};

He says that this is problematic because when calling set_name() with a temporary, noexcept-ness isn't strong (he uses the phrase "noexcept-ish").

Now, I have been using the above pattern pretty heavily in my own recent C++ code mainly because it saves me typing two copies of set_name() every time - yes, I know that can be a bit inefficient by forcing a copy construction every time, but hey I am a lazy typer. However Herb's phrase "This noexcept is problematic" worries me as I don't get the problem here: std::string's move assignment operator is noexcept, as is its destructor, so set_name() above seems to me to be guaranteed noexcept. I do see a potential exception throw by the compiler before set_name() as it prepares the parameter, but I am struggling to see that as problematic.

Later on on slide 32 Herb clearly states the above is an anti-pattern. Can someone explain to me why lest I have been writing bad code by being lazy?

解决方案

Others have covered the noexcept reasoning above.

Herb spent much more time in the talk on the efficiency aspects. The problem isn't with allocations, its with unnecessary deallocations. When you copy one std::string into another the copy routine will reuse the allocated storage of the destination string if there's enough space to hold the data being copied. When doing a move assignment the destination string's existing storage must be deallocated as it takes over the storage from the source string. The "copy and move" idiom forces the deallocation to always occur, even when you don't pass a temporary. This is the source of the horrible performance that is demonstrated later in the talk. His advice was to instead take a const ref and if you determine that you need it have an overload for r-value references. This will give you the best of both worlds: copy into existing storage for non-temporaries avoiding the deallocation and move for temporaries where you're going to pay for a deallocation one way or the other (either the destination deallocates prior to the move or the source deallocates after the copy).

The above doesn't apply to constructors since there's no storage in the member variable to deallocate. This is nice since constructors often take more than one argument and if you need to do const ref/r-value ref overloads for each argument you end up with a combinatorial explosion of constructor overloads.

The question now becomes: how many classes are there that reuse storage like std::string when copying? I'm guessing that std::vector does, but outside of that I'm not sure. I do know that I've never written a class that reuses storage like this, but I have written a lot of classes that contain strings and vectors. Following Herb's advice won't hurt you for classes that don't reuse storage, you'll be copying at first with the copying version of the sink function and if you determine that the copying is too much of a performance hit you'll then make an r-value reference overload to avoid the copy (just as you would for std::string). On the other hand, using "copy-and-move" does have a demonstrated performance hit for std::string and other types that reuse storage, and those types probably see a lot of use in most peoples code. I'm following Herb's advice for now, but need to think through some of this a bit more before I consider the issue totally settled (there's probably a blog post that I don't have time to write lurking in all this).

这篇关于为什么在 Herb Sutter 的 CppCon 2014 演讲(回归基础:现代 C++ 风格)中不推荐使用 setter 成员函数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆