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);
}
这仍然有缺点,它不能是虚拟的,它必须在一个标题中(虽然希望模块最终会有所帮助),但这似乎相当可教。
答案 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。因此,这是我投票的地方。
另请注意,此处的employee
和std::string
只是Herb演讲中的示例。它们是您在您的代码中处理的类型的替身。此建议旨在概括您必须处理的代码。