const对象和成员指针的const正确性,构造函数漏洞

const correctness with const objects and member pointers, constructor vulnerability

本文关键字:const 构造函数 漏洞 正确性 指针 对象 成员      更新时间:2023-10-16
class Test
{
public:
Test() : i(0), ptr(&i) {}
int i;
int *ptr;
void change_const (int x) const { *ptr=x; }
};
int main()
{
const Test obj;
obj.ptr = &obj.i; // error
obj.change_const(99);
return 0;
}

虽然在obj中,ptr属于int *const类型,但是构造函数可以使他指向const int类型的i。明确的尝试当然是失败的。为什么构造函数会提供有关const正确性的漏洞?其他非直接显而易见的漏洞,如

int *ptr;
const int **c_ptr = &ptr; // error
const int c = 10;
*c_ptr = &c;
*ptr = 20; // because here change of c possible

也被认为是可以预防的。

const是一个语言级别的概念。也就是说,当您编译代码并将其作为机器代码执行时,所有数据或多或少都被视为数据。请注意,我说"或多或少"是因为我们忽略了这样一个事实,即理论上,const数据可能存储在只读页面中,并在写入时触发页面故障;但由于页面大小的粒度,这并不常见。因此,正在发生的情况如下:

构造函数初始化ptr的值以指向i的地址。由于obj对象是const,因此无法直接修改i的值,也无法更改ptr指向的位置。但是,您可以访问和操作ptr指向的内存(在本例中为i的值)。

因此,由于编译器不检查/知道/关心ptr是否指向i,因此它不会捕获const的冲突。相反,它只是看到您修改ptr所指向的数据。

显然,构造函数(如果不是ctor的主体,则至少是初始值设定项列表)需要能够向i写入值。

碰巧,C++实现这一点的方法是使this成为构造函数(和析构函数)中非常数的指针。基本上,objconst-ness直到构造函数完成执行才开始。这就是漏洞存在的原因,因为对于如何构造const限定对象的技术问题,有一个简单但不完美的解决方案。

也许原则上可以采取不同的做法。我想你需要一个单独的const版本的构造函数,编译器在其中应用不同的规则(就像正常的成员函数可以是const一样),将数据成员视为const,因此(1)允许它们初始化但不分配,(2)禁止从&i初始化ptr,因为后者的类型为int const*。C++没有做到这一点,因此它有一个你已经钻过的漏洞。如果它真的这样做了,在某些情况下,人们将更难编写构造函数,所以这是一种设计权衡。

请注意,类似地,volatile限定的对象在其自己的构造函数或析构函数中不是volatile

Steve Jessop回答了这个问题,但值得一提的是,这里引用了标准(强调我的):

12.1/4构造函数不应是虚拟的(10.3)或静态的(9.4)。构造函数可以为const、volatile或const-voile对象调用。构造函数不应声明为const、volatile或const volatile(9.3.2)。const和volatile语义(7.1.6.1)不应用于正在构建的对象。当派生最多的对象(1.8)的构造函数结束时,它们就会生效构造函数不应使用ref限定符进行声明。

因此,从构造函数的角度来看,即使创建了常量对象,*this也不是常量对象。这可能会有不同的设计,但常量对象的构造函数将远不如非常量对象的构造器灵活;例如,它们总是必须初始化初始化器列表中的所有成员;他们不能在构造函数的主体中使用循环等来设置复杂成员的值。