17

Herb Sutter回归本源!CppCon上的现代 C++演示要点讨论了传递参数的不同选项,并比较了它们的性能与编写/教学的容易程度。“高级”选项(在所有测试案例中提供最佳性能,但对于大多数开发人员来说太难编写)是完美的转发,给出的示例(PDF,第 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类型不是 a时才可以使用这种方法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);
}

这仍然有缺点,它不能是虚拟的,并且它必须在标题中(尽管希望模块最终会渲染那个没有实际意义的),但这似乎相当可教。

4

3 回答 3

4

我认为你所拥有的可能是正确的,但为了不写一个简单的“我同意”的“答案”,我将提出这个建议,它将根据正确的类型检查分配 - 无论是 lval,rval,常量,随便:

template <class String>
auto set_name(String&& name) 
-> decltype(name_ = std::forward<String>(name), void()) {
    name_ = std::forward<String>(name);
}
于 2014-10-01T18:33:53.707 回答
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 *

是的,我相信这是故意的。当一个字符串字面量 like"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");

约束应该定义通用引用,以便它只接受并推导std::string右值参数的类型或左值参数的cv-std::string&(这就是为什么在与该条件std::decay进行比较之前对其进行编辑)。std::stringstd::is_same


鉴于这是演示文稿中测试的案例之一,显然它旨在与这种类型一起使用。

我认为测试版本(4)没有受到限制(注意它被命名为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它采用一个通用引用,只接受std::string' 的右值和左值(尽管这是一个模板,但仅此而已)。在您std::enable_if将任何不可分配的内容传递给的版本中,std::string无论尝试这样做时是否存在约束,都会产生错误。请注意,最终我们可能只是将该name参数移动到_name如果它是非常量右值引用,因此检查可分配性是没有意义的,除非我们不使用 SFINAE 从重载决议中排除该函数以支持其他重载。


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

template <class String,
          class = std::enable_if_t<std::is_same<std::decay_t<String>,
                                                std::string>::value>>

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

于 2014-10-01T19:30:52.367 回答
1

我试图在评论中加入这些想法,但它们不适合。正如我在上面的评论和 Herb 的精彩演讲中提到的那样,我想写这个。

很抱歉迟到了。我已经查看了我的笔记,实际上我对向 Herb 推荐选项 4 的原始约束减去 errant 感到内疚!。没有人是完美的,正如我妻子肯定会证实的,尤其是我。:-)

提醒: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在重载集中。正确性仍然是王道...

如果我们想将单个字符分配给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用 a设置 a 的名称不是逻辑错误char(除非您想添加文档employee)。因此,即使最初的 C++98/03 实现不允许使用以下语法:

e.set_name('A');

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

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

我们正在处理选项#4,因为我们迫切希望最大限度地优化这个东西。

由于这些原因,我认为这is_assignable是约束此功能的最佳特征。从风格上看,我发现Barry 拼写这个约束的技巧是完全可以接受的。因此,这就是我投票的地方。

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

于 2015-04-12T17:19:40.370 回答