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


86

我们知道,“常量变量”表示一旦分配,就不能更改该变量,如下所示:

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:什么时候i是an int const,为什么riconst呢?


3
另外,boolalpha(cout)是非常不寻常的。您可以std::cout << boolalpha代替。
isanae

6
ri成为与难以区别的“别名”非常重要i
卡兹(Kaz)2016年

1
这是因为引用始终是同一对象的边界。i也是参考,但由于历史原因,您没有以明确的方式声明它。因此i,引用是指存储,并且ri是引用相同的存储。但是i 和之间在性质上没有区别ri。由于您无法更改引用的绑定,因此无需将其限定为const。让我断言@Kaz注释比经过验证的答案要好得多(永远不要使用指针解释引用,ref是名称,ptr是变量)。
Jean-BaptisteYunès16年

1
很棒的问题。在看到此示例之前,我也希望在这种情况下也is_const能返回true。我认为,这是一个很好的例子,说明了为什么const从根本上倒退了。一个“可变”属性(如la Rust的属性mut)看起来会更加一致。
凯尔·斯特兰德

1
也许标题应该更改为“为什么是is_const<int const &>::value假的?” 或类似;除了询问类型特质的行为外,我
MM

Answers:


52

这似乎是违反直觉的,但我想明白这一点的方式是要认识到,在某些方面,引用被视为语法指针

对于指针来说,这似乎合乎逻辑:

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(可以使它指向其他地方)而是指针对象

因此,我们可以正确地看到指针本身的常量性返回为。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

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,但如果我们将这个样子:

int const& const ri = i; // not allowed

问:我们知道,一旦将引用绑定到名称/变量,就无法更改此绑定,而是更改其绑定对象。所以我想类型ri应该和i:什么时候i是a int const,为什么riconst呢?

为什么decltype()未将引用转移到裁判绑定的对象?

我想这是为了实现与指针的语义等效,也许decltype()(声明的类型)的功能是回顾绑定发生之前所声明的内容。


13
听起来您是在说“引用不能为const,因为它们始终是const”?
ruakh

2
可能确实是“在绑定它们之后,在语义上对它们的所有动作都被转移到了它们所引用的对象上”,但是OP希望它也同样适用,但decltype发现并非如此。
ruakh

10
这里有很多曲折的假设和可疑地适用于指针的类比,当我像Sonyuanyao那样检查标准时,我认为混淆了这个问题,使它超出了必要之处:引用不是cv合格的类型,因此它不可能const,因此std::is_const必须返回false。他们可以改用必须返回的措词true,但事实并非如此。而已!关于指针的所有这些东西,“我假设”,“我假设”等都没有提供任何真正的说明。
underscore_d

1
@underscore_d与此处的评论部分相比,这可能是一个更好的聊天问题,但是您是否通过这种思考引用的方式获得了“不必要的困惑”示例?如果可以通过这种方式实现它们,那么以这种方式思考又有什么问题呢?(我不认为这个问题计数,因为decltype不是一个运行时操作,因此,“在运行时,引用的行为像指针”理念,无论正确与否,并没有真正适用。
凯尔东街

1
引用也不在语法上像指针一样对待。它们具有不同的语法来声明和使用。所以我什至不知道你的意思。
乔丹·梅洛

55

为什么“ ri”不是“ const”?

std::is_const 检查类型是否为const限定。

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

但是引用不能是const限定的。参考文献[dcl.ref] / 1

除通过使用typedef名称([dcl.typedef],[temp.param])或decltype-specifier([dcl.type.simple])引入cv限定符外,具有cv限定符的引用格式不正确。 ,在这种情况下,cv限定词将被忽略。

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


5
直接遵循标准,因此直截了当,而不是接受的答案的所有令人费解的假设。
underscore_d

5
@underscore_d这是一个很好的答案,除非您意识到操作人员也在询问原因。“因为它是这样说的”对于问题的这一方面并没有真正的用处。
simpleuser 2013年

5
@ user9999999其他答案也没有真正回答该方面。如果参考不能反弹,为什么不is_const返回true?该答案试图对指针如何可选地可重定位进行类比,而引用却不是-这样做时,出于相同的原因导致自相矛盾。除了编写标准的人有些武断的决定外,我不确定这两种方式是否有真正的解释,有时候这是我们所希望的。因此,这个答案。
underscore_d

2
另一个重要的方面,我认为,是decltype不是函数,因此是直接对引用本身而不是在参照的对象。(这可能与“引用基本上是指针”的答案更相关,但我仍然认为这是使此示例令人困惑的部分内容,因此在这里值得一提。)
Kyle Strand

3
为了进一步阐明@KyleStrand的评论,其decltype(name)行为与general有所不同decltype(expr)。因此,例如,decltype(i)的声明类型iconst int,而isdecltype((i))int const &
Daniel Schepler


7

为什么不是宏const?职能?文字?类型名称?

const 事物只是不可变事物的子集。

因为引用类型就是那个类型,所以const在所有引用类型上都要求使用-qualifier以便与其他类型(尤其是指针类型)对称是很有意义的,但这很快就会变得很繁琐。

如果C ++有默认不可变对象,要求mutable你什么关键字希望const,那么这本来是很容易:根本就没有允许程序员添加mutable到引用类型。

就其本身而言,它们没有条件就不会改变。

而且,由于它们const不合格,因此在引用类型上产生true可能会更加令人困惑is_const

我发现这是一个合理的折衷方案,特别是因为无论如何,仅由于不存在用于对引用进行变异的语法这一事实而增强了不可变性。


5

这是C ++中的一个怪癖/功能。尽管我们不认为引用是类型,但实际上它们是在类型系统中“坐下”的。尽管这看起来很尴尬(考虑到使用引用时,引用语义会自动出现,并且引用“摆脱干扰”),但仍有一些合理的理由说明为什么要在类型系统中对引用进行建模,而不是将引用作为外部的单独属性进行建模类型。

首先,让我们考虑并非声明名称的每个属性都必须在类型系统中。从C语言开始,我们有“存储类”和“链接”。可以将名称引入extern const int ri,其中extern指示静态存储类和链接的存在。类型是const int

C ++显然接受这样一个概念,即表达式具有类型系统之外的属性。该语言现在具有“值类”的概念,它是一种尝试组织越来越多的表达式可以显示的非类型属性的尝试。

但是引用是类型。为什么?

过去在C ++教程中曾解释说,这样const int &ri引入的声明ri具有type 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是相同的。如果两个结构或类具有相同的类型,则它们的布局以及访问所有成员的语义都相同。引用的存在改变了类型的这些方面,因此将它们合并到类型系统中是一个直接的设计决定。(但是,请注意此处的一个反参数:struct / class成员可以是static,它也会更改表示形式;但这不是类型!)

因此,引用在类型系统中为“第二类公民”(与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-qualifiable类型:在ISO C90发现呢!

一些答案暗示了引用没有const限定词的事实。那真是个红鲱鱼,因为声const int &ri = i明甚至都没有尝试进行const-qualified引用:它是对const限定类型的引用(它本身不是const)。就像const in *ri声明一个指向某物的指针一样const,但是该指针本身不是const

也就是说,确实引用不能const本身带有限定符。

然而,这并不是那么奇怪。即使使用ISO C 90语言,也不是所有类型都可以const。也就是说,数组不能是。

首先,声明常量数组的语法不存在:int a const [42]是错误的。

但是,上面的声明试图做的事情可以通过一个中间体来表达typedef

typedef int array_t[42];
const array_t a;

但这并没有像看起来那样做。在此声明,这不是a它得到const合格的,但元素!也就是说,a[0]const int,但a只是“整数数组”。因此,这不需要诊断:

int *p = a; /* surprise! */

这样做:

a[0] = 1;

再一次强调了这样的想法,即引用在某种意义上是类型系统中的“第二类”,例如数组。

请注意,类比的含义更深,因为数组还具有“不可见的转换行为”,如引用。无需程序员使用任何显式运算符,标识符就a自动变成一个int *指针,就像使用了表达式&a[0]一样。这类似于引用ri,当我们将其用作主要表达式时,如何神奇地表示i绑定到的对象。这只是另一个“衰变”,例如“指针衰减数组”。

就像我们一定不要对“指向指针的数组”的迷恋误入为“数组只是C和C ++中的指针”而感到困惑一样,我们同样也不能认为引用只是没有自己类型的别名。

decltype(ri)抑制对引用对其引用对象的常规转换时,这与sizeof a抑制数组到指针的转换以及对数组类型本身进行操作以计算其大小没有什么不同。


您在这里(+1)有很多有趣和有用的信息,但是或多或少地局限于“引用在类型系统中”这一事实-这不能完全回答OP的问题。在这个答案和@songyuanyao的答案之间,我想说的是有足够的信息来了解情况,但感觉这是答案的必要背景,而不是完整的答案。
凯尔·斯特兰德

1
另外,我认为这句话值得强调:“当您使用ri时,引用被透明地解析了……”一个关键点(我已经在评论中提到过,但到目前为止在任何答案中都没有出现) )decltype 不会执行此透明解析(这不是函数,因此ri不会按照您所描述的意义“使用”)。这符合中非常漂亮,与你的整个重点类型系统-它们关键连接是decltype一个类型系统的操作
凯尔·斯特兰德

嗯,关于数组的内容感觉很切线,但是最后一句话很有帮助。
凯尔·斯特兰德

.....尽管在这一点上我已经支持了您,但我不是OP,所以您听我的批评不会让您真正得失....
Kyle Strand

显然,简单(正确的答案)将我们引向标准,但我喜欢这里的背景思考,尽管有人可能会认为其中的部分内容是假设。+1。
史蒂夫·基德

-5

const X&x”表示x是X对象的别名,但是您不能通过x更改该X对象。

并参阅std :: is_const


这甚至都没有试图回答这个问题。
bolov '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.