这是C++中的一个怪异/特性。虽然我们不把引用视为类型,但它们实际上“坐落”在类型系统中。尽管这似乎很奇怪(因为当使用引用时,引用语义会自动发生且引用“离开视线”),但有一些可辩护的原因使得引用建模为类型系统的一部分,而不是作为类型外部的单独属性。
首先,让我们考虑到并非每个声明名称的属性都必须在类型系统中。从C语言中,我们有“存储类”和“链接”。可以将名称声明为extern const int ri
,其中extern
表示静态存储类和链接的存在。类型只是const int
。
C++显然接受了表达式具有类型系统之外属性的概念。该语言现在有一个"值类"的概念,试图组织表达式可以展示的越来越多的非类型属性。
然而,引用是类型。为什么?
过去在C++教程中解释过,“const int &ri”这样的声明引入了“ri”作为具有类型const int
的变量名,但具有引用语义。引用语义不是类型;它只是一种属性,表示名称和存储位置之间的不寻常关系。此外,引用不是类型的事实被用于合理化为什么不能基于引用构造类型,即使类型构造语法允许这样做。例如,不可能创建指向引用的数组或指针:const int &ari[5]
和const int &*pri
。
但实际上,引用是类型,因此decltype(ri)
检索到某个未限定的引用类型节点。您必须沿着类型树下降以获取基础类型,使用remove_reference
函数可以实现。
当您使用ri
时,引用会自动解决,以便ri
“看起来和感觉像i”并且可以称为它的“别名”。但在类型系统中,ri
实际上具有一个类型,该类型是"引用到const int
"。
为什么引用是类型?
考虑如果引用不是类型,则这些函数将被认为具有相同的类型:
void foo(int);
void foo(int &);
由于原因显而易见,这是不可能的。如果它们具有相同的类型,那么任何一个声明都可以适用于任何一个定义,因此每个 (int)
函数都必须被怀疑会采取引用。
同样地,如果引用不是类型,那么这两个类声明将是等效的:
class foo {
int m;
};
class foo {
int &m;
};
一个翻译单元使用一个声明是正确的,而在同一程序中的另一个翻译单元使用另一个声明也是正确的。
事实上,引用意味着实现上的差异,不可能将其与类型分开,因为在C++中,类型与实体的实现有关:它的“布局”,就像说它的二进制位。如果两个函数具有相同的类型,则可以使用相同的二进制调用约定来调用它们:ABI是相同的。如果两个结构或类具有相同的类型,则它们的布局以及对所有成员的访问语义也是相同的。引用的存在改变了这些类型的方面,因此将其纳入类型系统是一个直接的设计决策。(然而,请注意这里的反驳:结构/类成员可以是静态的,这也会改变表示方式;但那不是类型!)
因此,引用在类型系统中是“二等公民”(与ISO C中的函数和数组类似)。我们不能对引用执行某些操作,例如声明指向引用的指针或数组。但这并不意味着它们不是类型。只是以一种没有意义的方式不是类型。
并非所有这些二等公民限制都是必要的。鉴于有引用结构,可能会有引用数组!例如:
// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };
// ar[0] is now an alias for x: could be useful!
这只是在C++中没有实现。指向引用的指针根本没有意义,因为从引用中提取的指针只会指向被引用的对象。没有引用数组的可能原因是C++人员认为数组是从C继承的一种低级特性,有很多无法修复的缺陷,他们不想以数组作为任何新功能的基础。然而,如果存在引用数组,将清楚地说明引用必须是类型。
非const限定类型:在ISO C90中也发现了!
有些答案暗示引用不带const限定符。这是一个误导,因为声明const int &ri = i甚至没有尝试使引用带const限定符:它是对const限定类型的引用(它本身不是const)。就像const in *ri声明指向某个const的指针,但该指针本身并不是const。
话虽如此,引用本身确实不能携带const限定符。
然而,这并不奇怪。即使在ISO C 90语言中,也不是所有类型都可以是const。即,数组不行。
首先,不存在声明const数组的语法:int a const [42]是错误的。
但是,上述声明试图执行的操作可以通过中间typedef来表示:
typedef int array_t[42];
const array_t a;
但是这个并不像它看起来的那样。在这个声明中,并不是
a
被
const
修饰,而是数组的元素!也就是说,
a[0]
是一个
const int
,但
a
只是“int类型的数组”。因此,这不需要进行诊断:
int *p = a; /* surprise! */
这是做什么的:
a[0] = 1
再次强调,引用在类型系统中某种意义上是“二等公民”,就像数组一样。
请注意这个比喻更加深刻的部分,因为数组也有“隐式转换行为”,就像引用一样。程序员不需要使用任何显式运算符,标识符a自动变成一个int*指针,就好像使用了表达式&a[0]一样。这类似于引用ri,在我们将其用作主要表达式时,神奇地表示它绑定的对象i。这只是另一种“衰减”,就像“数组向指针衰减”一样。
正如我们不能因为“数组向指针”衰变而混淆认为“数组在C和C++中只是指针”,我们也不能认为引用只是没有自己类型的别名。
当decltype(ri)抑制了引用到其指示对象的通常转换时,这与sizeof a抑制数组到指针的转换并操作数组类型本身以计算其大小并没有太大的区别。
boolalpha(cout)
非常不寻常。你可以使用std::cout << boolalpha
代替。 - isanaei
也是一个引用,但由于历史原因,你没有以显式的方式声明它。因此,i
是一个引用,它引用了一个存储,而ri
是一个引用,它引用了相同的存储。但是,在i
和ri
之间的本质上没有区别。由于无法更改引用的绑定,因此没有必要将其标记为const
。让我声称 @Kaz 的评论比经过验证的答案好得多(永远不要使用指针来解释引用,引用是一个名称,指针是一个变量)。 - Jean-Baptiste Yunèsis_const
在这种情况下返回true
。在我看来,这是为什么const
基本上是反向的一个很好的例子; “可变”属性(如Rust的mut
)似乎更一致。 - Kyle Strandis_const<int const&>::value
是假的?”或类似的内容;我很难看出除了询问类型特征的行为之外,这个问题还有什么意义。 - M.M