首先,正如这个问题(对象的 std::vector 和 const-correctness)operator=()
所引发的那样,您(我可能会说非常巧妙)使用“placement new”作为实现赋值运算符的手段的全部动机是现在作废了。从 C++11 开始,该问题的代码现在没有错误。在这里查看我的答案。
其次, C++11 的emplace()
函数现在几乎完全按照您使用placement new所做的工作,除了现在编译器本身几乎保证它们都是按照C++ 标准定义良好的行为。
第三,当接受的答案指出:
因为this
不能保证引用新对象
我想知道这是否是因为变量中包含的this
值可能会被放置新的复制构造操作更改,而不是因为使用该类的实例的任何东西都可能保留它的缓存值,以及旧的实例数据,而不是读取来自内存的对象实例的新值。如果是前者,在我看来,您可以通过使用指针this
的临时副本来确保赋值运算符函数内部是正确的,如下所示:this
// Custom-defined assignment operator
A& operator=(const A& right)
{
if (this == &right) return *this;
// manually call the destructor of the old left-side object
// (`this`) in the assignment operation to clean it up
this->~A();
// Now back up `this` in case it gets corrupted inside this function call
// only during the placement new copy-construction operation which
// overwrites this objct:
void * thisBak = this;
// use "placement new" syntax to copy-construct a new `A`
// object from `right` into left (at address `this`)
new (this) A(right);
// Note: we cannot write to or re-assign `this`.
// See here: https://stackoverflow.com/a/18227566/4561887
// Return using our backup copy of `this` now
return *thisBak;
}
但是,如果它与被缓存的对象有关并且每次使用时都不会重新读取,我想知道是否volatile
可以解决这个问题!即:volatile const int c;
用作类成员而不是const int c;
.
第四,在我的其余答案中,我将重点放在volatile
应用于类成员的 的用法上,看看这是否可以解决这两个潜在的未定义行为案例中的第二个:
您自己的解决方案中的潜在 UB:
// Custom-defined assignment operator
A& operator=(const A& right)
{
if (this == &right) return *this;
// manually call the destructor of the old left-side object
// (`this`) in the assignment operation to clean it up
this->~A();
// use "placement new" syntax to copy-construct a new `A`
// object from `right` into left (at address `this`)
new (this) A(right);
return *this;
}
您提到的潜在 UB 可能存在于其他解决方案中。
// (your words, not mine): "very very bad, IMHO, it is
// undefined behavior"
*const_cast<int*> (&c)= assign.c;
尽管我认为添加volatile
可能会解决上述两种情况,但我在此答案的其余部分中的重点是上面的第二种情况。
tldr;
在我看来,如果您添加volatile
并制作类成员变量volatile const int c;
而不是仅const int c;
. 我不能说这是一个好主意,但我认为抛弃const
并写信c
成为明确定义的行为并且完全有效。否则,行为是未定义的,只是因为读取可能c
会被缓存和/或优化,因为它只是const
,而不是volatile
。
阅读以下内容以了解更多详细信息和理由,包括查看一些示例和一些组装。
const 成员和赋值运算符。如何避免未定义的行为?
写给const
成员只是未定义的行为......
...因为编译器可能会优化对变量的进一步读取const
,因为它是. 换句话说,即使您已经正确更新了内存中给定地址中包含的值,编译器可能会告诉代码只是反刍寄存器中保存它第一次读取的值的最后一个值,而不是回到内存中地址并在每次从该变量中读取时实际检查新值。
所以这:
// class member variable:
const int c;
// anywhere
*const_cast<int*>(&c) = assign.c;
可能是未定义的行为。它可能在某些情况下有效,但在其他情况下无效,在某些编译器上但在其他编译器上无效,或者在某些版本的编译器中有效,但在其他情况下无效。我们不能依赖它来获得可预测的行为,因为语言没有指定每次我们将变量设置为const
然后写入和读取时应该发生什么。
例如这个程序(见这里:https ://godbolt.org/z/EfPPba ):
#include <cstdio>
int main() {
const int i = 5;
*(int*)(&i) = 8;
printf("%i\n", i);
return 0;
}
打印5
(尽管我们希望它打印8
)并在main
. (请注意,我不是装配专家)。我已经标记了printf
线条。您可以看到,即使8
写入该位置 ( mov DWORD PTR [rax], 8
),这些printf
行也不会读出该新值。他们读出了先前存储的内容5
,因为他们不认为它会发生变化,即使它确实发生了变化。行为未定义,因此在这种情况下省略了读取。
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 5
lea rax, [rbp-4]
mov DWORD PTR [rax], 8
// printf lines
mov esi, 5
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
volatile const
然而,写入变量并不是未定义的行为......
...因为volatile
告诉编译器最好在每次读取该变量时读取实际内存位置的内容,因为它可能随时更改!
你可能会想:“这有意义吗?” (有一个volatile const
变量。我的意思是:“什么可能会改变一个const
变量以使我们需要标记它volatile
!?)答案是:“嗯,是的!确实有道理!”在微控制器和其他低级内存映射嵌入式设备上,一些可能随时被底层硬件更改的寄存器是只读的。为了在 C 或 C++ 中将它们标记为只读,我们使它们const
,但为了确保编译器在每次读取变量时更好地知道它实际上在其地址位置读取内存,而不是依赖于保留先前缓存值的优化,我们还将它们标记为volatile
。因此,将地址标记0xF000
为一个名为的只读 8 位寄存器REG1
,我们会在某处的头文件中这样定义它:
// define a read-only 8-bit register
#define REG1 (*(volatile const uint8_t*)(0xF000))
现在,我们可以随心所欲地读取它,并且每次我们要求代码读取变量时,它都会。这是定义明确的行为。现在,我们可以做这样的事情,这段代码不会被优化出来,因为编译器知道这个寄存器值实际上可以在任何给定时间改变,因为它是volatile
:
while (REG1 == 0x12)
{
// busy wait until REG1 gets changed to a new value
}
而且,要标记REG2
为 8 位读/写寄存器,当然,我们只需删除const
. 然而,在这两种情况下volatile
都是必需的,因为硬件可能在任何给定时间更改值,因此编译器最好不要对这些变量做出任何假设或尝试缓存它们的值并依赖缓存的读数。
// define a read/write 8-bit register
#define REG2 (*(volatile uint8_t*)(0xF001))
因此,以下不是未定义的行为!据我所知,这是非常明确的行为:
// class member variable:
volatile const int c;
// anywhere
*const_cast<int*>(&c) = assign.c;
即使变量是const
,我们也可以丢弃const
并写入它,编译器会尊重它并实际写入它。而且,既然变量也被标记为volatile
,编译器将每次读取它,并且也尊重它,与读取REG1
或REG2
以上相同。
因此,现在我们添加了这个程序volatile
(在此处查看:https ://godbolt.org/z/6K8dcG ):
#include <cstdio>
int main() {
volatile const int i = 5;
*(int*)(&i) = 8;
printf("%i\n", i);
return 0;
}
prints 8
,现在是正确的,并在main
. 我再次标记了这些printf
线条。请注意我也标记的新行和不同行!这些是汇编输出的唯一变化!其他每一行都是完全相同的。下面标记的新行发出并实际读取变量的新值并将其存储到寄存器eax
中。接下来,在准备打印时,不是像以前那样将硬编码的内容移动到 register 中5
,而是将刚刚读取的 register 的内容移动到 register 中,其中现在包含一个, 。解决了!添加修复它!esi
eax
8
esi
volatile
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 5
lea rax, [rbp-4]
mov DWORD PTR [rax], 8
// printf lines
mov eax, DWORD PTR [rbp-4] // NEW!
mov esi, eax // DIFFERENT! Was `mov esi, 5`
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
这是一个更大的演示(在线运行:https ://onlinegdb.com/HyU6fyCNv )。您可以看到我们可以通过将变量转换为非常量引用或非常量指针来写入变量。
在所有情况下(同时转换为非常量引用或非常量指针以修改 const 值),我们可以使用 C++ 样式转换或 C 样式转换。
在上面的简单示例中,我验证了在所有四种情况下(甚至使用 C 样式强制转换来强制转换为引用:(int&)(i) = 8;
,奇怪的是,因为 C 没有引用 :))汇编输出是相同的。
#include <stdio.h>
int main()
{
printf("Hello World\n");
// This does NOT work!
const int i1 = 5;
printf("%d\n", i1);
*const_cast<int*>(&i1) = 6;
printf("%d\n\n", i1); // output is 5, when we want it to be 6!
// BUT, if you make the `const` variable also `volatile`, then it *does* work! (just like we do
// for writing to microcontroller registers--making them `volatile` too). The compiler is making
// assumptions about that memory address when we make it just `const`, but once you make it
// `volatile const`, those assumptions go away and it has to actually read that memory address
// each time you ask it for the value of `i`, since `volatile` tells it that the value at that
// address could change at any time, thereby making this work.
// Reference casting: WORKS! (since the `const` variable is now `volatile` too)
volatile const int i2 = 5;
printf("%d\n", i2);
const_cast<int&>(i2) = 7;
// So, the output of this is 7:
printf("%d\n\n", i2);
// C-style reference cast (oddly enough, since C doesn't have references :))
volatile const int i3 = 5;
printf("%d\n", i3);
(int&)(i3) = 8;
printf("%d\n\n", i3);
// It works just fine with pointer casting too instead of reference casting, ex:
volatile const int i4 = 5;
printf("%d\n", i4);
*(const_cast<int*>(&i4)) = 9;
printf("%d\n\n", i4);
// or C-style:
volatile const int i5 = 5;
printf("%d\n", i5);
*(int*)(&i5) = 10;
printf("%d\n\n", i5);
return 0;
}
样本输出:
Hello World
5
5
5
7
5
8
5
9
5
10
笔记:
- 我还注意到,在修改
const
类成员时,即使它们不是,上述方法也有效volatile
。请参阅我的“std_optional_copy_test”程序!例如:https ://onlinegdb.com/HkyNyTt4D 。然而,这可能是未定义的行为。要使其定义明确,请创建成员变量volatile const
而不是const
.
- 您不必从
volatile const int
to 转换的原因volatile int
(即:为什么只是int
引用或int
指针)工作得很好,是因为volatile
影响变量的读取,而不是变量的写入。因此,只要我们通过 volatile 变量方式读取变量,就可以保证我们的读取不会被优化。这就是为我们提供定义明确的行为的原因。写入总是有效——即使变量不是volatile
.
参考:
- [我自己的回答] “安置新”有什么用?
- x86 组装指南
- 将对象的“this”指针更改为指向不同的对象
- 编译器资源管理器输出,带有汇编,来自 godbolt.org:
- 这里:https ://godbolt.org/z/EfPPba
- 在这里:https ://godbolt.org/z/6K8dcG
- [我的答案] STM32 微控制器上的寄存器级 GPIO 访问:像 STM8 一样编程 STM32(寄存器级 GPIO)