给数据一个有效的类型算作副作用吗?

Does giving data an effective type count as a side-effect?

假设我有一大块动态分配的数据:

void* allocate (size_t n)
{
  void* foo = malloc(n);
  ...
  return foo;
}

我希望将foo指向的数据用作特殊类型,type_t。但我想稍后再做,而不是在分配期间。为了给分配的数据一个有效类型,我可以这样做:

void* allocate (size_t n)
{
  void* foo = malloc(n);
  (void) *(type_t*)foo;
  ...
  return foo
}

根据 C11 6.5/6,此左值访问应使有效类型 type_t:

For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access.

但是,(void) *(type_t*)foo; 行没有副作用,因此编译器应该可以自由优化它,我不希望它生成任何实际的机器代码。

我的问题是:像上面这样的技巧安全吗?给数据一个有效的类型算作副作用吗?或者通过优化代码,编译器是否也会优化有效类型的选择?

也就是说,有了上面的左值访问技巧,如果我现在这样调用上面的函数:

int* i = allocate(sizeof(int));
*i = something;

这是否会像预期的那样导致严格的别名违规 UB,或者现在是有效类型 int

您引用的标准中的短语清楚地仅说明了有关对象的访问的内容。标准描述的 object 的有效类型的唯一变化是之前的两个短语,清楚地描述了你必须将你想要创建的类型存储到对象中有效。

6.5/6

If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value.

标准中没有任何内容建议写入对象的操作只需要在操作具有其他副作用(例如更改位模式)的情况下被识别为设置有效类型存储在该对象中)。另一方面,使用积极的基于类型的优化的编译器似乎无法将对象的有效类型的可能更改识别为副作用,即使写入没有其他可观察到的副作用也必须维护。

要理解 Effective Type 规则的实际含义,我认为有必要了解它的来源。据我所知,它似乎源自缺陷报告#028,更具体地说,是用来证明其中给出的结论的理由。给出的结论是合理的,但给出的理由是荒谬的。

本质上,基本前提涉及以下可能性:

void actOnTwoThings(T1 *p1, T2 *p2)
{
  ... code that uses p1 and p2
}
...
...in some other function
  union {T1 v1; T2 v2; } u;
  actOnTwoThings(&u.v1, &u.v2);

因为将联合作为一种类型编写并作为另一种类型读取会产生实现定义的行为,因此通过指针写入一个联合成员并读取另一个联合成员的行为并未完全由标准定义,因此应该 (根据 DR #028 的逻辑)被视为未定义行为。尽管在上述许多场景中使用 p1 和 p2 访问相同的存储实际上应该被视为 UB,但基本原理是完全错误的。指定一个动作产生实现定义的行为与说它产生未定义的行为有很大不同,尤其是在标准对实现定义的行为可能施加限制的情况下。

从联合的行为中导出指针类型规则的一个关键结果是,如果代码使用任何成员在任何情况下多次编写联合,则行为是完全明确定义的,没有实现定义的方面序列,然后读取最后写入的成员。虽然要求实现允许这样做会阻止一些其他有用的优化,但很明显有效类型规则是为要求这种行为而编写的。

基于联合行为的类型规则所产生的一个更大的问题是,如果新的位模式匹配旧的。由于实现必须将新的位模式定义为表示作为新类型写入的值,因此它还必须将(相同的)旧位模式定义为表示相同的值。给定函数(假设 'long' 和 'long long' 是同一类型):

 long test(long *p1, long long *p2, void *p3)
 {
   if (*p1)
   {
     long long temp;
     *p2 = 1;
     temp = *(long long*)p3;
     *(long*)p3 = temp;
   }
   return *p1;
 }

gcc 和 clang 都将决定通过 *(long*)p3 的写入不会有任何影响,因为它只是存储回通过 *(long long*)p3 读取的相同位模式,这是真的如果在通过 *p2 写入存储的情况下,将在实现定义的行为中处理以下对 *p1 的读取,但如果这种情况被视为 UB,则不成立。不幸的是,由于标准对于行为是实现定义的还是未定义的是不一致的,因此对于写入是否需要被视为副作用也是不一致的。

从实用的角度来看,当不使用-fno-strict-aliasing时,gcc和clang应该被视为处理C的一种方言,其中Effective Types一旦设置,就会成为永久性的。他们无法可靠地识别所有可能更改有效类型的情况,并且处理所需的逻辑可以轻松有效地处理 gcc 的作者长期以来声称如果不进行彻底优化就无法处理的许多情况。