严格的别名规则

Strict aliasing rule

我正在阅读有关 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;

我认为这些规则在这里不适用:

我认为此代码不正确。我对吗?代码是否正确?

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

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

例如。我们有 struct sockaddr_in 并且我们必须将它转换为 struct sockaddr。上面的规则也不适用,所以这个转换不正确吗?

是的,它是无效的,但不是因为您要将 char* 转换为 A*:这是因为您没有获得实际上指向 [=] 的 A* 17=] 并且,正如您所确定的,none 类型别名选项适合。

你需要这样的东西:

#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;

现在类型别名根本不会出现(但请继续阅读,因为还有更多工作要做!)。

实际上,这还不够。我们还必须考虑对齐。尽管上面的代码可能看起来有效,但为了完全安全等等,您需要将 placement-new 放入正确对齐的存储区域,而不是随意的 char 块。

标准库(C++11 起)为我们提供了 std::aligned_storage 来做到这一点:

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

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

Storage data;

然后,做你的 placement-new:

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

并使用它:

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

全部看起来像这样:

#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;
}

(live demo)

即便如此,从 C++17 开始,这还是有点复杂;更多信息见the relevant cppreference pages,关注std::launder.

当然,这整件事看起来是做作的,因为您只需要一个 A,因此不需要数组形式;事实上,您首先会创建一个沼泽标准 A。但是,假设 buf 在现实中实际上更大并且您正在创建分配器或类似的东西,这是有道理的。

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

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

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

甚至,就此而言,

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

调用 UB,因为赋值用于使用 int 访问 union foofloat 的存储值,这不是允许的类型之一。然而,任何高质量的编译器都应该能够识别出对从 union foo 明显新鲜派生的左值所做的操作是对 union foo 的访问,以及对 [=12= 的访问] 可以影响其成员的存储值(如本例中的 float 成员)。

标准的作者可能拒绝将脚注规范化,因为这样做需要正式定义何时通过新派生的左值访问是对父级的访问,以及什么样的访问模式构成别名。虽然大多数情况下都是很明确的,但也有一些极端情况,用于低级编程的实现可能比那些用于低级编程的实现解释得更悲观。高端数字运算,标准的作者认为,任何能够弄清楚如何处理更难的案例的人都应该能够处理简单的案例。