Ser*_*sta 15 c++ casting strict-aliasing language-lawyer
这是关于内存重用的另一个问题的后续内容.由于最初的问题是关于具体实施,答案与具体实施有关.
所以我想知道,在一致的实现中,为一个不同类型的数组重用基本类型数组的内存是否合法:
我结束了以下示例代码:
#include <iostream>
constexpr int Size = 10;
void *allocate_buffer() {
void * buffer = operator new(Size * sizeof(int), std::align_val_t{alignof(int)});
int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok
for (int i=0; i<Size; i++) in[i] = i; // Defined behaviour because int is a fundamental type:
// lifetime starts when is receives a value
return buffer;
}
int main() {
void *buffer = allocate_buffer(); // Ok, defined behaviour
int *in = static_cast<int *>(buffer); // Defined behaviour since the underlying type is int *
for(int i=0; i<Size; i++) {
std::cout << in[i] << " ";
}
std::cout << std::endl;
static_assert(sizeof(int) == sizeof(float), "Non matching type sizes");
static_assert(alignof(int) == alignof(float), "Non matching alignments");
float *out = static_cast<float *>(buffer); // (question here) Declares a dynamic float array starting at buffer
// std::cout << out[0]; // UB! object at &out[0] is an int and not a float
for(int i=0; i<Size; i++) {
out[i] = static_cast<float>(in[i]) / 2; // Defined behaviour, after execution buffer will contain floats
// because float is a fundamental type and memory is re-used.
}
// std::cout << in[0]; // UB! lifetime has ended because memory has been reused
for(int i=0; i<Size; i++) {
std::cout << out[i] << " "; // Defined behaviour since the actual object type is float *
}
std::cout << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我添加了注释,解释了为什么我认为此代码应该定义了行为.恕我直言一切都很好,AFAIK标准符合,但我无法找到这里标记问题的行是否有效.
Float对象会重复使用int对象的内存,因此当浮点数的生命周期开始时,int的生命周期结束,因此stric-aliasing规则应该不是问题.数组是动态分配的,因此对象(int和float)实际上都是在返回的void类型数组中创建的operator new.所以我认为一切都应该没问题.
但由于它允许在现代C++中通常不赞成的低级对象替换,我必须承认我有一个疑问......
所以问题是:上面的代码是否调用UB,如果是,在哪里以及为什么?
免责声明:我建议在可移植的代码库中反对此代码,这实际上是一个语言律师问题.
Run Code Online (Sandbox Code Playgroud)int *in = reinterpret_cast<int *>(buffer); // Defined behaviour because alignment is ok
正确.但可能不是你所期望的那种意义.[expr.static.cast]
"指向
cv1 void"的类型的prvalue可以转换为"指向cv2 T" 的类型的prvalue ,其中T是一个对象类型,cv2与cv-qualification相同,或者cv-qualification比cv1.如果原始指针值表示A内存中字节的地址并且A不满足对齐要求T,则未指定结果指针值.否则,如果原始指针值指向一个对象a,并且存在一个b类型T(忽略cv-qualification)的对象,该对象是指针可互换的a,则结果是指向的对象b.否则,转换指针值不变.
没有int任何指针可互换的对象buffer,因此指针值不变.in是指向int*原始内存区域的类型指针.
Run Code Online (Sandbox Code Playgroud)for (int i=0; i<Size; i++) in[i] = i; // Defined behaviour because int is a fundamental type: // lifetime starts when is receives a value
是不正确的.[intro.object]
当隐式更改union的活动成员或创建临时对象时,对象由定义,new-expression创建.
明显缺席的是作业.没有int创建.实际上,通过消除,in是一个无效的指针,并且取消引用它是UB.
后者float*也跟随UB.
即使在没有通过正确使用new (pointer) Type{i};创建对象的所有上述UB的情况下,也不存在数组对象.(无关的)对象恰好在内存中并排发生.这意味着指针运算与结果指针也是UB.[expr.add]
当向指针添加或从指针中减去具有整数类型的表达式时,结果具有指针操作数的类型.如果表达式
P指向具有元素x[i]的数组对象x的n元素,则表达式P + J和J + P(其中J具有值j)指向(可能是假设的)元素,x[i+j] if 0 ? i+j ? n;否则,行为是未定义的.同样,表达式P - J指向(可能是假设的)元素,x[i?j] if 0 ? i?j ? n;否则行为是未定义的.
假设元素指的是一个过去(假设)元素.请注意,指向碰巧与另一个对象位于同一地址位置的一个past-the-end元素的指针不指向该另一个对象.
Passer By的答案涵盖了示例程序具有未定义行为的原因.我将尝试回答如何在没有最小UB的UB的情况下重用存储(在标准C++中,对于阵列存储的重用在技术上是不可能的,因为标准的当前措辞,所以为了实现重用,程序员必须依赖于实现"做正确的事").
转换指针不会自动显示对象.您必须首先构造浮动对象.这将开始它们的生命周期并结束int对象的生命周期(对于非平凡的对象,需要首先调用析构函数):
for(int i=0; i<Size; i++)
new(in + i) float;
Run Code Online (Sandbox Code Playgroud)
您可以使用placement new返回的指针(在我的示例中被丢弃)直接使用新构造的float对象,或者您可以std::launder使用buffer指针:
float *out = std::launder(reinterpret_cast<float*>(buffer));
Run Code Online (Sandbox Code Playgroud)
然而,这是 许多更典型重用类型的存储unsigned char(或std::byte)而不是存储int对象.