当你使用const_cast去除const限定符时,修改一个值是什么情况下是可以的?

3
根据§7.1.5.1/4规定:
除了任何声明为mutable(7.1.1)的类成员可以被修改外,任何在其生命周期(3.8)中尝试修改const对象的行为都会导致未定义的行为。
因此我的问题是:什么时候一个对象是const对象?
特别地,非const对象中的const成员是否被视为const对象?
class Foo {
    const Bar bar;

    void replaceBar(Bar bar2) {
        *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
    }
}

这是因为我有一个不可变类(所有字段都是const),但我想要一个移动构造函数,它在技术上修改了传入的值。在这种情况下,“作弊”是可以接受的,因为它不会破坏逻辑上的常量性。


是的,确实是未定义行为。 - T.C.
1
如果您正在定义移动构造函数,那么您的成员变量中可能有一个指针。在这种情况下,使用可变指向常量对象的指针而不是常量指向常量对象的指针是否可接受?如果它不是指针或者不可平凡复制,则移动构造函数可能无法帮助。 - sjdowling
1
@sjdowling 不一定。最好从一个 std::string 开始移动。 - T.C.
2
我不同意“技术上”的分类。它确实修改了传递的值,并且确实破坏了您所谓的“逻辑常量性”。如果您将其定义为“const”,为什么编译器不应该假定在Foo a; Foo b(move(a));中,a.bar没有被修改?如果任何a的析构函数被内联,那么如果在编译时已知构造函数的效果,则可以轻松优化这些析构函数中的条件。 - user743382
2个回答

2
简单的规则是:如果原始对象不是const,那么可以去掉const限定词。因此,如果您有一个非const对象,并且将其作为const引用传递给函数,则在函数中去掉const限定词是合法的。 在您的示例中,原始对象是const,因此去掉const限定词是未定义的行为。

更准确地说:强制转换常量是允许的,但滥用它来尝试修改对象是不允许的。 - user743382
@hvd...如果原始对象是const的话。(如果它不是const,那么去除const属性并进行修改是粗鲁的,但是合法的)(为了更加准确!)(并且添加更多的括号,像这些:() (())() ((()())()))。 - Yakk - Adam Nevraumont
@Yakk 很正确。不过,我们所提到的对象确实被定义为“const”。 :) - user743382
Scott Meyers提供了一个非常好的例子,说明何时转换const是可以的,详见这个SO。不过我并不建议在其他情况下这样做。 - Giovanni Botta

0

让我们把这个变成一个完整的例子:

struct Bar { int x; };

struct Foo {
  const Bar bar;
  Foo( int x ):bar(x) {}

  void replaceBar(Bar bar2) {
    *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
  }
};

现在,让我们打破这个世界。

int main() {
  Foo f(3);
  Bar b = {2};
  f.replaceBar(b);
  std::cout << f.bar.x << "\n";
}

以上代码可以并且应该输出3,因为一个带有x=3const对象Bar被创建了。编译器可以并且应该假设这个const对象在其生命周期内不会改变。

让我们打破这个世界:

struct Bar {
  int* x;
  Bar(int * p):x(p) {}
  ~Bar(){ if (x) delete x; }
  Bar(Bar&& o):x(o.x){o.x=nullptr;}
  Bar& operator=(Bar&& o){
    if (x) delete x;
    x = o.x;
    o.x = nullptr;
  }
  Bar(Bar const&)=delete;
  Bar& operator=(Bar const&)=delete;
};

struct Foo {
  const Bar bar;
  Foo( int* x ):bar(x) {}

  void replaceBar(Bar bar2) {
    *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
  }
};

现在同一个游戏可能会导致编译器删除两次某些内容。

int main() {
  int* p1 = new int(3);
  Foo f( p1 );
  Bar b( new int(2) );
  f.replaceBar(std::move(b));
}

编译器将在replaceBar中删除p1,并应该在main结束时也删除它。这是因为您保证f.bar.x在其作用域结束之前会保持不变(const),然后您在replaceBar中违反了该承诺。

现在,这只是编译器有理由做的事情:一旦您修改了声明为const的对象,编译器可以实际上做任何事情,因为您已经引发了未定义行为。鼻子恶魔、时间旅行——任何事情都可能发生。

编译器利用某些行为未定义(即不允许)来进行优化。


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