【问题标题】:const correctness with const objects and member pointers, constructor vulnerabilityconst 对象和成员指针的 const 正确性,构造函数漏洞
【发布时间】:2014-01-13 01:25:49
【问题描述】:
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中的ptrint *const类型,但构造函数可以让他指向i类型const int。明确尝试这样做当然会失败。为什么构造函数会提供这个关于 const 正确性的漏洞?其他非直接明显的漏洞,如

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

也是经过深思熟虑的预防。

【问题讨论】:

  • C++ 有时会给你足够的绳索让你上吊。只是不要一开始就写狗的代码并按规则玩

标签: c++ class pointers const-correctness


【解决方案1】:

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

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

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

【讨论】:

  • 所以编译器不/不能尊重/关心构造函数中this的类型? const Test *const this; this->ptr = &(this->i);
  • @mb84:是的,请参阅下面史蒂夫·杰索普的回复。 const 不能保证直到对象被初始化之后,也就是对象的单个构造函数完成执行之后。
【解决方案2】:

显然构造函数(或者至少是初始化列表,如果不是 ctor 的主体)需要能够将值写入i

碰巧的是,C++ 实现这一点的方式是使this 在构造函数(和析构函数)中成为指向非常量的指针。基本上,objconst-ness 在构造函数完成执行之前不会开始。这就是漏洞存在的原因,因为对如何构造const-qualified 对象的技术问题提供了一种简单但不完美的解决方案。

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

请注意,volatile-qualified 对象在其自己的构造函数或析构函数中也不是 volatile

【讨论】:

  • 是的,我也考虑过单独的const 构造函数版本的这些可能性,但我认为理论上最好只有像你的观点(2)这样的东西:改变运算符& 返回具有type 的(成员)变量的const *type。关于第 (1) 点,this 应该真正保持非常量,直到构造函数完成以允许复杂的赋值,如提到的 jogojapan(循环,...)。所以只有在一个额外的const-constructor 中的& 的另一个含义可以阻止这个漏洞?也许?
  • @mb84:嗯,您必须确定& 的含义是必要且充分的。如果您想允许i = 1,那么人们会期望init(&i) 可以使用void init(int *p) { *p = 1; }。我想在这种情况下你会强迫他们使用const_cast,但它越来越难看。它不仅仅是指针,同样的问题也适用于引用。大概您会禁止将i 绑定到非常量引用(即使它是非常量左值),同时使&i 具有类型const int*(即使iint) .它不漂亮,这并不是说它不起作用。
  • 想想看,这还是不够的,因为它仍然允许有人写void setptr(Test *t) { t->ptr = &t->i; }。即使您在构造函数中使用& 魔法,如果this 具有Test* 类型,那么您也不会阻止setptr(this)
  • 嗯。也许 1. 使 & 在构造函数内部变魔术,而不是在函数调用内部(就像您的 init(&i) 向上一样)。 2. 仅在函数调用(如您的setptr(this))中制作this 魔术(= const)会做到这一点。甚至没有必要将 i 绑定到非常量引用,因为您必须在此别名之前添加一个魔术 &
【解决方案3】:

Steve Jessop 回答了这个问题,但就其价值而言,这是来自标准的引述(强调我的):

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

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

【讨论】:

  • 感谢您的回答,另请参阅我在 Steve Jessop 的评论。你怎么看?
  • 我认为拥有构造函数的 const 版本是有意义的,并且只允许使用它来初始化 const 对象。现在有了统一的初始化语法和移动语义,复杂的成员通常可以在没有构造函数体的情况下进行初始化。 const 对象无论如何都很少见。所以我想如果我要重新设计 C++,我会提出这个建议。但是现在更改它显然会导致很多向后兼容性问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-08-17
  • 1970-01-01
  • 2014-10-18
  • 1970-01-01
  • 1970-01-01
  • 2012-01-06
  • 1970-01-01
相关资源
最近更新 更多