严格的别名规则

时间:2015-07-24 16:05:07

标签: c++ strict-aliasing

我正在阅读有关reinterpret_cast及其别名规则(http://en.cppreference.com/w/cpp/language/reinterpret_cast)的说明。

我写了那段代码:

struct A
{
  int t;
};

char *buf = new char[sizeof(A)];

A *ptr = reinterpret_cast<A*>(buf);
ptr->t = 1;

A *ptr2 = reinterpret_cast<A*>(buf);
cout << ptr2->t;

我认为这些规则不适用于此:

  • T2是对象的(可能是cv限定的)动态类型
  • T2和T1都是(可能是多级,可能是每个级别的cv限定)指向相同类型T3的指针(自C ++ 11起)
  • T2是聚合类型或联合类型,它将上述类型之一保存为元素或非静态成员(包括递归地,包含联合的子聚合和非静态数据成员的元素):这使得它安全地从结构的第一个成员和从联合的元素转换到包含它的结构/联合。
  • T2是对象动态类型的(可能是cv限定的)有符号或无符号变体
  • T2是对象动态类型的(可能是cv限定的)基类
  • T2是char或unsigned char

在我看来,这段代码不正确。我对吗?代码是否正确?

另一方面,连接函数(man 2 connect)和struct sockaddr呢?

   int connect(int sockfd, const struct sockaddr *addr,
               socklen_t addrlen);

EG。我们有struct sockaddr_in,我们必须将它转换为struct sockaddr。以上规则也不适用,这种投射不正确吗?

2 个答案:

答案 0 :(得分:5)

是的,它无效,但不是因为您要将char*转换为A*:这是因为您没有获得实际指向A*的{​​{1}}并且,正如您所确定的,没有任何类型的别名选项适合。

你需要这样的东西:

A*

现在输入别名根本就没有了(虽然继续阅读,因为还有更多要做的事情!)。

实际上,这还不够。我们还必须考虑 alignment 。虽然上面的代码似乎可以正常工作,但是为了完全安全,您需要将#include <new> #include <iostream> struct A { int t; }; char *buf = new char[sizeof(A)]; A* ptr = new (buf) A; ptr->t = 1; // Also valid, because points to an actual constructed A! A *ptr2 = reinterpret_cast<A*>(buf); std::cout << ptr2->t; 放置到正确对齐的存储区域中,而不仅仅是new s的偶然块。 / p>

标准库(自C ++ 11开始)为我们提供了char

std::aligned_storage

或者,如果您不需要动态分配它,只需:

using Storage = std::aligned_storage<sizeof(A), alignof(A)>::type;
auto* buf = new Storage;

然后,做你的位置 - 新:

Storage data;

使用它:

new (buf) A();
// or: new(&data) A();

所有内容都是这样的:

auto ptr = reinterpret_cast<A*>(buf);
// or: auto ptr = reinterpret_cast<A*>(&data);

live demo

即便如此,由于C ++ 17,这有点复杂;有关详细信息,请参阅the relevant cppreference pages,并注意#include <iostream> #include <new> #include <type_traits> struct A { int t; }; int main() { using Storage = std::aligned_storage<sizeof(A), alignof(A)>::type; auto* buf = new Storage; A* ptr = new(buf) A(); ptr->t = 1; // Also valid, because points to an actual constructed A! A* ptr2 = reinterpret_cast<A*>(buf); std::cout << ptr2->t; }

当然,这一切似乎都是人为的,因为你只需要一个std::launder,因此不需要数组形式;事实上,你首先要创建一个标准的A。但是,假设A实际上实际上更大,并且您正在创建一个分配器或类似的东西,这是有道理的。

答案 1 :(得分:0)

从中派生C ++规则的C别名规则包括一个脚注,该脚注指定规则的目的是说什么时候可以别名。该标准的作者认为没有必要禁止实现在没有别名的情况下以不必要的限制性方式应用规则,因为他们认为编译器作者会尊重这句谚语:“不要阻止程序员去做。需要完成”,该标准的作者将其视为C精神的一部分。

很少需要使用聚合成员类型的左值对别名进行实际别名的情况,因此,标准不需要编译器识别此类别名是完全合理的。但是,在不涉及别名的情况下限制性地应用规则会导致以下情况:

union foo {int x; float y;};
int *p = &foo.x;
*p = 1;

或者就此而言,

union foo {int x; float y;};
foo.x = 1;

调用UB,因为该分配用于使用union foo来访问floatint的存储值,这不是允许的类型之一。但是,任何高质量的编译器都应该能够识别出,对从union foo显然是新派生的左值执行的操作是对union foo的访问和对{{1}的访问}可以影响其成员(在这种情况下,例如union foo成员)的存储值。

该标准的作者可能拒绝对脚注进行规范化,因为这样做需要正式定义何时通过新派生的左值进行的访问是对父级的访问,以及哪种访问模式构成别名。虽然大多数情况是很明确的,但在某些极端情况下,旨在用于低级编程的实现可能比用于例如以下描述的实现更悲观地解释。高端号码处理,该标准的作者认为,任何能够弄清楚如何处理较困难案件的人都应该能够处理较简单的案件。