C++,类,常量和奇怪的语法

7

今天我重新阅读了《C++ Primer(第四版)》中关于成员函数和const引用等部分,并写出了这个奇怪的小程序:

using std::cout;
using std::endl;

class ConstCheater
{
public:
    ConstCheater(int avalue) : ccp(this), value(avalue) {}
    ConstCheater& getccp() const {return *ccp;}
    int value;
private:
    ConstCheater* ccp;
};

int main()
{
    const ConstCheater cc(7); //Initialize the value to 7
    cout << cc.value << endl;
    cc.getccp().value = 4;    //Now setting it to 4, even though it's const!
    cout << cc.value << endl;
    cc.value = 4;             //This is illegal
    return 0;
}

我的问题是——为什么C++允许这样的语法?当一个类被声明为const时,为什么我可以编辑普通的数据成员?难道const的目的不是为了使其值不能被修改吗?

2
您可以规避C++的大多数安全机制,因为该语言的基本理念是程序员非常清楚自己在做什么。对我来说,您的程序看起来几乎不像是一个“意外”。如果您绝对自己把脚打伤,请随便去做。 C ++不会阻止您。 - fredoverflow
@FredOverflow,在这种情况下很明显,因为他正在试图让它变得明显,但即使清楚为什么会发生这种情况,解决这个问题仍然是一个有趣的概念性问题,因为人们可以想象这种情况在错综复杂的方式中意外发生。 - Catskul
8个回答

6
即使 getccp() 是一个 const 方法,它也不能保证你对其返回的引用所做的事情。该方法本身不修改对象,因此不会违反规则。
如果它返回了一个 const ConstCheater&,那就不同了。
正如你的示例所显示的那样,const 要比仅应用于对象更加复杂。C++ FAQ 中有一个关于常量正确性的章节,特别是它涵盖了你在这里强调的情况。

我认为常见问题解答中没有涵盖这种情况...它只是简单地概述了这些函数的实现,但表面上似乎需要在某处使用const_cast - Potatoswatter
1
@Tony:这里的问题是他成功地创建了一个非const引用到const数据,而没有使用const_cast,这不完全是你所说的。 - Dennis Zickefoose
@Dennis:只有在这里创建的对象 const ConstCheater cc(7); 是const,其中的数据没有明确标记为const。拥有一个const对象意味着我们只能调用const方法,他也确实这样做了,但是返回的指针/引用(在这种情况下恰好指向同一对象)没有被标记为const。指针和引用不会从它们所指向的对象“继承”const属性,它取决于类的接口和内部实现来确保遵守const约定。 - Tony
@Tony:常量对象的所有成员本身都是常量。因此,没有明确定义的方法可以更改cc.value。是的,cc的常量性不会改变*ccp的常量性,但反之亦然。尝试在除构造函数以外的任何方法中将ccp分配给this,您将因为这个原因而得到编译时错误。 - Dennis Zickefoose
@Dennis:是的,你关于尝试分配给*ccp是正确的。我甚至已经把const的工作方式搞混了 :) - Tony
显示剩余2条评论

2
我会说,你标记为正确的Tony的答案是错误的,而Michael Burr的答案是正确的。
更清楚地说(至少对我来说):
有两个可能会引起误解的地方:
1. 隐式const的工作方式 2. 在常量对象构造期间解释this的方式

1. 隐式 const

隐式 const(当ConstCheater被声明为const时内部const化)并不会将cc转换为指向常量的指针,而是变成一个指向常量的指针,也就是说,当你这样做:

  const ConstCheater cc(7); 

从内部来看:
  ConstCheater * ccp;

"...到..."
  ConstCheater * const ccp;

...而不是...
  const ConstCheater * ccp;    

可能会预料到这种情况。
2. const 对象的构造
更奇怪的是,this 在构造函数中可以被传递给 cpp 的初始化器,因为人们会认为 this 应该被视为指向常量的指针,因此不是传递给 const-pointer 的有效值。
也就是说,人们可能会期望:
 ...: ccp(this) ... // expected to fail but doesnt

失败是因为在概念上你可能会认为这 (有点) 相当于:
 const ConstCheater         cc(7);
 const ConstCheater * const this = &cc; // const-pointer-to-const

因此,你会认为:
 ConstCheater * const ccp = this; //expected error!

“would fail!但实际上它没有失败,因为显然在构建过程中,this被特殊处理,就好像它是:”
 const ConstCheater * this = &cc; 

因此,在构造期间对象实际上是不可变的。
我不确定我完全理解推理,但Michael Burr指出存在一种逻辑和技术障碍,无法提供预期的行为,因此标准似乎削弱了当前有些奇怪的行为。
最近我问了一个相关的问题:为什么C++没有const构造函数?但到目前为止,我还没有完全理解为什么这是不可行的,尽管我认为这将给C++开发人员带来负担,因为他们必须为他们想要创建常量对象的任何类定义一个笨拙的const构造函数。

有点晚了 :) - 但是你说的很有道理。 - Seb Holzapfel

2

真正的问题不在于ConstCheater::getccp()的行为 - 而在于该行没有错误:

const ConstCheater cc(7);

该代码使用一个非const指针来初始化应该是const的this指针。但是,构造函数不能是const(9.3.2/5),但稍加思考就可以明白为什么。因此,构造函数允许使用指向const对象(或即将成为const对象)的指针来初始化非const指针。这就是你要解决的问题。

至于为什么允许这样做,我想标准很难尝试关闭这个漏洞,因为它必须枚举构造函数的所有方式,这些方式在构造const对象时必须被视为const,并且在构造非const对象时必须被视为non-const。这似乎是一项相当困难的任务。


2
构造函数允许修改常量对象的值,是的。但如果它不能这样做,那么它能做什么呢?
由于构造函数具有这种访问权限,它可以将其“转发”给其他人或“保存”以供以后使用。当然,这样做可能不是一个好主意。
这是C++安全机制无法防止您构建不良程序的情况之一。C++绝不是万无一失的。所以,请小心!

2

对象在构造函数完成后才变为常量。因此,当您存储它时,this是指向非常量内存的指针,但不久之后就会发生改变。这就是为什么允许第一次赋值,因为您没有做错任何事情。这意味着cpp是一个指向常量的指针,但编译器并不知道这一点。它无法知道;毕竟,您声明了它是非常量的。这仍然是未定义的行为,只是不同于您的编译器可以真正帮助您捕获的类型。


+1,更具体地说,对象的生命周期直到构造函数完成之前才开始,因此构造函数实际上正在查看具有少量明确定义属性的内存块(§3.8/1,§3.8/3)。 - Potatoswatter

0

你的对象并不完全是不可变的:只有你创建的指向它的引用是常量。


该对象无法被更改,因为它被直接声明为“const”。在这个程序中,“const”引用在哪里? - Potatoswatter
main 函数中,他声明了一个常量引用,名称为 const ConstCheater cc(7); - Tony
@Tony:那不是一个引用,它是一个对象。 - Potatoswatter

0
你正在做的是将对一个ConstCheater的引用声明为const。但是ConstCheater中没有任何内容会使value成为const。

0

const 限定符会限制您在对象上调用非 const 方法,因此问题在于您的设计允许您通过 const 方法给出非 const 引用成员。常见的解决方法是

      Member& getccp()       {return *member;}
const Member& getccp() const {return *member;} 

在某些情况下,当您的对象的逻辑常量性不受其成员的外部修改影响时,您可以允许这种情况发生。
      Member& getccp() const {return *member;}

另一个逻辑和形式常量不同的例子是可变成员。即,如果您在最后一个const方法调用时计算了一些术语,则可变可以是该术语。如果您在下一个调用中获得相同的输入,则可以轻松返回存储的值。

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