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

在 Herb Sutter 的 CppCon 2014演讲回到基础: 现代 C + + 风格中,他在幻灯片28(幻灯片的网页拷贝在这里)中提到了这种模式:

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

他说,这是有问题的,因为当使用临时命令调用 set _ name ()时,no竹-ness 并不强(他使用了短语“ no竹-ish”)。

现在,我已经在我最近的 C + + 代码中大量使用了上面的模式,主要是因为它让我每次输入两个 set _ name ()副本时节省了时间——是的,我知道每次都强制执行一个拷贝构造可能会有点低效,但是嘿,我是一个懒打字员。然而 Herb 的短语“ This noonly is 有问题”让我担心,因为我在这里没有得到问题: std: : string 的 move 赋值操作符和它的析构函数一样,都是 noonly,所以在我看来,上面的 set _ name ()似乎是保证不会出现 no but 的。我确实看到编译器 之前 set _ name ()在准备参数时抛出了一个潜在的异常,但我认为这是有问题的。

稍后在幻灯片32 Herb 清楚地指出上面是一个反模式。有没有人能解释一下为什么我会因为懒惰而写出糟糕的代码?

10593 次浏览

有两种方法可以调用该方法。

  • 在使用 rvalue参数时,只要参数类型的 move constructor为 no 除非没有问题(在使用 std::string的情况下,最有可能的情况是 no 除非) ,在任何情况下都最好使用条件 no 除非(以确保参数为 no 除非)
  • 对于 lvalue参数,在这种情况下将调用参数类型的 copy constructor,并且几乎可以肯定它将需要一些分配(这可能会抛出)。

在这种情况下,使用可能会错过使用,这是更好地避免。class的客户端假设没有抛出指定的异常,但是在有效的、可编译的、不可疑的 C++11可以抛出。

为什么通过值传递可能比通过常量引用传递更好,考虑了两个原因。

  1. 更有效率
  2. 除此之外

对于类型为 std::string的成员的 setter,他通过显示通过常量引用传递通常产生较少的分配(至少对于 std::string) ,驳斥了通过值传递更有效的说法。

他还反驳了允许 setter 为 noexcept的说法,他指出了 no 例外声明具有误导性,因为在复制參數的過程中仍然可能出现例外。

因此,他得出结论,通过常量引用传递优于通过值传递,至少在这种情况下是这样。但是,他确实提到了通过值传递对于构造函数来说是一种潜在的好方法。

我确实认为仅仅使用 std::string的示例不足以推广到所有类型,但是它确实引起了对按值传递代价高、但移动代价低的参数的质疑,至少是出于效率和异常原因。

其他人已经讨论了上面的 noexcept推理。

赫伯花了更多的时间谈论效率方面的问题。问题不在于分配,而在于不必要的释放。当您将一个 std::string复制到另一个 std::string中时,如果有足够的空间容纳正在复制的数据,那么复制例程将重用目标字符串的已分配存储。在执行移动分配时,必须释放目标字符串的现有存储,因为它从源字符串接管存储。“复制并移动”习惯用法强制释放始终发生,即使您没有传递临时的。这就是演讲后面所展示的糟糕表现的来源。他的建议是使用 const ref,如果您确定需要使用它,则使用 r- 值引用的重载。这将给你最好的两个世界: 复制到现有的存储为非临时避免释放和临时移动,你要支付一个释放或其他方式(或者目的地释放之前,或者源释放之后的复制)。

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

现在的问题是: 有多少类在复制时像 std: : string 那样重用存储?我猜 std: : Vector 可以,但除此之外我不确定。我知道我从来没有编写过像这样重用存储的类,但是我已经编写了许多包含字符串和向量的类。遵循 Herb 的建议不会对那些不重用存储空间的类造成损害,首先,你会使用接收函数的复制版本进行复制,如果你认为复制对性能的影响太大,那么你会使用一个 r 值引用重载来避免复制(就像你对 std: : string 所做的那样)。另一方面,对于 std: : string 和其他重用存储的类型来说,使用“复制并移动”确实有明显的性能损失,而且这些类型可能在大多数人的代码中有很多用处。我暂时采纳了赫伯的建议,但是在我认为这个问题完全解决之前,我需要再仔细考虑一下这个问题(可能有一篇博客文章我没有时间写)。

Herb 有一个观点,当您已经在内部分配了存储时,采用 by-value 可能是低效的,并导致不必要的分配。但是使用 const&几乎和使用原始的 C 字符串并将其传递给函数一样糟糕,这样会发生不必要的分配。

您应该从字符串中进行抽象读取,而不是字符串本身,因为这正是您所需要的。

现在,你可以这样做作为一个 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_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;
}
};

这样的对象可以直接从 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 样式的字符串指针,就会多少有些不必要地检查它的大小两次(第一次查找 '\0',第二次复制它们)。另一方面,这会给你的目标信息有多大,所以它可以预先分配,而不是重新分配。