允许使用char*对T*进行别名化.是否也允许反过来?

Sta*_*ked 27 c++ strict-aliasing language-lawyer type-punning

注意:此问题已重命名并缩小,以使其更具针对性和可读性.大多数评论都涉及旧文本.


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

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

但是,该标准允许此规则的例外:可以通过指向char或的指针访问任何对象unsigned char:

int i = 0;
char * c = reinterpret_cast<char*>(&i); // OK
Run Code Online (Sandbox Code Playgroud)

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

char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?
Run Code Online (Sandbox Code Playgroud)

Luc*_*ton 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如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

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

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

根据这些规则,让我们假设一个实现为我们提供magic_cast<T*>(p)了"以某种方式"将指针转换为另一个指针类型.通常这reinterpret_cast,在某些情况下产生未指定的结果,但正如我之前所解释的那样,对于标准布局类型的指针并非如此.然后,它显然是真的,所有的片段都是正确的(替换reinterpret_castmagic_cast),因为没有glvalues与结果任何参与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;
Run Code Online (Sandbox Code Playgroud)

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

// 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;
Run Code Online (Sandbox Code Playgroud)

这个片段是精心构造的.特别是,在new (&c) int;我被允许使用&c,即使c被摧毁,由于在3.8对象生存[basic.life]第5款中规定的规则.它的第6段提供了与存储引用非常相似的规则,第7段解释了一旦存储被重用后用于引用对象的变量,指针和引用会发生什么 - 我将统称为3.8/5- 7.

在这个实例&c中(隐式)转换为void*,这是正确使用尚未重用的存储指针之一.类似地p从构造&c新的之前获得int.c根据实施魔法的深度,它的定义可能会被移动到,但肯定不会在int构造之后:第7段将适用,这不是允许的情况之一.short对象的构造还依赖于p成为存储的指针.

现在,因为int并且short是微不足道的类型,我不必使用对析构函数的显式调用.我也不需要显式调用构造函数(也就是说,调用通常的标准放置新的声明<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;
Run Code Online (Sandbox Code Playgroud)

如果我们假设一个int对象(通常)用第二行构造,那么这必然意味着&c成为一个指向已经重用的存储的指针.因此第三行是不正确的 - 尽管由于3.8/5-7并且严格来说不是由于混叠规则.

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

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

读取对象的字节(这意味着使用char或者unsigned char)很好,但你甚至根本不使用reinterpret_cast任何魔法.static_castvia cv void*对于这项工作来说可以说是好的(虽然我觉得标准可以在那里使用更好的措辞).

  • @AlexandreHamez注意`void`是不完整的.所以你可以拥有一个`void*`来保存与另一个指针相同的地址(别名的含义之一),但它没关系,因为你无论如何也无法读取它 - 而且所谓的C++别名规则与读取有关.事实上,以任何形式传递地址值都不能自己违反别名规则(但还有很多其他规则......).这对你有意义吗?(如果你觉得某些部分不够清楚,我会改进这个答案.) (3认同)

Dav*_*eas 6

这个也是:

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

这是不正确的.别名规则声明在哪种情况下通过不同类型的左值访问对象是合法/非法的.还有的是说你可以通过类型的指针可以访问任何对象的特定规则char或者unsigned char,所以第一种情况下是正确的.也就是说,A => B并不一定意味着B => A.您可以int通过指针访问char,但是您无法char通过指针访问int.


为了Alf的利益:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

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

  • @ Cheersandhth.-Alf:你自己添加了*[array]*,我没有.如果你想让我完全拼写出来:*你无法通过指向int*的指针访问char**对象**关键是在代码中,char数组包含`char`对象,你无法访问**他们**(对象)通过指向`int`的指针.如果代码中有一个新的位置,那么`int`将根据生命周期规则结束`char`对象的生命周期,因为内存被重用于另一个对象,此时你将访问一个`int`对象存储在`char`数组中. (5认同)
  • @ Cheersandhth.-Alf:placement new将缓冲区(void*)传递给allocator函数.然后,allocator函数使用该缓冲区来*创建一个对象.注意,根据生命周期规则,在那个时间点缓冲区中的`char`对象终止它们的生命周期.这不是**别名.`new(c)int(5)`在缓冲区中创建一个值为5的整数.缓冲区现在是一个`int`.`reinterpret_cast <int*>(c)= 5`将缓冲区中的字符别名,就像它们是`int`一样.除非你声称`reinterpret_cast <int*>(c)`和`new(c)int`是等价的...... (4认同)
  • @ Cheersandhth.-Alf:我从未声称,别名规则(再次,花时间阅读§3.10/ 10)不处理*static*,而是处理对象的*dynamic*类型.再说一遍,如果我错了,请纠正我 - 引用标准,而不仅仅是意见. (3认同)
  • @ildjarn:您可以通过指向char的指针访问int,但不能通过指向int的指针访问char.在上面的代码中,内存中的对象是`char`,而`reinterpret_cast <int*>`创建了一个类型为`int*`的别名.你不能使用`int*`来访问(或多个)`char`对象. (3认同)
  • @ Cheersandhth.-Alf:再次,你的意见与标准的措辞.除非我误解了它们,在这种情况下,请告诉我标准的意图,提供与我所说的相矛盾的引用.如果错误,我将是第一个删除答案的人.顺便说一句,我发现你的最后评论令人反感.如果你有事实告诉,如果不是至少避免粗鲁.上述标准引用中的内容不清楚?(这将是一个有建设性的事情,而不是拖钓) (3认同)
  • @ Cheersandhth.-Alf:你一定误解了我所说的话,因为从最后一条评论的答案来看,没有任何矛盾.但是,不是试图争论你的立场,而是在寻找借口.我还没有在你身边发现任何争论,只是找借口:*我可能无法帮助你解决这个问题*.简单的问题(第三次):*上面的引用是否确定问题中的代码是正确的还是不正确的?*这是一个简单的是/否,如果您考虑它,添加列表中的哪个条目支持您的声明有效代码. (3认同)