for*_*818 8 c++ strict-aliasing language-lawyer reinterpret-cast
我正在阅读有关严格别名的内容,但它仍然有点模糊,我无法确定定义/未定义行为的界限.我发现最详细的帖子集中在C.所以如果你能告诉我这是否允许以及自C++ 98/11以来发生了什么变化,那将是很好的...
#include <iostream>
#include <cstring>
template <typename T> T transform(T t);
struct my_buffer {
char data[128];
unsigned pos;
my_buffer() : pos(0) {}
void rewind() { pos = 0; }
template <typename T> void push_via_pointer_cast(const T& t) {
*reinterpret_cast<T*>(&data[pos]) = transform(t);
pos += sizeof(T);
}
template <typename T> void pop_via_pointer_cast(T& t) {
t = transform( *reinterpret_cast<T*>(&data[pos]) );
pos += sizeof(T);
}
};
// actually do some real transformation here (and actually also needs an inverse)
// ie this restricts allowed types for T
template<> int transform<int>(int x) { return x; }
template<> double transform<double>(double x) { return x; }
int main() {
my_buffer b;
b.push_via_pointer_cast(1);
b.push_via_pointer_cast(2.0);
b.rewind();
int x;
double y;
b.pop_via_pointer_cast(x);
b.pop_via_pointer_cast(y);
std::cout << x << " " << y << '\n';
}
Run Code Online (Sandbox Code Playgroud)
请不要过多关注可能的越界访问以及可能没有必要写这样的事实.我知道char*允许指向任何东西,但我也有一个T*指向a char*.也许还有一些我想念的东西.
这是一个完整的例子,包括push/pop via memcpy,afaik不受严格别名的影响.
TL; DR:上面的代码是否表现出未定义的行为(暂时忽略了一个超出范围的访问),如果是,为什么?C++ 11或其中一个较新的标准有什么变化吗?
小智 7
我知道
char*允许指向任何东西,但我也有一个T*指向achar*.
对,这是一个问题.虽然指针强制转换本身已定义了行为,但使用它来访问不存在的类型对象T则不然.
与C不同,C++不允许即兴创建对象*.您不能简单地将某个内存位置指定为类型T并创建该类型的对象,您需要具有该类型的对象.这需要放置new.以前的标准很不明确,但目前,每[intro.object]:
1 [...] 对象由定义(6.1),new-expression(8.3.4)创建,隐式更改union的活动成员(12.3)或创建临时对象时(7.4) ,15.2).[...]
由于您没有执行任何这些操作,因此不会创建任何对象.
此外,C++不会隐含地考虑指向同一地址的不同对象的指针.您&data[pos]计算指向char对象的指针.将其强制转换为T*不会指向T驻留在该地址的任何对象,并且取消引用该指针具有未定义的行为.C++ 17补充说std::launder,这是一种让编译器知道你想要访问该地址的不同对象的方法,而不是指向你的指针.
当您修改代码以使用放置new并且std::launder确保没有未对齐的访问时(我假设您为了简洁而将其留下),您的代码将具有已定义的行为.
*有关在未来版本的C++中允许这样做的讨论.
别名是两个引用相同对象的情况。那可能是引用或指针。
int x;
int* p = &x;
int& r = x;
// aliases: x, r ? *p refer to same object.
Run Code Online (Sandbox Code Playgroud)
对于编译器来说,重要的是要期望,如果使用一个名称编写了一个值,则可以通过另一个名称访问它。
int foo(int* a, int* b) {
*a = 0;
*b = 1;
return *a;
// *a might be 0, might be 1, if b points at same object.
// Compiler can't short-circuit this to "return 0;"
}
Run Code Online (Sandbox Code Playgroud)
现在,如果指针是不相关的类型,则编译器没有理由期望它们指向相同的地址。这是最简单的UB:
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int a = 0;
std::cout << a << std::endl;
x = foo(reinterpret_cast<float*>(&a), &a);
std::cout << a << "\n"; // Surprise?
}
Run Code Online (Sandbox Code Playgroud)
简而言之,严格的别名意味着编译器期望无关类型的名称引用不同类型的对象,因此它们位于单独的存储单元中。因为用于访问这些存储单元的地址实际上是相同的,所以访问存储值的结果是不确定的,并且通常取决于优化标志。
memcpy() 通过使用地址,通过指向char的指针来避免这种情况,并在库函数的代码内复制存储的数据。
严格的别名适用于工会成员,这是单独描述的,但原因是相同的:写入工会的一个成员并不能保证其他成员的值会改变。这不适用于存储在union中的结构开头的共享字段。因此,禁止通过联合进行类型修剪。(出于历史原因和维护遗留代码的便利,大多数编译器不赞成这样做。)
从2017年标准开始:6.10左值和右值
8如果程序尝试通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义
(8.1)—对象的动态类型,
(8.2)—对象的动态类型的cv限定版本,
(8.3)—与对象的动态类型类似的类型(定义见7.5),
(8.4)—一种类型,是与对象的动态类型相对应的有符号或无符号类型,
(8.5)—一种类型,是与对象的动态类型的CV限定版本相对应的有符号或无符号类型,
(8.6)—集合或联合类型,在其元素或非静态数据成员(包括递归地包括子集合或所包含的联合的元素或非静态数据成员)中包括上述类型之一,
(8.7)—一种类型,它是对象的动态类型的(可能是经过cv限定的)基类类型,
(8.8)— char,unsigned char或std :: byte类型。
在7.5中
1类型T的cv分解是cvi和Pi的序列,使得对于n> 0,T为“ cv0 P0 cv1 P1····cvn?1 Pn?1 cvn U”,其中每个cvi是一组cv -限定符(6.9.3),并且每个Pi是“指向”的指针(11.3.1),“指向类型为Ci的类的成员的指针”(11.3.3),“ Ni的数组”或“未知范围的数组” (11.3.4)。如果Pi指定一个数组,则元素类型上的cv限定词cvi + 1也被视为该数组的cv限定词cvi。[示例:用type-id const int **表示的类型具有两个cv分解,将U用作“ int”和“指向const int的指针”。— —结束示例] T的最长cv分解中的第一个,即cv1,cv2,...之后的cv限定词的n元组。。。cvn称为T的cv资格签名。
2如果两个类型T1和T2具有n相同的cv分解,从而对应的Pi分量相同并且用U表示的类型相同,则它们是相似的。
结果是:虽然您可以将指针reinterpret_cast指向不同,不相关且不相似的类型,但不能使用该指针访问存储的值:
char* pc = new char[100]{1,2,3,4,5,6,7,8,9,10}; // Note, initialized.
int* pi = reinterpret_cast<int*>(pc); // no problem.
int i = *pi; // UB
int* pc2 = reinterpret_cast<char*>(pi+2));
char c = *pc2; // no problem, unless increment didn't put us beyond array bound.
Run Code Online (Sandbox Code Playgroud)
重新解释演员表也不会创建它们指向的对象,并且为不存在的对象分配值是UB,因此,如果它指向的类不是很简单的话,则不能使用演员表的取消引用结果来存储数据。