const成员和赋值运算符.如何避免未定义的行为?

Ale*_*tov 33 c++ const undefined-behavior assignment-operator

回答了关于std :: vector of objects和const-correctness的问题,得到了不应有的 downvote和关于undefined行为的评论.我不同意,因此我有一个问题.

考虑使用const成员的类:

class A { 
public: 
    const int c; // must not be modified! 
    A(int c) : c(c) {} 
    A(const A& copy) : c(copy.c) { }     
    // No assignment operator
}; 
Run Code Online (Sandbox Code Playgroud)

我想要一个赋值运算符,但我不想const_cast在下面的代码中使用其中一个答案:

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is undefined behavior
    return *this; 
} 
Run Code Online (Sandbox Code Playgroud)

我的解决方案是

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}  
Run Code Online (Sandbox Code Playgroud)

我有未定义的行为吗?

请你没有UB的解决方案.

Ste*_*sop 39

您的代码导致未定义的行为.

不只是"未定义如果A被用作基类而这个,那个或那个".实际上是未定义的,总是如此.return *this已经是UB,因为this不能保证引用新对象.

具体来说,考虑3.8/7:

如果在对象的生命周期结束之后并且在重用或释放对象占用的存储之前,则在原始对象占用的存储位置创建新对象,指向原始对象的指针,引用引用原始对象,或者原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用来操纵新对象,如果:

...

- 原始对象的类型不是const限定的,如果是类类型,则不包含任何类型为const限定的非静态数据成员或引用类型,

现在,"在对象的生命周期结束之后,在重用或释放对象占用的存储之前,在原始对象占用的存储位置创建一个新对象"正是您正在做的事情.

你的目标是类类型,并且它确实包含非静态数据成员,其类型为const限定.因此,在赋值运算符运行后,引用旧对象的指针,引用和名称不能保证引用新对象并可用于操作它.

作为可能出错的具体示例,请考虑:

A x(1);
B y(2);
std::cout << x.c << "\n";
x = y;
std::cout << x.c << "\n";
Run Code Online (Sandbox Code Playgroud)

期待这个输出?

1
2
Run Code Online (Sandbox Code Playgroud)

错误!你可能得到那个输出似乎是合理的,但是const成员是3.8/7中规定的规则的例外,因此编译器可以将它x.c视为它声称的const对象.换句话说,允许编译器将此代码视为:

A x(1);
B y(2);
int tmp = x.c
std::cout << tmp << "\n";
x = y;
std::cout << tmp << "\n";
Run Code Online (Sandbox Code Playgroud)

因为(非正式地)const对象不会改变它们的值.优化涉及const对象的代码时,此保证的潜在价值应该是显而易见的.为了能够在x.c 调用UB的情况下进行任何修改,必须删除此保证.因此,只要标准作者完成了他们的工作而没有错误,就没有办法做你想做的事.

[*]事实上,我对使用thisnew作为参数的问题表示怀疑- 可能你应该将它复制到void*第一个,并使用它.但是我并不担心这是否特别是UB,因为它不能保存整个功能.

  • 很棒的发现.我认为比@ sbi的答案更好.+1`:)` (3认同)

sbi*_*sbi 23

第一:当你创建一个数据成员时const,你告诉编译器和全世界这个数据成员永远不会改变.当然,你不能分配它,你肯定不会欺骗编译器接受这样做的代码,无论这个技巧多么聪明.
您可以将const数据成员赋值运算符分配给所有数据成员.你不能两者兼得.

至于你对问题的"解决方案":
我认为在为该对象调用的成员函数内的对象上调用析构函数会立即调用UB.未初始化的原始数据调用构造函数,这就是被调用的地方,现在的构造是在原始数据调用的居住对象的成员函数中创建一个对象 ...也是非常多的声音就像UB给我.(见鬼,只是把它拼出来让我的脚趾甲卷曲.)而且,不,我没有标准的章节和诗句.我讨厌阅读标准.我想我无法忍受它的表.

但是,除了技术性之外,我承认只要代码保持与示例一样简单,您就可以在几乎所有平台上使用"解决方案" .不过,这并不是一个好的解决方案.事实上,我认为它甚至不是一个可接受的解决方案,因为IME代码永远不会那么简单.多年来它将被扩展,改变,变异和扭曲,然后它将默默地失败,并且需要麻烦的36小时调试转移以找到问题.我不了解你,但每当我发现这样的一段代码负责36小时的调试乐趣时,我想扼杀那个为我做这件事的愚蠢的傻瓜.

香草萨特,在他的GotW#23,通过剖析这一块一块的想法,最后总结说,"是充满陷阱的,它往往是错误的,它使生活派生类的作者一个人间地狱 ...... 从来没有使用的伎俩通过使用显式析构函数然后放置new来实现复制构造方面的复制赋值,即使这个技巧每三个月在新闻组中出现一次"(强调我的).

  • @Alexey:`<耸肩>`你现在也可能想要1000万美元,"没有争论".你仍然不会得到它. (4认同)
  • 我每天都想要免费的蛋糕吃午饭,但这不会发生.你想要的是从根本上与C++不兼容.也许你应该退后一步 - 很明显,你正在创建的类本身不是`const`,因为实例可以修改,而`c`字段不是`const`,因为修改实例会修改`c`.因此,``c`应该不被标记为`const`.**使它成为非`constst并使其成为`private`,并添加一个返回值的成员函数`int getFoo()const`,而不是而不是试图通过箍来做C++和*基本逻辑*所说的是一个荒谬的事情. (4认同)
  • @Alexey目前尚不清楚为什么要改变你明确告诉编译器*永远不会改变*的东西. (3认同)
  • @sbi:"我没有这个标准的章节和章节" - 实际上,我认为如果不是`const`数据成员,它将被定义为行为.也许由于Herb Sutter和其他人提出的所有原因设计不佳,但AFAIK定义,只要它仅用于动态类型为A的对象.这是基于我的答案中的章节和经文. (3认同)
  • GotW链接+1.我认为这本身就说明为什么你的"解决方案"值得投票. (2认同)

Pup*_*ppy 9

如果它有一个const成员,你如何分配给A?你正试图完成一些根本不可能的事情.你的解决方案没有新的行为,不一定是UB,但绝对是你的.

简单的事实是,你正在改变一个const成员.你需要取消对你的成员的约束,或者抛弃赋值运算符.你的问题没有解决办法 - 这是完全矛盾的.

编辑以获得更清晰:

Const强制转换并不总是引入未定义的行为.但是,你肯定做到了.除了你确定T是一个POD类之外,在你放入它之前不会调用所有析构函数并且你甚至没有调用正确的析构函数是不明确的.此外,还有各种形式的继承所涉及的未定义的未定义行为.

您确实调用了未定义的行为,并且可以通过不尝试分配给const对象来避免这种情况.

  • @Alexey:你确实在bucketloads中有未定义的行为,你可以通过不尝试分配给const对象来避免它. (2认同)