【问题标题】:Is the strict aliasing rule really a "two-way street"?严格的别名规则真的是“双向街道”吗?
【发布时间】:2014-08-27 05:05:16
【问题描述】:

these comments 用户@Deduplicator 坚持如果别名或别名指针中的 是指向字符的指针类型(合格或不合格),则严格别名规则允许通过不兼容的类型进行访问, 签名或未签名 char *)。所以,他的断言基本上是两者兼而有之

long long foo;
char *p = (char *)&foo;
*p; // just in order to dereference 'p'

char foo[sizeof(long long)];
long long *p = (long long *)&foo[0];
*p; // just in order to dereference 'p'

符合规定并具有明确的行为。

然而,在我的阅读中,只有第一种形式是有效的,也就是说,当 aliasing 指针是指向字符的指针时;但是,一个人不能在另一个方向上做到这一点,即。 e.当别名指针指向不兼容的类型(字符类型除外)时,aliased 指针为char *

因此,上面的第二个 sn-p 将具有未定义的行为。

这是怎么回事?它是否正确?作为记录,我已经阅读了this question and answer,并且接受的答案明确指出

规则允许char * 例外。始终假定char * 为其他类型起别名。 但是这不会以另一种方式工作,没有假设您的结构别名为字符缓冲区。

(强调我的)

【问题讨论】:

  • 确实,标准只允许第一个版本。
  • @OliCharlesworth 好吧,我也这么认为。告诉 Deduplicator,他(相当傲慢地)断言“我需要学习更好地阅读标准”...... >.
  • FWIW 相关的 C99 标准条款是 6.5 p7。
  • @Deduplicator:如果你的意思是char buf[...]; fread(buf, ...); foo(((MyStruct *)buf)->member);,它不起作用。
  • @Deduplicator:对齐是另一回事。请参阅我上面引用的标准报价,了解为什么这对于别名 POV 无效。

标签: c pointers strict-aliasing


【解决方案1】:

您说这是无效的是正确的。正如你自己引用的那样(所以我不会在这里重新引用)保证的有效转换仅从任何其他类型到 char*。

另一种形式确实违反标准并导致未定义的行为。然而,作为一个小小的奖励,让我们讨论一下这个标准的背后。

字符,在每个重要的体系结构上,是唯一允许完全不对齐访问的类型,这是因为读取字节指令必须在任何字节上工作,否则它们几乎毫无用处。这意味着对 char 的间接读取在我所知道的每个 CPU 上总是有效的。

但是,相反的方法将不适用,除非指针在大多数拱门上与 8 字节对齐,否则您无法读取 uint64_t。

但是,有一个非常常见的编译器扩展允许您将正确对齐的指针从 char 转换为其他类型并访问它们,但这是非标准的。另请注意,如果您将指向任何类型的指针转​​换为指向 char 的指针,然后将其转换回结果指针,则保证等于原始对象。因此这没关系:

struct x *mystruct = MakeAMyStruct();
char * foo = (char *)mystruct;
struct x *mystruct2 = (struct mystruct *)foo;

mystruct2 将等于 mystruct。这也保证了结构正确对齐以满足其需求。

所以基本上,如果你想要一个指向 char 的指针和一个指向另一种类型的指针,请始终声明指向另一种类型的指针,然后强制转换为 char。或者甚至更好地使用联合,这就是它们的基本用途......

请注意,该规则有一个明显的例外。 malloc 的一些旧实现用于返回 char*。始终保证此指针可以成功转换为任何类型而不会破坏别名规则。

【讨论】:

  • 任何指针转换都没有别名问题;可能仅在取消引用强制转换的结果然后使用该表达式访问内存时才会出现。并且行为取决于内存的有效类型,以及解引用表达式的类型;它完全独立于可能存在或已经存在的任何其他指针的类型。所以你最后一段中的情况实际上不需要例外。
【解决方案2】:

重复数据删除器是正确的。当字符值用于生成对象的表示时,允许编译器实现“严格别名”优化的未定义行为不适用。

某些对象表示不需要表示对象类型的值。如果 对象的存储值具有这样的表示,并由左值表达式读取 没有字符类型,行为未定义。如果这样的表示 由通过左值修改对象的全部或任何部分的副作用产生 没有字符类型的表达式,则行为未定义。这样一个 表示称为陷阱表示。

但是,您的第二个示例具有未定义的行为,因为 foo 未初始化。如果您初始化foo,那么它只有实现定义的行为。这取决于long long 的实现定义的对齐要求以及long long 是否有任何实现定义的填充位。

考虑是否将第二个示例更改为:

long long bar() {
    char *foo = malloc(sizeof(long long));
    char c;
    for(c = 0; c < sizeof(long long); c++)
        foo[c] = c;
    long long *p = (long long *) p;
    return *p;
}

现在对齐不再是问题,此示例仅依赖于long long 的实现定义表示。返回的值取决于long long 的表示,但如果该表示被定义为没有填充位,则此函数必须始终返回相同的值,并且它也必须始终是有效值。如果没有填充位,此函数无法生成陷阱表示,因此编译器无法对其执行任何严格的别名类型优化。

您必须非常努力地找到符合标准的 C 实现,该实现在其任何整数类型中都具有实现定义的填充位。我怀疑你会找到一个实现任何类型的严格别名类型的优化。换句话说,编译器不会使用访问陷阱表示导致的未定义行为来允许严格混叠优化,因为没有实现严格混叠优化的编译器定义任何陷阱表示。

还要注意,如果 buf 被初始化为全零('\0' 字符),那么这个函数将不会有任何未定义或实现定义的行为。整数类型的全位零表示保证不是陷阱表示,并保证其值为 0。

现在来看一个使用char 值来创建long long 值的保证有效(可能非零)表示的严格符合示例:

#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char **argv) {
    int i;
    long long l;
    char *buf;

    if (argc < 2) {
        return 1;
    }
    buf = malloc(sizeof l);
    if (buf == NULL) {
        return 1;
    }
    l = strtoll(argv[1], NULL, 10);
    for (i = 0; i < sizeof l; i++) {
        buf[i] = ((char *) &l)[i];
    }
    printf("%lld\n", *(long long *)buf);
    return 0;
}

此示例没有未定义的行为,并且不依赖于long long 的对齐或表示。这是为访问对象创建字符类型异常的那种代码。这尤其意味着标准 C 允许您在可移植的 C 代码中实现自己的 memcpy 函数。

【讨论】:

  • "重复数据删除器是正确的。" - 抱歉,不,他不是 - 在原始(现在显然已删除)cmets 中,他断言像 char c[sizeof(long]; 这样的数组可以通过 long 类型的左值重新解释和别名。由于那里没有long 类型的对象,因此行为未定义。关于陷阱表示的引用没有说明这个问题。这是关于描述另一类未定义行为的略有不同的问题。
  • 我删除了我之前的评论,因为它是基于对标准的错误解读。你是对的,编译器可以假设取消引用long 指针不能为数组c 别名,因为它知道c 的有效类型。但它通常不能假设指向long 的指针和指向char 的指针不能指向同一个对象,因为它通常不知道也不能假设对象char 的类型的有效点到。因此,当char 对象没有声明的类型时,存在两条路,而在使用指针时,编译器无法追溯到定义。
  • 你的第一段听起来好像你在说如果foo被初始化了OP的第二个sn-p就不会是UB(实际上它会是UB);你能澄清一下吗?
  • 与您的代码示例相关,仍然可能存在没有填充位的陷阱表示(具体示例 - 1 的补码上的负零或不支持负零的符号幅度实现)
猜你喜欢
  • 2015-01-16
  • 2021-12-22
  • 2015-10-15
  • 2017-02-25
  • 2016-09-12
  • 2018-12-14
  • 2015-05-31
  • 1970-01-01
相关资源
最近更新 更多