decltype(auto)的一些用途是什么?

213
在C++14中引入了decltype(auto)这个习语。
通常它的用法是允许auto声明在给定表达式上使用decltype规则。
在寻找这个习语的“好”用法的例子时,我只能想到以下类似的事情(由Scott Meyers提供),即用于函数的返回类型推断。
template<typename ContainerType, typename IndexType>                // C++14
decltype(auto) grab(ContainerType&& container, IndexType&& index)
{
  authenticateUser();
  return std::forward<ContainerType>(container)[std::forward<IndexType>(index)];
}

这个新的语言特性还有其他哪些实用的例子吗?

2
这篇文章基本上建议尝试避免使用这个习语,因为当使用它时,你给编译器的优化选项更少。 - user2485710
我曾经使用过 decltype(auto) 来完成类似于 template<class U, V> decltype(auto) first(std::pair<U, V>& p) { return p.first; } 的功能,但我后来意识到必须使用 return (p.first);,这令人惊讶地起作用了(但如果我没记错的话,这甚至是有意的)。 - dyp
@user2485710 不确定它是否特别涉及优化,更多的是关于decltype(auto)可能会导致某些东西被复制/移动到声明的对象中,与预期相反,从而增加意外事故的潜在风险。 - underscore_d
在你上面给出的例子中,如果 container 实际上是一个 rvalue,我认为使用 decltype(auto) 可能会导致意外引用无效内存。然而,你可以通过 ContainerType 的值类型返回,并且副本省略应该会给你与 decltype(auto) 相同的东西,但可以安全地作为引用返回。https://godbolt.org/z/GsYjxs - Steve Bronder
是的,这里有另一个例子,其中容器的内部值被销毁,但我们从函数中请求对它的引用。https://godbolt.org/z/7jE5Me - Steve Bronder
2个回答

245

泛型代码中的返回类型转发

对于非泛型代码,就像您提供的初始示例一样,您可以手动选择将引用作为返回类型:

auto const& Example(int const& i) 
{ 
    return i; 
}

但是在泛型代码中,您希望能够完美地转发返回类型,而不知道您处理的是引用还是值。 decltype(auto) 可以提供这种能力:

template<class Fun, class... Args>
decltype(auto) Example(Fun fun, Args&&... args) 
{ 
    return fun(std::forward<Args>(args)...); 
}

推迟递归模板的返回类型推导

在几天前的这个问题及答案中,当模板的返回类型被指定为 decltype(iter(Int<i-1>{})) 而不是 decltype(auto) 时,会在模板实例化期间遇到无限递归。

template<int i> 
struct Int {};

constexpr auto iter(Int<0>) -> Int<0>;

template<int i>
constexpr auto iter(Int<i>) -> decltype(auto) 
{ return iter(Int<i-1>{}); }

int main() { decltype(iter(Int<10>{})) a; }
decltype(auto) 用于在模板实例化的尘埃落定后延迟返回类型推导。您还可以在其他情况下使用 decltype(auto),例如草案标准N3936中所述。草案这样声明:autodecltype(auto) 类型说明符指定了一个占位符类型,该类型将稍后被初始化程序的推导或带有尾随返回类型的显式说明所替换。 如果函数声明符包括尾随返回类型(8.3.5),那么它就指定了函数的声明返回类型。如果函数的声明返回类型包含一个占位符类型,那么函数的返回类型将从函数体中的return语句中推导出来(如果有的话)。草案还举了一个变量初始化的例子:
int i;
int&& f();
auto x3a = i;                  // decltype(x3a) is int
decltype(auto) x3d = i;        // decltype(x3d) is int
auto x4a = (i);                // decltype(x4a) is int
decltype(auto) x4d = (i);      // decltype(x4d) is int&
auto x5a = f();                // decltype(x5a) is int
decltype(auto) x5d = f();      // decltype(x5d) is int&&
auto x6a = { 1, 2 };           // decltype(x6a) is std::initializer_list<int>
decltype(auto) x6d = { 1, 2 }; // error, { 1, 2 } is not an expression
auto *x7a = &i;                // decltype(x7a) is int*
decltype(auto)*x7d = &i;       // error, declared type is not plain decltype(auto)

24
在C++14中,(i)i的不同行为是一项新的功能吗? - Danvil
22
在C++11中,decltype(expr)decltype((expr))是不同的,这个新特性推广了这种行为。 - TemplateRex
30
刚刚了解到这个,感觉这是一个糟糕的设计决策……在括号语法的含义上添加一个点式细微差别。 - Kahler
3
常常引起这种反感的例子是单行文件转字符串语法(也在那个链接中提到)。它的每个部分似乎都是相反的。你可能完全不会期望存在歧义,并且会强制从示例中删除多余的括号;你本来期望依靠SFINAE通过消除过程来解决歧义,但是声明以外的其他候选项已经被预先排除了(SFisAE);而且当你沮丧时,你可能一旦编译通过就会继续前进,认为任意的括号都可以解决歧义,但实际上它们却引入了歧义。我想对于计算机科学101教授来说这是最令人烦恼的事情。 - John P
3
@Bruice: “为什么 decltype(auto) x4d = (i); decltype(x4d) 是 int&?”Decltype 有两种用法:(1) 和 (2): https://en.cppreference.com/w/cpp/language/decltype它可以应用于名称或其他表达式。decltype(i) 意味着“名为 i 的实体的类型”,而 decltype((i)) 阻止了这种解释,意味着“求值为对 i 的引用的表达式的类型”。在我看来,这非常微妙,但这就是生活。现在当强迫症使人们在“return”表达式周围使用括号时,它就很重要,就好像“return”是一个函数一样。 :)这也使得宏变得更加棘手。 - Billy Donahue
显示剩余2条评论

48

引用来自这里的内容:

  • decltype(auto) 主要用于推导转发函数和类似包装器的返回类型,其中您希望该类型完全“跟踪”某个表达式。

  • 例如,考虑以下函数:


   string  lookup1();
   string& lookup2();
在C++11中,我们可以编写以下包装器函数,这些函数记得保留返回类型的引用性:
   string  look_up_a_string_1() { return lookup1(); }
   string& look_up_a_string_2() { return lookup2(); }
  • 在C++14中,我们可以自动化这个过程:

   decltype(auto) look_up_a_string_1() { return lookup1(); }
   decltype(auto) look_up_a_string_2() { return lookup2(); }
  • 然而,decltype(auto) 的使用目的并不是为了广泛使用。

  • 尤其是,虽然它可以用于声明本地变量,但这样做可能只是一种反模式,因为本地变量的引用性不应该取决于初始化表达式。

  • 而且,它对你编写的返回语句很敏感。

  • 例如,下面的两个函数具有不同的返回类型:


   decltype(auto) look_up_a_string_1() { auto str = lookup1(); return str; }
   decltype(auto) look_up_a_string_2() { auto str = lookup2(); return(str); }
  • 第一个返回 string,第二个返回 string&,它是对局部变量 str 的引用。

提案中可以看到更多预期的用途。


12
为什么不直接在返回值上使用 auto - BЈовић
@BЈовић 也可以使用一般化返回类型推导(即auto返回),但是原帖询问了decltype(auto)的用法。 - 101010
6
虽然问题仍然相关。那么auto lookup_a_string() { ... }的返回类型将是什么?它总是一个非引用类型吗?因此,auto lookup_a_string() ->decltype(auto) { ... }需要强制允许返回引用(在某些情况下)。 - Aaron McDaid
2
@AaronMcDaid 可扣除的“auto”是基于传递值模板定义的,所以它不能是引用。请稍等,“auto”可以是任何东西,包括引用。 - curiousguy
6
另一个值得一提的例子是返回std::vector中的元素。假设您有这样一个结构体:template<typename T> struct S { auto & operator[](std::size_t i) { return v[i]; } std::vector<T> v; }。然后 S<bool>::operator[] 会因为 std::vector<bool> 的特化而返回悬空引用。将返回类型更改为decltype(auto)可以解决此问题。 - Xoph
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接