为什么在C++中引用不是“const”?

87
我们知道 "const 变量" 表示一旦赋值,就不能改变该变量,例如:
int const i = 1;
i = 2;

上面的程序将无法编译; gcc会提示一个错误:

assignment of read-only variable 'i'

没问题,我能理解,但是以下示例超出了我的理解范围:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

它输出

true
false
奇怪。我们知道一旦引用绑定到一个名称/变量上,我们不能改变这个绑定,只能改变它所绑定的对象。因此我认为ri的类型应该与i相同:当iint const时,为什么ri不是const

3
另外,boolalpha(cout)非常不寻常。你可以使用std::cout << boolalpha代替。 - isanae
6
“ri”被视为与“i”无法区分的“别名”,这就是所有的内容。 - Kaz
1
这是因为引用总是绑定到相同的对象。i 也是一个引用,但由于历史原因,你没有以显式的方式声明它。因此,i 是一个引用,它引用了一个存储,而 ri 是一个引用,它引用了相同的存储。但是,在 iri 之间的本质上没有区别。由于无法更改引用的绑定,因此没有必要将其标记为 const。让我声称 @Kaz 的评论比经过验证的答案好得多(永远不要使用指针来解释引用,引用是一个名称,指针是一个变量)。 - Jean-Baptiste Yunès
1
很棒的问题。在看到这个例子之前,我也会预期is_const在这种情况下返回true。在我看来,这是为什么const基本上是反向的一个很好的例子; “可变”属性(如Rust的mut)似乎更一致。 - Kyle Strand
1
也许标题应该改为“为什么is_const<int const&>::value是假的?”或类似的内容;我很难看出除了询问类型特征的行为之外,这个问题还有什么意义。 - M.M
显示剩余2条评论
6个回答

55

为什么"ri"不是"const"?

std::is_const检查类型是否有const限定或者没有。

如果T是一个const限定的类型(即const或const volatile),则提供成员常量值等于true。对于任何其他类型,该值为false。

但是引用不能被const限定。References [dcl.ref]/1

Cv限定的引用是非法的,除非cv限定符是通过typedef名称([dcl.typedef],[temp.param])或decltype-specifier([dcl.type.simple])引入的,在这种情况下,cv限定符将被忽略。

所以is_const<decltype(ri)>::value将返回false,因为ri(引用)不是const限定类型。正如您所说,我们不能在初始化后重新绑定引用,这意味着引用始终是“const”,另一方面,const限定引用或非const限定引用实际上可能没有意义。


5
直接进入标准,因此直截了当地表达观点,而不是接受答案中的所有猜测。 - underscore_d
5
这是一篇不错的回答,但要注意作者同时也在问为什么要这样做。"因为它说了这样做"并不足以回答这个问题的这个方面。 - simpleuser
6
另一个回答也没有回答这个方面。如果引用无法重新绑定,为什么is_const不返回true?那个答案试图将指针如何是可选可重置的类比到引用上,而引用则不可重置,这样做会因同样的原因导致自我矛盾。我不确定有真正的解释,除了标准写作人员做出了某种程度的任意决定,有时候这就是我们所能希望的最好结果。因此,这就是我的回答。 - underscore_d
2
我认为另一个重要的方面是,decltype 不是一个函数,因此它直接在引用本身上工作,而不是在所引用的对象上。 (这可能更与“引用基本上是指针”的答案相关,但我仍然认为这是使这个例子令人困惑并且值得在这里提到的部分。) - Kyle Strand
3
进一步解释 @KyleStrand 的评论:decltype(name) 与普通的 decltype(expr) 有所不同。例如,decltype(i)i 声明的类型,即 const int,而 decltype((i)) 将是 int const & - Daniel Schepler
显示剩余8条评论

55

这可能看起来有些反直觉,但我认为理解这一点的方法是意识到,在某些方面,引用在语法上像指针一样对待。

对于指针而言,这似乎是合乎逻辑的:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

输出:

true
false
这是合乎逻辑的,因为我们知道不是指针对象是 const(它可以指向其他地方),而是被指向的对象是 const。
因此,我们正确地将指针本身的 const 属性返回为 false。
如果我们想要使指针本身成为 const,我们必须这样说:
int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

输出:

true
true

因此,我认为我们可以看到与引用的句法类比。

然而,引用在语义上与指针有一个重要区别,就是一旦绑定,我们不允许将引用重新绑定到另一个对象。

因此,即使引用指针具有相同的语法,规则仍然不同,所以语言防止我们像这样声明引用本身为const

---

And so I think we see a syntactic analogy with the reference.

However references are semantically different to pointers especially in one crucial respect, we are not allowed to rebind a reference to another object once bound.

So even though references share the same syntax as pointers the rules are different and so the language prevents us from declaring the reference itself const like this:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

我认为我们不允许这样做是因为当语言规则防止引用被重绑定时,与指针(如果未声明为const)相同的方式,它似乎并不需要。

所以回答这个问题:

问) 为什么C++中“引用”不是“const”?

在您的示例中,语法使得被引用的内容与声明指针时一样被视为const

正确或错误地,我们不允许将引用本身设置为const,但是如果允许,它看起来会像这样:

int const& const ri = i; // not allowed
Q)我们知道一旦一个引用与名称/变量绑定,我们不能改变这个绑定,只能改变其绑定的对象。那么我认为ri的类型应该与i相同:当iint const时,为什么ri不是const
为什么decltype()没有转移到引用所绑定的对象上?
我认为这是为了与指针的语义等价,也可能与decltype()的功能有关(声明类型),它会回顾在绑定发生之前进行了什么声明

13
你的意思好像是“引用不能被const修饰,因为它们总是const”? - ruakh
2
也许 "在语义上,一旦它们被绑定,所有对它们的操作都会传递给所引用的对象" 是正确的,但是 OP 希望这也适用于 decltype,但发现并不是这样的。 - ruakh
11
这段话中夹杂了许多曲折的推测和不太适用的指针类比,我认为这些只会让问题变得更加混乱。与此相反,sonyuanyao 像检查标准一样直戳要点:引用不是 cv-可限定类型,因此它不能是“const”,所以“std::is_const”必须返回“false”。他们本可以使用意味着“std::is_const”必须返回“true”的措辞,但他们没有这样做。就是这样!所有关于指针、 “我假设”、“我猜测”等的内容都没有提供任何真正的阐释。 - underscore_d
1
@underscore_d 这可能更适合在聊天中讨论,而不是在这里的评论部分,但是你有没有一个“不必要的混淆”的例子,来说明这种关于引用的思考方式? 如果它们可以这样实现,那么以这种方式考虑它们有什么问题呢?(我认为这个问题不算,因为 decltype 不是运行时操作,所以“在运行时,引用的行为类似于指针”的想法,无论正确与否,都不适用。) - Kyle Strand
1
为了完全清晰起见,这是我原始评论的修正版本:引用不像指针在语法上被处理; 它们被视为指针上的*语法糖。因此,如果ri是一个指针,那么OP期望的类似is_const表达式实际上是is_const <decltype(* ri)> ::value(即解除引用指针,就好像decltype()是一个函数)。 - Kyle Strand
显示剩余14条评论

18

你需要使用std::remove_reference来获取你要找的值。

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

欲了解更多信息,请参考此帖子


9
为什么宏不是const?函数?字面值?类型名称? const的东西只是不可变的东西的一个子集。 由于引用类型只是那些类型,为了与其他类型(特别是指针类型)对称,要求在所有类型上都需要使用const限定符可能有一定道理,但这会很快变得非常繁琐。
如果C++默认具有不可变对象,并要求在任何您不希望const的内容上使用mutable关键字,那么这将变得容易:只需不允许程序员将mutable添加到引用类型即可。
现在,没有资格。因为它们是不可变的,没有资格。
而且,由于它们没有被const限定,所以is_const返回真值可能更加混乱。
我认为这是一个合理的妥协,特别是因为不可变性只是通过不存在修改引用的语法来实施的。

6

这是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;

但是这个并不像它看起来的那样。在这个声明中,并不是aconst修饰,而是数组的元素!也就是说,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抑制数组到指针的转换并操作数组类型本身以计算其大小并没有太大的区别。

您提供了很多有趣和有用的信息(+1),但它更或多或少地局限于 "引用在类型系统中" 这个事实--这并不能完全回答 OP 的问题。在这个回答和 @songyuanyao 的回答之间,我认为已经有足够的信息来理解情况,但它感觉像是回答所需的 背景 而不是一个完整的回答。 - Kyle Strand
1
此外,我认为这句话值得强调:“当您使用ri时,引用会被透明地解析...”一个关键点(我在评论中提到过,但迄今为止没有出现在任何答案中)是decltype不执行此透明解析(它不是一个函数,因此ri并非以您描述的方式“使用”)。这与您对类型系统的整体关注非常契合--关键联系在于decltype一种类型系统操作 - Kyle Strand
嗯,关于数组的内容感觉有些离题,但最后一句话很有帮助。 - Kyle Strand
尽管此时我已经为你点赞了,而且我也不是原帖作者,所以听取我的批评对你来说既没有得到什么,也没有失去什么。 - Kyle Strand
显然,一个简单(且正确的)答案只是指向标准,但我喜欢这里的背景思考,尽管有些人可能会认为其中的某些部分是假设。+1。 - Steve Kidd

-6

“const X& x” 表示 x 是一个 X 对象的别名,但你不能通过 x 改变那个 X 对象。

请参见 std::is_const


1
这并没有回答问题。 - bolov

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