模板参数可以是引用类型吗?

11
我开始学习C++,目前正在尝试使用模板,如果我的措辞不是100%准确,请谅解。
我正在使用以下资料:
  • C++ Templates: The Complete Guide(第二版)
  • Effective Modern C++:42种改进你对C++11和C++14的使用方法
第一本书介绍了以下模板函数。
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b<a?a:b) {
  return b < a ? a : b;
}

并且说明这个定义有一个缺陷,因为T1T2可能是一个引用类型,所以返回类型也可能是引用类型。

然而,第二本书指出,如果ParamType(在我们的情况下是T1T2)既不是指针类型也不是引用类型,那么调用表达式中的引用部分将会被忽略。

通过以下例子进行说明:

template<typename T>
void f(T param);

int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int

现在我在想,第一个代码片段的返回类型怎么可能是引用类型呢?


4
max<const int&, const int&>(42, 51). - Jarod42
@polygamma 它不会被推断为这样,但像Jarod在他们的评论中所指定的那样,有人可以指定它。 - NathanOliver
1
在C或C++中,表达式从不具有引用类型(是的,在C中没有引用,但类型系统的工作方式是相同的)。 - curiousguy
2个回答

8

他们两个都是对的:

查看在cppinsights中生成的代码

template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(b<a?a:b) {
  return b < a ? a : b;
}

template<typename T1, typename T2>
auto max2(T1 a, T2 b){
  return b < a ? a : b;
}

max(j,i);
max2(j,i);

会 '生成':

template<>
int & max<int, int>(int a, int b)
{
  return b < a ? a : b;
}

template<>
int max2<int, int>(int a, int b)
{
  return b < a ? a : b;
}

问题出在C++11的-> decltype(b<a?a:b),如果你将其移除(在C++14及更高版本中),函数将不再返回引用。更准确地说,自从C++14以来,函数的返回类型通常是通过使用decltype(auto)(而不仅仅是auto)进行推断的,如下所示:
template<typename T1, typename T2>
decltype(auto) max(T1 a, T2 b) {
  return b < a ? a : b;
}

然而,在这种情况下,我们应该只使用auto作为返回类型,以确保我们基于自动类型推导规则而不是decltype来获取我们想要的结果。

现在,

static_assert( is_same_v<decltype(i),int> );
static_assert( is_same_v<decltype((i)),int&> );
static_assert( is_same_v<decltype(i+j),int> );
static_assert( is_same_v<decltype(true?i:j),int&> );

请查看https://en.cppreference.com/w/cpp/language/operator_other#Conditional_operator

  1. 如果E2和E3是相同类型和相同值类别的glvalue,则结果具有相同的类型和值类别[...]。
  1. 否则,结果为prvalue[...]

在C++中意味着:

static_assert( is_same_v<decltype(true?i:j),int&> ); // E2 and E3 are glvalues 
static_assert( is_same_v<decltype(true?i:1),int> ); // Otherwise, the result is a prvalue

1
?: 的结果并不总是 xvalue。其类别取决于操作数的类型和值类别。我认为 a ? a : b 是 lvalue。 - eerorika
@eerorika,针对一些b和c的三元条件表达式。你是对的,我看错了。 - Martin Morterol
@PasserBy 你好,即使删除它会使注释没有意义,我是否应该删除“返回...”? - Martin Morterol
2
@Martinm 是的,评论是为了改进帖子。一旦完成,评论可以由其所有者删除。并且鼓励人们这样做。(我将在10分钟内删除此评论) - Sjoerd
@MartinMorterol 对于 decltype 的情况,如果函数返回引用会有什么问题?你能否解释一下,从书中无法理解为什么这是一个大问题。 - Daemon
@Daemon 如果你在cppinsights上运行链接,你会得到这个警告 warning: reference to stack memory associated with parameter 'a' returned [-Wreturn-stack-address] ,这是非常明确的提示 ;)。如果需要的话,你可以在这里查看:https://dev59.com/ZWw15IYBdhLWcg3wuuCn - Martin Morterol

3

由于T1或T2可能是引用,因此返回类型可以是引用类型。

我认为这引用有误。返回类型可能是引用类型,但原因不同。

如果您继续阅读第二本书,在第3项中您将找到问题的答案。

将decltype应用于名称会产生该名称的声明类型。名称通常是lvalue表达式,但这并不影响decltype的行为。然而,对于比名称更复杂的lvalue表达式,decltype通常确保报告的类型是lvalue引用。也就是说,如果一个类型为T的lvalue表达式不是名称,则decltype将其报告为T&。
然而,这种行为有一个值得注意的含义。在
int x = 0;
中,x是一个变量的名称,因此decltype(x)是int。但是将名称x括在括号“(x)”中会产生一个比名称更复杂的表达式。作为名称,x是lvalue,并且C++还定义了表达式(x)也是lvalue。因此,decltype((x))是int&。在名称周围加上括号可以改变decltype为其报告的类型!
现在唯一剩下的问题是: b<a?a:b 是lvalue吗?

来自 cppreference

4) 如果 E2 和 E3 是相同类型和相同值类别的 glvalue,则结果具有相同的类型和值类别,并且如果 E2 和 E3 中至少一个是位域,则结果为位域。

因此,ab 是lvalues,如果它们是相同类型的,则 b<a?a:b 也将是 lvalue。在这里可以找到很好的解释。

这意味着 max(1, 2) 调用的返回类型将是 int&,而 max(1, 2.0) 的返回类型将是 int


2
在语言设计方面,我认为decltype同时执行两个不同的操作(这些操作只是微妙的不同,并且并非总是不同)是一种可憎的做法。另一方面,static曾经被认为是一个糟糕的设计选择,但实际上它执行了两个非常不同的操作(顶层与函数块),然后执行了两个密切相关的操作(类静态变量与函数静态变量)。 - curiousguy
2
非常感谢您的回答。我只想解决一件事。您写道:“我假设这是引用错误”,但书中的确切引用是:“可能发生返回类型是引用类型的情况,因为在某些条件下T可能是引用”。 - polygamma
1
Martin Morterol的答案应该与这个一起阅读。关于decltype行为的描述非常重要。只有这样才清楚为什么decltype(true?i:j)是int&,而decltype(true?i:1)是int。 - Hari

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