`decltype(auto)` 变量有哪些实际应用场景?

28
从我的个人经验和咨询类似于“decltype(auto)的用途有哪些?”这样的问题的答案中,我可以找到足够多的有价值的应用场景,其中decltype(auto)作为一个函数返回类型占位符。

然而,我很难想出任何合法的(即有用、现实、有价值)用例来使用decltype(auto)变量。唯一想到的可能是存储函数返回decltype(auto)的结果以供稍后传播,但是那里也可以使用auto&&,而且更简单。
我甚至在我所有的项目和实验中进行了搜索,391个decltype(auto)的出现都是返回类型占位符。
那么,“decltype(auto)变量是否有任何现实的用例?或者说,只有用作返回类型占位符时才有用处吗?

你如何定义“现实”?

我正在寻找一个提供价值的用例(即它不仅仅是一个示例,用于展示该特性的工作方式),在这种情况下,decltype(auto)是完美的选择,与其他替代方案(如auto&&)或不声明变量相比。问题域并不重要,它可能是某些模糊的元编程角落案例或晦涩的函数式编程构造。然而,示例需要让我感到“嘿,这很聪明/美丽!”使用任何其他功能来达到相同的效果将需要更多的样板文件或具有某种缺点。

1
你如何定义“现实主义”? - Nicol Bolas
@NicolBolas:我正在寻找一些使用案例,提供价值(即它不仅是一个展示功能如何工作的示例),其中decltype(auto)是完美选择,与诸如auto&&或根本不声明变量相比。领域并不重要,它可以是一些晦涩的元编程角落案例。但是这个例子需要让我感到聪明!使用任何其他功能来实现相同效果将需要更多的样板文件或具有某种缺点。很抱歉我不能更加精确。 - Vittorio Romeo
1
@Eljay:受宠若惊!我可以给你很多decltype(auto)返回函数的例子……但是变量目前让我感到困惑 :) - Vittorio Romeo
我最近使用了decltype(auto)变量来进行NRVO,就像这样:https://godbolt.org/z/9oz37Was1 - Artyer
2个回答

18

实际上,变量的用途与函数相同。其思想是我们使用 decltype(auto) 变量存储函数调用的结果:

decltype(auto) result = /* function invocation */;

那么,result

  • 如果结果是 prvalue,则为非引用类型

  • 如果结果是 lvalue,则是(可能带 const/volatile 修饰的)左值引用类型

  • 如果结果是 xvalue,则是右值引用类型

现在我们需要一个新版本的 forward 来区分 prvalue 和 xvalue 的情况:(避免使用名称 forward 以防止 ADL 问题)

template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
    return std::forward<T>(arg);
}

然后使用。
my_forward<decltype(result)>(result)

std::forward不同,此函数用于转发decltype(auto)变量。 因此,它不会无条件地返回引用类型,并且应该使用decltype(variable)调用,其中可以是TT&T&&,以便区分lvalue,xvalue和prvalue。 因此,如果result是:
  • 非引用类型,则使用非引用T调用第二个重载,并返回非引用类型,从而产生prvalue;

  • 左值引用类型,则使用T&调用第一个重载,并返回T&,从而产生左值;

  • 右值引用类型,则使用T&&调用第二个重载,并返回T&&,从而生成xvalue。

以下是一个示例。 假设要包装std::invoke并将某些内容打印到日志中:(此示例仅供说明)
template <typename F, typename... Args>
decltype(auto) my_invoke(F&& f, Args&&... args)
{
    decltype(auto) result = std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    my_log("invoke", result); // for illustration only
    return my_forward<decltype(result)>(result);
}

现在,如果调用表达式是:
  • 一个 prvalue, 那么 result 是一个非引用类型,并且该函数返回一个非引用类型;

  • 一个非常量 lvalue, 那么 result 是一个非常量 lvalue 引用,并且该函数返回一个非常量 lvalue 引用类型;

  • 一个常量 lvalue, 那么 result 是一个常量 lvalue 引用,并且该函数返回一个常量 lvalue 引用类型;

  • 一个 xvalue, 那么 result 是一个右值引用类型,并且该函数返回一个右值引用类型。

给出以下函数:
int f();
int& g();
const int& h();
int&& i();

以下断言成立:
static_assert(std::is_same_v<decltype(my_invoke(f)), int>);
static_assert(std::is_same_v<decltype(my_invoke(g)), int&>);
static_assert(std::is_same_v<decltype(my_invoke(h)), const int&>);
static_assert(std::is_same_v<decltype(my_invoke(i)), int&&>);

(实时演示仅移动测试用例)
如果使用 auto&&,代码将很难区分 prvalues 和 xvalues。

@VittorioRomeo:“我并不认为decltype(auto)是解决这个问题的最佳方案。” 这就是使你的问题基于观点的原因:谁来决定什么是“最好的”?LF的代码非常易读,而你的替代示例则有些奇怪。不清楚你在做什么或其目的是什么。这是很多额外的代码,只是为了可能避免复制/移动(因为几乎每个编译器都会NVRO该代码)。 - Nicol Bolas
@VittorioRomeo,你是对的,我的先前答案完全错误。我已经用更好的版本更新了我的答案(不使用if constexpr)。我还添加了一个仅移动类型的测试用例。 - L. F.
@L.F.: 这个似乎还是有问题。任何prvalue都会像以前一样被复制。而RVO不可能实现。请参见https://gcc.godbolt.org/z/gNCk_x - Vittorio Romeo
@L.F.:现在它移动而不是复制,但据我所见,它可以防止RVO。 - Vittorio Romeo
显示剩余4条评论

6

这可能不是一个非常深入的答案,但基本上 decltype(auto) 被提议 用于返回类型推导,以便在返回类型实际上是引用时推导出引用(与普通的 auto 相反,它永远不会推导出引用,或者 auto&& 总是这样做)。

它也可以用于变量声明,并不一定意味着应该有比其他情况更好的场景。实际上,在变量声明中使用 decltype(auto) 只会使代码阅读更加复杂,因为对于变量声明来说,它具有完全相同的含义。另一方面,auto&& 形式允许您声明一个常量变量,而 decltype(auto) 则不允许。


我不明白auto&&如何允许您声明一个常量变量(引用在顶层永远不是const),而decltype(auto)则可以毫无问题地实现这一点。 - L. F.
@L.F. 抱歉回复晚了,可能我表达不够完整:我的意思是,在某些情况下,您可以通过在 auto&& 中添加 const 修饰符来转换为常量,而 decltype(auto) 则不允许这样做。我知道它不可能在所有场景中使用(例如 int a; int& b=a; const auto&& c=b;),但在像 int foo() { return 0; } const auto&& a=foo(); 这样的情况下是可行的。也许没有什么用处,但是可能性是存在的。无论如何,请纠正我是否遗漏了什么。 - cbuchart

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