28

注意:此问题已被重命名并简化,以使其更具针对性和可读性。大多数评论都引用了旧文本。


根据标准,不同类型的对象可能不会共享相同的内存位置。所以这是不合法的:

std::array<short, 4> shorts;
int* i = reinterpret_cast<int*>(shorts.data()); // Not OK

然而,该标准允许这条规则有一个例外:任何对象都可以通过指向charor的指针来访问unsigned char

int i = 0;
char * c = reinterpret_cast<char*>(&i); // OK

但是,我不清楚这是否也允许反过来。例如:

char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?
4

3 回答 3

23

由于涉及的指针转换,您的某些代码是有问题的。请记住,在这些情况下reinterpret_cast<T*>(e)具有语义,static_cast<T*>(static_cast<void*>(e))因为所涉及的类型是标准布局。(事实上​​,我建议您在处理存储时始终使用static_castvia 。)cv void*

仔细阅读标准表明,在指针转换到或从指针转换过程中,T*假定确实T*涉及到一个实际的对象——这在你的一些片段中很难实现,即使是“作弊”,这要归功于类型的琐碎性涉及(稍后会详细介绍)。但是,那将是题外话,因为...

别名与指针转换无关。这是 C++11 文本,概述了通常称为“严格别名”规则的规则,来自 3.10 Lvalues 和 rvalues [basic.lval]:

10 如果程序尝试通过非下列类型之一的左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 对象的动态类型的 cv 限定版本,
  • 与对象的动态类型类似(如 4.4 中定义)的类型,
  • 与对象的动态类型相对应的有符号或无符号类型,
  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,
  • 聚合或联合类型,在其元素或非静态数据成员中包括上述类型之一(递归地包括子聚合或包含联合的元素或非静态数据成员),
  • 一个类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,
  • char 或 unsigned char 类型。

(这是 C++03 中相同子句的第 15 段,文本中有一些细微的变化,例如使用 'lvalue' 而不是 'glvalue',因为后者是 C++11 的概念。)

根据这些规则,让我们假设一个实现为我们提供了magic_cast<T*>(p)“以某种方式”将指针转换为另一种指针类型的方法。通常这reinterpret_cast,在某些情况下会产生未指定的结果,但正如我之前解释的那样,对于指向标准布局类型的指针而言,情况并非如此。那么很明显,您的所有片段都是正确的(用 替换reinterpret_castmagic_cast,因为magic_cast.

这是一个似乎错误使用的片段magic_cast,但我认为它是正确的:

// assume constexpr max
constexpr auto alignment = max(alignof(int), alignof(short));
alignas(alignment) char c[sizeof(int)];
// I'm assuming here that the OP really meant to use &c and not c
// this is, however, inconsequential
auto p = magic_cast<int*>(&c);
*p = 42;
*magic_cast<short*>(p) = 42;

为了证明我的推理,假设这个表面上不同的片段:

// alignment same as before
alignas(alignment) char c[sizeof(int)];

auto p = magic_cast<int*>(&c);
// end lifetime of c
c.~decltype(c)();
// reuse storage to construct new int object
new (&c) int;

*p = 42;

auto q = magic_cast<short*>(p);
// end lifetime of int object
p->~decltype(0)();
// reuse storage again
new (p) short;

*q = 42;

这个片段是精心构建的。特别是,即使由于 3.8 对象生命周期 [basic.life] 的第 5 段中规定的规则而被销毁,new (&c) int;我仍允许使用。相同的第 6 段给出了与存储引用非常相似的规则,第 7 段解释了一旦对象的存储被重用,用于引用对象的变量、指针和引用会发生什么——我将它们统称为 3.8/5- 7.&cc

在这种情况下&c(隐式)转换为void*,这是对尚未重用的存储的指针的正确使用之一。类似地p是从构造&c新的之前获得的。int它的定义可能会移到 销毁之后c,这取决于实现魔法的深度,但肯定不是在int构造之后:第 7 段将适用,这不是允许的情况之一。对象的构造short也依赖于p成为指向存储的指针。

现在,因为intshort是微不足道的类型,我不必使用对析构函数的显式调用。我也不需要对构造函数的显式调用(也就是说,对通常在中声明的标准放置 new 的调用<new>)。从 3.8 对象生命周期 [basic.life] 开始:

1 [...] T 类型对象的生命周期开始于:

  • 获得具有适合类型 T 的对齐和大小的存储,并且
  • 如果对象有非平凡的初始化,它的初始化就完成了。

T 类型对象的生命周期在以下情况下结束:

  • 如果 T 是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或者
  • 对象占用的存储空间被重用或释放。

这意味着我可以重写代码,以便在折叠中间变量之后q得到原始代码段。

请注意p不能折叠。也就是说,下面的内容肯定是不正确的:

alignas(alignment) char c[sizeof(int)];
*magic_cast<int*>(&c) = 42;
*magic_cast<short*>(&c) = 42;

如果我们假设一个int对象是(平凡地)用第二行构造的,那么这一定意味着&c成为一个指向已被重用的存储的指针。因此第三行是不正确的——尽管严格来说是由于 3.8/5-7 而不是由于别名规则。

如果我们不假设,那么第二行违反char c[sizeof(int)]了别名规则:我们正在通过类型的 glvalue读取实际上是一个对象int,这不是允许的例外之一。相比之下,*magic_cast<unsigned char>(&c) = 42;就可以了(我们假设short在第三行简单地构造了一个对象)。

就像 Alf 一样,我还建议您在使用存储时明确使用标准放置 new。跳过琐碎类型的破坏很好,但是当遇到时,*some_magic_pointer = foo;您很可能面临违反 3.8/5-7(无论获得该指针的方式多么神奇)或别名规则。这也意味着存储新表达式的结果,因为一旦构造了对象,您很可能无法重用魔术指针——再次由于 3.8/5-7。

但是,读取对象的字节(这意味着使用charor unsigned char)很好,而且您甚至根本不需要使用reinterpret_cast或任何魔术。static_castviacv void*可以说很适合这项工作(尽管我确实觉得标准可以在那里使用更好的措辞)。

于 2012-09-27T07:02:51.263 回答
6

这个也是:

// valid: char -> type
alignas(int) char c[sizeof(int)];
int * i = reinterpret_cast<int*>(c);

这是不正确的。别名规则规定了在哪些情况下通过不同类型的左值访问对象是合法/非法的。有一条特定的规则说您可以通过charor类型的指针访问任何对象unsigned char,因此第一种情况是正确的。也就是说,A => B 并不一定意味着 B => A。您可以int通过指向 的指针char访问 a,但char不能通过指向 的指针访问 a int


为了阿尔夫的利益:

如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 对象的动态类型的 cv 限定版本,
  • 与对象的动态类型类似(如 4.4 中定义)的类型,
  • 与对象的动态类型相对应的有符号或无符号类型,
  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,
  • 聚合或联合类型,在其元素或非静态数据成员中包括上述类型之一(递归地包括子聚合或包含联合的元素或非静态数据成员),
  • 一个类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,
  • char 或 unsigned char 类型。
于 2012-09-27T02:09:46.137 回答
2

关于……的有效性……</p>

alignas(int) char c[sizeof(int)];
int * i = reinterpret_cast<int*>(c);

reinterpret_cast产生有用的指针值的意义上,它本身是否可以,取决于编译器。在此示例中,未使用结果,特别是未访问字符数组。所以关于这个例子,没有什么可以说的了:它只是取决于.

但是让我们考虑一个确实涉及别名规则的扩展版本:

void foo( char* );

alignas(int) char c[sizeof( int )];

foo( c );
int* p = reinterpret_cast<int*>( c );
cout << *p << endl;

并且让我们只考虑编译器保证一个有用的指针值的情况,一个将指针放置在相同字节内存中的情况(这取决于编译器的原因是标准,在 §5.2.10/7 中,仅保证它用于类型对齐兼容的指针转换,否则将其保留为“未指定”(但是,整个 §5.2.10 与 §9.2/18 有点不一致)。

现在,对标准 §3.10/10 的一种解释,即所谓的“严格别名”条款(但请注意,该标准从未使用过“严格别名”一词),

如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 对象的动态类型的 cv 限定版本,
  • 与对象的动态类型类似(如 4.4 中定义)的类型,
  • 与对象的动态类型相对应的有符号或无符号类型,
  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,
  • 聚合或联合类型,在其元素或非静态数据成员中包括上述类型之一(递归地包括子聚合或包含联合的元素或非静态数据成员),
  • 一个类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,
  • 一个charunsigned char类型。

正如它本身所说,这与驻留在字节中的对象的动态类型有关。c

有了这种解释,如果在那里放置了一个对象,则读取操作*p是可以的,否则不是。所以在这种情况下,数组是通过指针访问的。并且没有人怀疑另一种方式是有效的:即使可能在这些字节中放置了一个对象,您也可以通过 §3.10/10 的最后一点自由地将该对象作为值序列访问。foointcharint*foointchar

因此,通过这种(通常的)解释,在foo放置了一个int之后,我们可以将它作为对象访问,因此在名为 的内存区域char中至少存在一个对象;我们可以访问它,所以至少那里也存在一个;所以大卫在另一个答案中断言对象不能被访问为, 与这种通常的解释不兼容。charcintintcharint

David 的断言也与最常见的placement new 用法不兼容。

关于还有什么其他可能的解释,也许可以与大卫的断言兼容,嗯,我想不出任何有意义的解释。

因此,总而言之,就神圣标准而言,仅将自己转换为T*指向数组的指针实际上是否有用取决于编译器,并且访问指向的可能值是否有效取决于存在的内容。特别是,考虑一个陷阱表示int:如果位模式恰好是那样,你不会希望它在你身上爆炸。所以为了安全起见,你必须知道里面有什么,位,正如foo上面的调用所示,编译器通常不知道,就像 g++ 编译器的严格的基于对齐的优化器一般不知道......</ p>

于 2012-09-27T05:46:55.137 回答