完美转发设置器的正确`enable_if`约束是什么?

时间:2014-10-01 17:52:22

标签: c++ c++11 perfect-forwarding enable-if forwarding-reference

Herb Sutter的回归基础! CppCon上的现代C ++ 演示要点讨论了传递参数的不同选项,并将其性能与写作/教学的简易性进行了比较。 “高级”选项(在所有测试的案例中提供最佳性能,但对大多数开发人员来说难以编写)是完美的转发,给出了(PDF, pg. 28)示例:

class employee {
    std::string name_;

public:
    template <class String,
              class = std::enable_if_t<!std::is_same<std::decay_t<String>,
                                                     std::string>::value>>
    void set_name(String &&name) noexcept(
      std::is_nothrow_assignable<std::string &, String>::value) {
        name_ = std::forward<String>(name);
    }
};

该示例使用带转发引用的模板函数,模板参数String使用enable_if进行约束。但是约束似乎是不正确的:似乎只有在String类型不是std::string时才会使用此方法,这没有任何意义。这意味着可以使用 std::string值以外的任何内容设置此std::string成员。

using namespace std::string_literals;

employee e;
e.set_name("Bob"s); // error

我考虑过的一个解释是,存在一个简单的拼写错误,约束旨在std::is_same<std::decay_t<String>, std::string>::value而不是!std::is_same<std::decay_t<String>, std::string>::value。然而,这意味着setter不能与例如const char *一起工作,并且它显然是打算使用这种类型,因为这是演示文稿中测试的案例之一。

在我看来,正确的约束更像是:

template <class String,
          class = std::enable_if_t<std::is_assignable<decltype((name_)),
                                                      String>::value>>
void set_name(String &&name) noexcept(
  std::is_nothrow_assignable<decltype((name_)), String>::value) {
    name_ = std::forward<String>(name);
}

允许任何可以分配给成员的内容与setter一起使用。

我有正确的约束吗?是否可以进行其他改进?原始约束是否有任何解释,或许它是脱离背景的?


此外,我想知道这个宣言中复杂的,“无法触及”的部分是否真的有益。由于我们没有使用重载,我们可以简单地依赖于普通模板实例化:

template <class String>
void set_name(String &&name) noexcept(
  std::is_nothrow_assignable<decltype((name_)), String>::value) {
    name_ = std::forward<String>(name);
}

当然,关于noexcept是否真的很重要还有一些争论,有人说除了移动/交换原语之外不要太担心它:

template <class String>
void set_name(String &&name) {
    name_ = std::forward<String>(name);
}

也许对于概念,限制模板并不是不合理的难度,只是为了改进错误信息。

template <class String>
  requires std::is_assignable<decltype((name_)), String>::value
void set_name(String &&name) {
    name_ = std::forward<String>(name);
}

这仍然有缺点,它不能是虚拟的,它必须在一个标题中(虽然希望模块最终会有所帮助),但这似乎相当可教。

3 个答案:

答案 0 :(得分:4)

  

我考虑的一个解释是,这是一个简单的拼写错误,而且约束的目的是std::is_same<std::decay_t<String>, std::string>::value而不是!std::is_same<std::decay_t<String>, std::string>::value

是的,屏幕上显示的正确约束为is_same,而不是!is_same。看起来幻灯片上有拼写错误。


  

然而,这意味着setter无法使用,例如const char *

是的,我相信这是故意的。当像"foo"这样的字符串文字被传递给接受通用引用的函数时,推导出的类型不是指针(因为数组仅在指针时衰减为指针)通过值)捕获模板参数,而不是const char(&)[N]。也就是说,每次使用不同长度的字符串文字调用set_name都会实例化一个新的set_name专门化,例如:

void set_name(const char (&name)[4]); // set_name("foo");
void set_name(const char (&name)[5]); // set_name("foof");
void set_name(const char (&name)[7]); // set_name("foofoo");

约束应该定义通用引用,以便它只接受和推导rvalue参数的std::string类型,或者 cv - {{1对于左值参数(在与std::string&条件中的std::decay进行比较之前,这是std::string的原因。


  

它显然是打算使用这种类型,因为它是演示文稿中测试的案例之一。

我认为测试版本(4)没有受到约束(注意它被命名为std::is_same +完美转发),所以它可以简单如下:

String&&

这样当传递一个字符串文字时,它就不像函数调用那样在函数调用之前构造template <typename String> void set_name(String&& name) { _name = std::forward<String>(name); } 实例就像非模板化版本那样(不必要地在被调用者的代码中分配内存来构建)一个std::string临时表,最终会移动到可能预分配的目的地,如std::string):

_name

  

在我看来,正确的约束更像是:void set_name(const std::string& name); void set_name(std::string&& name);

不,正如我写的那样,我不认为其目的是限制std::enable_if_t<std::is_assignable<decltype((name_)), String>::value>>接受类型可分配set_name。再一次 - 原始std::string只有std::enable_if函数的单个实现,其中通用引用只接受set_name的rvalues和lvalues(仅此而已一个模板)。在您的std::string版本中传递任何不可分配给std::enable_if的内容都会产生错误,无论是否在尝试时都是constaint。请注意,最终我们可能只是将std::string参数移动到name,如果它是非常量右值参考,那么检查可分配性是没有意义的,除非我们的情况没有使用SFINAE将该函数从过载分辨率中排除,以支持其他过载。


  

完美转发设置器的正确_name约束是什么?

enable_if

或者根本没有约束,如果它不会导致任何性能损失(就像传递字符串文字一样)。

答案 1 :(得分:3)

我认为你所拥有的可能是对的,但是为了不写一个简单的“我同意”的“答案”,我会提出这个,而不是根据正确的类型检查分配 - 是否是一个lval ,rval,const,等等:

template <class String>
auto set_name(String&& name) 
-> decltype(name_ = std::forward<String>(name), void()) {
    name_ = std::forward<String>(name);
}

答案 2 :(得分:1)

我试图在评论中加入这些想法,但它们不合适。我想在上面的评论和Herb的精彩演讲中都提到了这一点。

很抱歉迟到了。我已经查看了我的笔记,我确实向Herb推荐了选项4的原始约束减去错误的!。没有人是完美的,因为我的妻子肯定会证实,尤其是我。 : - )

提醒:Herb的观点(我同意)是从简单的C ++ 98/03建议开始

set_name(const string& name);

并仅在需要时从那里移动。选项#4是相当多的运动。如果我们考虑选项#4,我们就要计算应用程序关键部分的负载,存储和分配。我们需要尽可能快地将name分配给name_。如果我们在这里,代码可读性远不如性能重要,尽管正确性仍然是王道。

根本不提供约束(对于选项#4)我认为稍微不正确。如果其他一些代码试图限制自己是否可以调用employee::set_name,那么如果set_name完全没有约束,则可能得到错误的答案:

template <class String>
auto foo(employee& e, String&& name) 
-> decltype(e.set_name(std::forward<String>(name)), void()) {
    e.set_name(std::forward<String>(name));
    // ...

如果set_name不受约束且String推断为某些完全不相关的类型X,则foo上的上述约束不恰当地包含foo的{​​{1}}实例化过载集。而正确性仍然是王......

如果我们想将单个字符分配给name_怎么办?说A。它应该被允许吗?它应该快速邪恶吗?

e.set_name('A');

好吧,为什么不呢?! std::string只有一个赋值运算符:

basic_string& operator=(value_type c);

但请注意,没有相应的构造函数:

basic_string(value_type c);  // This does not exist

因此is_convertible<char, string>{}false,但is_assignable<string, char>{}true

尝试使用string设置char的名称​​不是一个逻辑错误(除非您要向employee添加文档这么说)。因此,即使最初的C ++ 98/03实现不允许语法:

e.set_name('A');

它确实以较低效的方式允许相同的逻辑操作:

e.set_name(std::string(1, 'A'));

我们正在处理选项#4,因为我们迫切希望尽可能地优化这件事。

由于这些原因,我认为is_assignable是限制此功能的最佳特征。在风格上,我找到了Barry's technique for spelling this constraint quite acceptable。因此,这是我投票的地方。

另请注意,此处的employeestd::string只是Herb演讲中的示例。它们是您的代码中处理的类型的替身。此建议旨在概括您必须处理的代码。