未定义的对象(〔basic.life〕/8):为什么允许引用重新绑定(和常量修改)
Undead objects ([basic.life]/8): why is reference rebinding (and const modification) allowed?
"不死"子句
我把不死子句称为C++规则,即在对象被破坏后,如果在同一地址创建了一个新对象,有时它可以被视为与旧对象相同的对象。这个规则在C++中一直存在,但在附加条件上有一些变化。
这个问题使我不得不阅读最新的亡灵条款。Lifetime[basic.life]/8中的修订条件为:
(8.1)新对象的存储正好覆盖存储原始对象占用的位置,以及
嗯,嗯。不同地址的对象不会是同一个对象。
(8.2)新对象与原始对象的类型相同(忽略顶级cv限定符),以及
再说一次,duh。
(8.4)原始对象和新对象都不是潜在重叠的子对象([interro.object])。
它不能是基类、classic(或具有使其地址不唯一的特殊声明的成员)。再说一遍,啊。
(8.3)原始对象既不是一个完整的对象const限定也不是此类对象的子对象,以及
现在这很有趣。被替换的对象不能是:
- 一个完整的const对象
- 完整const对象的一部分
另一方面,被复活的对象可以是:
- 常量成员子对象
- 此类常量成员的子对象
- const对象数组中的元素
构造子对象
所以在我看来,所有这些对象x
都可以复活:
构造成员子对象
struct CI {
const int x;
};
CI s = { 1 };
new ((void*)&s.x) int(2);
int r = s.x; // OK, 2
const成员的子对象:
struct T {
int x;
};
struct CT {
const T m = { 1 };
};
CT s;
new ((void*)&s.m.x) int (2);
int r = s.m.x;
常量对象数组中的元素:
const int x[1] = { 1 };
new ((void*)&x[0]) int (2);
int r = x[0];
具有const和引用成员的类
此外,类类型的对象中包含const或引用成员似乎并不被禁止;复活的对象仍然被称为CCD_ 2。
使用const成员类:
struct CIM {
CIM(int i): m(i) {}
const int m;
};
CIM x(1);
new ((void*)&x) CIM(2);
int r = x.m; // OK, 2
使用参考成员上课:
struct CRM {
CRM (int &r): m(r) {}
int &m;
};
int i=1,j=2;
CRM x(i);
new ((void*)&x) CRM(j);
int r = x.m; // OK, 2
问题
- 对该条款的解释正确吗
- 如果是,是否有其他条款禁止这些覆盖操作
- 如果是,这是有意的吗?为什么会改变
- 这对代码生成器来说是一个突破性的变化吗?所有编译器真的支持这一点吗?它们不是基于常量成员进行优化吗?数组的常量元素是不可变的,引用是不可重循环的
- 额外的问题:这会影响具有足够存储类(当然不是动态创建的对象)和足够初始化的常量对象的ROM能力吗
注意:我后来添加了奖金,因为在讨论中提到了将常数放入ROM。
如果标准中与对象生命周期相关的所有要求都不在[基本生命]中,那将是令人惊讶的。
在你引用的标准段落中,"完整"形容词被无意中添加到"对象"这个名字中的可能性很小。
在论文P0137中,人们可以阅读到这篇理性的文章(以下引用在@LanguageLawyer评论中的论文):
这对于允许std::optional等类型包含const子对象是必要的;现有的限制是为了允许只读性而存在的,因此只影响完整的对象。
为了让我们放心,我们可以验证编译器是否遵循了标准措辞:它们对完全常量对象执行常量优化,但对非常量完全对象的常量成员子对象不执行常量优化:
让我们考虑一下这个代码:
struct A{const int m;};
void f(const int& a);
auto g(){
const int x=12;
f(x);
return x;
}
auto h(){
A a{12};
f(a.m);
return a.m;
}
Clang和GCC在针对x86_64:时都会生成此程序集
g(): # @g()
push rax
mov dword ptr [rsp + 4], 12
lea rdi, [rsp + 4]
call f(int const&)
mov eax, 12 ;//the return cannot be anything else than 12
pop rcx
ret
h(): # @h()
push rax
mov dword ptr [rsp], 12
mov rdi, rsp
call f(int const&)
mov eax, dword ptr [rsp] //the content of a.m is returned
pop rcx
ret
返回的值放置在寄存器eax
中(根据ABI规范:System V x86处理器特定的ABI):
在函数
g
中,编译器可以自由地假设x
不能通过对f
的调用来更改,因为x
是一个完整的常量对象。因此,值12
被直接作为立即值放置在eax
寄存器中:mov eax, 12
。在函数
h
中,编译器不能自由地假设a.m
不能通过对f
的调用来更改,因为a.m
不是完整const对象的子对象。因此,在调用f
之后,必须将a.m
的值从内存加载到eax
:mov eax, dword ptr [rsp]
。
答案的大部分必要部分已经由其他用户给出,所以我将它们收集在社区wiki中,以便这个问题可以得到一个可接受的答案。
-
是的,除了关于
const
数组(正如T.C.的评论中所指出的)。元素为x
0的数组本身就是const
对象,因此如果它是完整的对象,那么它的元素就不能在原地销毁和重新创建(正如胡桃木的评论中指出的那样)。 -
没有。正如语言律师在评论中指出的那样,它被允许的事实是明确的。俄罗斯国家机构要求允许它,并且对C++20进行了相应的修改以允许它
-
请参见上文。
-
- 正如Oliv的回答中所指出的,编译器确实意识到,他们不应该基于非
const
完整对象的const
成员不能被替换的假设进行优化,因为标准规定它们可以被替换 - OP还询问这是否是一个"错误";"断裂变化";。好吧,在什么意义上?由于C++17不允许这种形式的替换(如果尝试,可能会导致UB),而C++20允许,这意味着如果编译器的行为为了实现C++20行为而改变,那么UB就会减少,这不是一个突破性的改变
- 编译器在C++17时代有没有进行过这种优化,后来被C++20禁止了?俄罗斯国家机构的评论暗示了一个合理的论点,即为什么答案可能是";否";。与之相关的美国国家机构的评论更为明确。在这个答案的末尾给出了一个可能被这种优化破坏的代码示例,以避免格式问题。这里,在
v
中创建的第一个S
对象被销毁,然后在其位置创建新的S
对象。v.data()
是最初指向旧对象的指针。它现在指向新对象(导致打印2
)还是不指向对象,导致UB?我认为我们可以放心地假设,没有一个广泛使用的编译器会将此代码视为UB,以便应用C++17中技术上允许的优化。(当然,也有一些注意事项:编译器本可以在标准库类中使用魔术来关闭这种优化,或者他们本可以让push_back
操作始终用其自身的清洗版本替换存储的指针,但在实践中,这并没有做到。)总的来说,编译器似乎不必因为C++20中替换规则的放松而改变他们的行为
- 正如Oliv的回答中所指出的,编译器确实意识到,他们不应该基于非
-
不,没有。显然,最初只有完整的
const
对象是可ROM的(而不是非const
完整对象的const
子对象),正如所讨论的,完整的const
对象不受此更改的影响。
代码:
#include <iostream>
#include <vector>
struct S {
S(int x) : x(x) {}
const int x;
};
int main() {
std::vector<S> v;
v.push_back(S(1));
v.pop_back();
v.push_back(S(2));
std::cout << v.data()->x;
}
- CBasePin 递增对拥有过滤器的引用.循环引用?
- 常量引用和引用之间的区别
- 为什么对const的引用可以引用文字
- 如何通过引用获取引用变量的地址?
- 取消引用unique_ptr引用
- 模板类中引用的引用的类型是什么
- 引用其他引用和指向引用的指针
- 构造函数参数中的引用调用引用的默认构造函数
- 为什么添加对右值引用的引用不是错误
- 在python中通过引用传递引用
- 如何检测ptr在引用超出范围后是否仍在引用有效引用
- 对于 C++ lambda,通过引用捕获引用的规则是什么
- C++引用和引用参数
- 在 C++ 中通过引用返回 - 引用赋值与值赋值
- 使用"(boost::any a)"而不是"(const boost::any&a)"来防止引用到引用
- 引用到引用在C++中是什么意思?(不是右值引用)
- 在c++中将指针取消引用到引用中
- 通过引用传递引用
- Java 引用和C++引用有什么区别
- 为什么我们需要在 lambda 中捕获引用的引用