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

Nia*_*las 62 c++ stl c++11

在Herb Sutter的CppCon 2014谈话回归基础:现代C++风格他在幻灯片28(幻灯片的网络副本在这里)上引用了这种模式:

class employee {
  std::string name_;
public:
  void set_name(std::string name) noexcept { name_ = std::move(name); }
};
Run Code Online (Sandbox Code Playgroud)

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

现在,我在我最近的C++代码中使用了上述模式,主要是因为它节省了我每次都输入两个set_name()的副本 - 是的,我知道每次强制复制构造都会有点效率低下,但是,嘿,我是一个懒惰的人.然而Herb的短语"这个noexcept是有问题的 "让我担心,因为我没有在这里得到问题:std :: string的移动赋值运算符是noexcept,因为它的析构函数,所以上面的set_name()似乎保证noexcept.我确实看到编译器 set_name()之前抛出了一个潜在的异常,因为它准备了参数,但我很难看到它有问题.

后来在幻灯片32上Herb明确指出上面是一个反模式.有人可以向我解释为什么我一直懒惰地编写糟糕的代码?

Bre*_*all 31

其他人已经涵盖了noexcept上述原因.

赫伯在关于效率方面的谈话上花了更多的时间.问题不在于分配,而在于不必要的解除分配.当您将一个复制std::string到另一个复制例程时,如果有足够的空间来容纳正在复制的数据,则复制例程将重用已分配的目标字符串存储.在执行移动分配时,必须取消分配目标字符串的现有存储,因为它从源字符串接管存储."复制和移动"习语强制释放总是发生,即使你没有通过临时.这是演讲后期演示的可怕表演的来源.他的建议是改为使用const ref,如果你确定需要它,那么r值参考就会超载.这将为您提供两全其美的选择:复制到非临时存储器的现有存储中,避免重新分配,移动临时工作,您将以某种方式支付解除分配费用(目的地在移动之前解除分配或来源在复制后解除分配).

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

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

  • 这是一个很好的答案。如果您可以稍微修改它以提及此“反模式”标签仅适用于 std::string 维护容量(如 Herb 所述),并且如果我们不使用能够重用其现有容量的复杂类型,并且总是无论如何都必须取消分配,我会将其标记为已接受的答案。谢谢,顺便说一句,你真的为我击中了头。 (2认同)

Vau*_*ato 9

考虑到为什么传递值可能比传递const引用更好的原因有两个.

  1. 更高效
  2. noexcept

在setter方法类型的成员的情况下std::string,他揭穿声称路过值通过显示通过由const引用通常产生较少的分配(至少对于更有效的std::string).

他还驳斥了要求,它允许setter方法是noexcept通过显示的noexcept声明是误导性的,因为异常仍然会出现在复制参数的过程.

因此他总结说,通过const引用是优先于通过值,至少在这种情况下.然而,他确实提到过价值传递对于施工人员而言是一种潜在的好方法.

我确实认为std::string单独的例子不足以推广到所有类型,但它确实质疑了按价值传递昂贵到复制但廉价移动参数的做法,至少在效率和异常原因方面.

  • 我发现效率论证非常引人注目,但你在问题中把它放在一边:)霍华德不喜欢类似(中)效率原因的复制和交换习惯用法.即使在`name_.capacity()> name.length()`的情况下,它也会执行分配(因此速度较慢并且可能会抛出),因此您只需要一个memcpy.仅这一点似乎足以将其标记为反模式,并争论提供两个重载 (3认同)
  • 通过值传递可以是"noexcept",但正如Herb在谈话中指出的那样,"不是真的",因为在许多情况下,必须传递副本,并且创建该副本可以抛出.当然,异常发生在调用之前,但它仍然发生. (3认同)
  • @JonathanWakely我也发现效率参数引人注目,但还不足以让我为我编写的每一个setter函数编写两个重载.如果基准测试显示它是一个问题,或者我试图让Boost库过去审查,那么我打扰,否则不是. (2认同)

Yak*_*ont 5

Herb有一个观点,即当你已经分配了存储空间时,取值按值可能效率低下并导致不必要的分配.但是接受const&它几乎同样糟糕,就像你使用原始C字符串并将其传递给函数一样,会发生不必要的分配.

你应该采取的是从字符串读取的抽象,而不是字符串本身,因为这是你需要的.

现在,你可以这样做template:

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

这是合理有效的.然后添加一些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); }
};
Run Code Online (Sandbox Code Playgroud)

所以我们在界面上得到错误而不是实现.

这并不总是实用的,因为它需要公开地公开实现.

这是string_view类型类可以进入的地方:

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 e-b; }
  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;
  }
};
Run Code Online (Sandbox Code Playgroud)

这样的对象可以直接从一个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()); }
};
Run Code Online (Sandbox Code Playgroud)

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

唯一的低效率是,如果你传入一个C风格的字符串指针,你有点不必要地重复它的大小两次(第一次寻找'\0',第二次复制它们).另一方面,这会为您的目标信息提供有关它的大小,因此它可以预先分配而不是重新分配.

  • @NiallDouglas 然而,`range_view`、`array_view` 和`string_view` 类在这个问题之外还有很多用途:获取范围、连续数组或字符串的非复制子部分的能力非常强大。这恰好是它们的另一种用途。 (2认同)