【问题标题】:What's the correct `enable_if` constraint on perfect forwarding setter?完美转发设置器的正确“enable_if”约束是什么?
【发布时间】:2014-11-26 15:43:32
【问题描述】:

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&lt;std::decay_t&lt;String&gt;, std::string&gt;::value 而不是!std::is_same&lt;std::decay_t&lt;String&gt;, std::string&gt;::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);
}

这仍然有缺点,它不能是虚拟的,并且它必须在标题中(尽管希望模块最终会呈现这种情况),但这似乎相当可教。

【问题讨论】:

  • 好吧,房间里有人说它的约束不正确并且不接受字符串文字——也许我们可以抄送@HowardHinnant?在我看来也不对。 Video of the talk
  • @PiotrS.: MSVC 没有别名,所以我忘记了:/
  • @MooingDuck 嘿,VS2013 支持别名,它们的 stdlib 实现了 C++14 type_trait 别名。
  • @bames53:是吗?我想知道我怎么忽略了这一点!

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


【解决方案1】:

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

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

【讨论】:

  • 当然,当您重新添加noexcept 子句时,它会变得有些难看。
  • 这确实救了我的命。我无法让任何 is_* type_traits 正常工作,并且像这样使用 decltype 让我无法理解。非常感谢!
【解决方案2】:

我考虑的一个解释是,有一个简单的错字,并且约束是 std::is_same&lt;std::decay_t&lt;String&gt;, std::string&gt;::value 而不是 !std::is_same&lt;std::decay_t&lt;String&gt;, std::string&gt;::value

是的,屏幕上显示的正确约束是is_same,而不是!is_same。您的幻灯片上似乎有错字。


但是,这意味着 setter 无法使用,例如,const char *

是的,我相信这是故意的。当像"foo" 这样的字符串字面量被传递给接受通用引用的函数时,推导的类型不是指针(因为数组只有在被捕获时才会衰减为指针模板参数按值),而是const char(&amp;)[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&amp; 的左值参数(这就是为什么在 std::is_same 条件下与 std::string 比较之前是 std::decayed)。


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

我认为测试版本(4)没有受到限制(注意它被命名为String&amp;&amp;+完美转发),所以它可以很简单:

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&lt;std::is_assignable&lt;decltype((name_)), String&gt;::value&gt;&gt;

不,正如我所写,我不认为其意图是限制 set_name 接受类型 assignablestd::string。再一次 - 原来的 std::enable_if 有一个 set_name 函数的实现,它采用一个通用引用,只接受 std::string 的右值和左值(尽管这是一个模板,但仅此而已)。在您的std::enable_if 版本中,将任何不可分配的内容传递给std::string 都会产生错误,无论尝试这样做时是否存在约束。请注意,最终我们可能只是将 name 参数移动到 _name,如果它是 非 const 右值引用,因此检查可分配性是没有意义的,除非我们不使用 SFINAE 来排除它重载决议中的函数有利于其他重载。


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

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

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

【讨论】:

  • “我不认为意图是限制 set_name 接受可分配给 std::string 的类型” 如果我们将其与其他解决方案进行比较,它至少应该可以隐式转换为 std::string 以允许与其他解决方案相同的转换。但这似乎不合适,因为我们确实希望在分配之前执行隐式转换。
  • @dyp:这就是为什么类型仅限于std::strings,而不是将 assignable 类型限制为字符串的一个很好的理由,这就是你的意思吗?
  • 好吧,我的意思是其他解决方案允许 x.set_name("hello");,而这个不允许 - "hello" 不是 std::string,但它可以隐式转换为 std::string&amp;&amp;std::string const&amp;std::string.
  • video 看来,它被显示为!is_same,并且从 Herb 的讨论中,我相信他显然想要支持字符串文字,尽管您提出了一个很好的观点,即为每个使用的字符串文字的长度。 -- 另外,我提到约束可以被忽略,因为我们没有使用重载,但我提出了一个替代方案,因为 Herb 认为完美的转发器应该受到约束。此外,一些编译器了解enable_if 并使用它来改进诊断。
  • @bames53:不要看视频中的幻灯片,看 Herb 背后的大屏幕(我看到了is_same)。他甚至问它是否支持字符串文字(观众说“它不支持”)以及为什么不支持(“为什么它错误地受到约束”)——至少这是我所看到和听到的。我认为添加 is_assignable 约束甚至不会改善诊断,如果推导的参数的类型不是 copy-assignable / move-assignable to @,最终将引发错误987654368@
【解决方案3】:

我试图将这些想法放在评论中,但它们不适合。正如上面的 cmets 和 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 的这个实例化包含在重载集中。而且正确性仍然是王道...

如果我们想为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&lt;char, string&gt;{}false,但is_assignable&lt;string, char&gt;{}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 演讲中的示例。它们是你的代码中处理的类型的替身。此建议旨在概括您必须处理的代码。

【讨论】:

  • 关于需要一个约束来防止将 int 分配给 name_,这是一个很好的观点,但在我看来,这是一个特殊情况,因为从 int 到 @ 的隐式转换已应用 987654355@。理想情况下,字符串类在这种情况下能够禁止隐式转换,但我认为至少我们可以在employee::set_name 中做一些事情。 this 呢?或者可能为此使用大括号过于微妙,因此与 SFINAE 约束一样不可教?
  • @bames53:在我的示例中使用int 作为示例“其他类型”是我的错误。我应该使用class rutabaga 并演示rutabaga 没有转换为std::string。但是有时employees 吃掉rutabagas,因此可以想象fooemployeerutabaga 上过载,或者rutabaga 隐式转换为vegetable 或模板化的东西代表vegetable 的参数。 IE。使用扩展的 SFINAE,any 表达式可以用作约束,甚至是 e.set_name
  • 但是如果你用is_assignable约束,你的fooString = int仍然会在重载集中,因为int(和double等)可以转换为@ 987654375@.
猜你喜欢
  • 2016-07-03
  • 2019-06-15
  • 2021-10-28
  • 1970-01-01
  • 2019-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多