为什么在Herb Sutter的CppCon 2014演讲中不推荐使用setter成员函数(回到基础:现代C ++风格)?

在Herb Sutter's CppCon 2014的演讲中回到基础:现代C ++风格,他在幻灯片28( 这里是幻灯片的networking副本)中提到了这种模式:

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

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

现在,我已经在我自己最近的C ++代码中大量使用了上面的模式,主要是因为它每次都省去了inputset_name()的两个副本 – 是的,我知道每次强制执行一个复制构造可能会有点低效,但是,嘿,我是一个懒惰的typer。 然而草药的短语“这noexcept是有问题的 ”担心我,因为我没有在这里得到问题:std :: string的移动赋值运算符是noexcept,因为它的析构函数,所以上面set_name()似乎是保证noexcept。 在编译参数之前,我确实看到了编译器 set_name() 之前抛出的一个潜在的exception,但是我很努力地认为这是个问题。

后来在幻灯片32草药明确指出,上述是一个反模式。 有人能向我解释为什么为什么我一直懒惰地写错代码?

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

Herb花费了更多的时间在谈论效率方面。 问题不在于分配,而在于不必要的释放。 将一个std::string复制到另一个时,如果有足够的空间来容纳正在复制的数据,则复制例程将重新使用目标string的已分配存储。 在执行移动分配时,目标string的现有存储必须从源string中接pipe存储,才能解除分配。 “复制和移动”习惯用法,即使没有通过临时性,也会强制释放。 这是演讲稍后演示的可怕表演的来源。 他的build议是采取一个const ref,如果你确定你需要它有r值参考的重载。 这将为您提供两全其美的解决scheme:复制到非临时对象的现有存储器中,避免重新分配,并移动到临时对象,然后您将以某种方式支付解除分配(或者目标在移动之前释放,或者源复制后)释放。

以上不适用于构造函数,因为在成员variables中没有存储来释放。 这是很好的,因为构造函数通常需要多个参数,并且如果您需要为每个参数执行const ref / r-ref ref重载,则最终会出现构造函数重载的组合爆炸。

现在的问题是:有多less类在复制时像std :: string那样重用存储? 我猜std :: vector的确如此,但是除此之外我不确定。 我知道我从来没有写过一个像这样重用存储的类,但是我写了很多包含string和向量的类。 按照Herb的build议,对于不重复使用存储的类不会伤害到你,你首先会复制sink函数的复制版本,如果你确定复制的性能太大,你会使一个r值引用重载,以避免复制(就像你对std :: string)。 另一方面,使用“copy-and-move”对std :: string和其他重用存储的types有一个明显的性能影响,这些types在大多数人的代码中可能看到很多用途。 我现在正在跟随Herb的build议,但是在我认为这个问题完全解决之前,需要考虑一些这个问题(可能有一个博客文章,我没有时间写这个)。

有两个原因考虑为什么通过价值可能比通过const参考传递更好。

  1. 更高效
  2. noexcept

std::stringtypes的成员的setter的情况下,他通过显示通过const引用通常产生更less的分配(至less对于std::string )来剥夺通过值传递更有效的说法。

他还驳斥了这样一种说法,即通过显示noexcept声明是误导性的,它允许setter不能noexcept ,因为在复制参数的过程中仍然会发生exception。

因此,他得出结论,至less在这种情况下,通过常量引用比通过价值更受欢迎。 不过,他确实提到,价值传递对于build设者来说是一个潜在的好方法。

我认为单独使用std::string的例子不足以推广到所有types,但是这确实会引起人们质疑按值传递昂贵到复制但便宜移动参数的做法,至less为了提高效率和例外的原因。

Herb有一个观点,那就是当你已经有了分配的存储空间的时候,通过价值获取效率可能是低效的,并且导致了不必要的分配。 但是,通过const&几乎一样的糟糕,就像你拿一个原始的Cstring并传递给函数一样,就会发生不必要的分配。

你应该采取的是从string中读取的抽象,而不是string本身,因为这是你所需要的。

现在,您可以将其作为template来执行此操作:

 class employee { std::string name_; public: template<class T> void set_name(T&& name) noexcept { name_ = std::forward<T>(name); } }; 

这是相当有效的。 然后添加一些SFINAE也许:

 class employee { std::string name_; public: template<class T> std::enable_if_t<std::is_convertible<T,std::string>::value> set_name(T&& name) noexcept { name_ = std::forward<T>(name); } }; 

所以我们在界面上得到错误,而不是执行。

这并不总是实用的,因为它要求公开地公开实施。

这是一个string_viewtypes可以进来的地方:

 template<class C> struct string_view { // could be private: C const* b=nullptr; C const* e=nullptr; // key component: C const* begin() const { return b; } C const* end() const { return e; } // extra bonus utility: C const& front() const { return *b; } C const& back() const { return *std::prev(e); } std::size_t size() const { return eb; } bool empty() const { return b==e; } C const& operator[](std::size_t i){return b[i];} // these just work: string_view() = default; string_view(string_view const&)=default; string_view&operator=(string_view const&)=default; // myriad of constructors: string_view(C const* s, C const* f):b(s),e(f) {} // known continuous memory containers: template<std::size_t N> string_view(const C(&arr)[N]):string_view(arr, arr+N){} template<std::size_t N> string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){} template<std::size_t N> string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){} template<class... Ts> string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){} template<class... Ts> string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){} string_view(C const* str):string_view(str, str+len(str)) {} private: // helper method: static std::size_t len(C const* str) { std::size_t r = 0; if (!str) return r; while (*str++) { ++r; } return r; } }; 

这样的对象可以直接从一个std::string或一个"raw C string"构造,并且几乎无成本地存储你需要知道的东西,以便从中产生一个新的std::string

 class employee { std::string name_; public: void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); } }; 

和现在我们的set_name有一个固定的接口(不是一个完美的向前的),它可以让它的实现不可见。

唯一的低效就是,如果你传递一个C风格的string指针,你有一些不必要的大小两次(第一次寻找'\0' ,第二次复制它们)。 另一方面,这给你的目标信息有多大,所以它可以预先分配,而不是重新分配。

你有两种方法来调用这些方法。

  • 使用rvalue参数,只要参数types的move constructor不带有问题(如果std::string最可能是noexcept),在任何情况下最好使用有条件的noexcept(确保参数是noexcept)
  • 使用lvalue参数,在这种情况下,参数types的copy constructor将被调用,几乎可以确定它将需要一些分配(可能会抛出)。

在这种情况下,可能会错过使用,最好避免。 这个class的客户端假设没有exception抛出,但是有效的,可编译的,而不是可疑的C++11可能抛出。