首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >通过不兼容的指针复制变量的位是否总是未定义的行为?

通过不兼容的指针复制变量的位是否总是未定义的行为?
EN

Stack Overflow用户
提问于 2022-04-03 10:14:45
回答 2查看 168关注 0票数 1

例如,这能

代码语言:javascript
复制
unsigned f(float x) {
    unsigned u = *(unsigned *)&x;
    return u;
}

在平台上造成不可预测的结果,

  • unsignedfloat都是32位。
  • 指针对于所有类型都有固定的大小。
  • unsignedfloat可以存储到内存的同一部分,并从内存的同一部分加载。

我知道严格的混叠规则,但大多数例子显示有问题的情况下违反严格混叠如下。

代码语言:javascript
复制
static int g(int *i, float *f) {
    *i = 1;
    *f = 0;
    return *i;
}

int h() {
    int n;
    return g(&n, (float *)&n);
}

据我理解,编译器可以自由地假定if是隐式restrict。如果编译器认为h是冗余的(因为if不能别名),那么*f = 0;的返回值可以是0,或者如果考虑到if的值是相同的,则返回值可以是0。这是一种未定义的行为,所以从技术上讲,任何事情都可能发生。

但是,第一个例子有点不同。

代码语言:javascript
复制
unsigned f(float x) {
    unsigned u = *(unsigned *)&x;
    return u;
}

对不起,我的措辞不清楚,但一切都是“就地”完成的。除了“将unsigned u = *(unsigned *)&x;的比特复制到u”之外,我想不出编译器可能解释行u的任何其他方法。

在实践中,我在https://godbolt.org/中测试过的所有编译器都为第一个示例生成了相同的结果,而对于第二个示例则产生了不同的结果( 01)。

我知道从技术上讲,unsignedfloat有不同的大小和对齐需求,或者应该存储在不同的内存段中。在这种情况下,即使是第一段代码也没有意义。但是,在大多数现代平台上,是否第一个例子仍未定义(它是否会产生不可预测的结果)?

  • unsignedfloat都是32位。
  • 指针对于所有类型都有固定的大小。
  • unsignedfloat可以存储到内存的同一部分,并从内存的同一部分加载。

在实际代码中,我确实写了

代码语言:javascript
复制
unsigned f(float x) {
    unsigned u;
    memcpy(&u, &x, sizeof(x));
    return u;
}

经过优化后,编译的结果与使用指针转换相同。这个问题是关于对代码的严格混叠规则的标准的解释,例如第一个例子。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2022-04-03 12:11:05

通过不兼容的指针复制变量的位是否总是未定义的行为?

是。

规则是https://port70.net/~nsz/c/c11/n1570.html#6.5p7

对象的存储值只能由具有下列类型之一的lvalue表达式访问:

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 与对象的有效类型对应的有符号或无符号类型的类型,
  • 对象的有效类型的限定版本对应的有符号或无符号类型的类型,
  • 在其成员中包含上述类型之一的聚合或联合类型(递归地包括次聚合或包含的联合的成员),或
  • 字符类型。

x对象的有效类型是float --它是用该类型定义的。

  • unsignedfloat不兼容,
  • unsigned不是float的合格版本,
  • unsigned不是有符号或无符号类型的float
  • unsigned不是与float的合格版本相对应的有符号或无符号类型,
  • unsigned不是聚合或联合类型。
  • unsigned不是字符类型。

“应”被违反,它是未定义的行为(请参阅https://port70.net/~nsz/c/c11/n1570.html#4p2 )。没有其他解释。

我们还有https://port70.net/~nsz/c/c11/n1570.html#J.2

在下列情况下未对行为进行定义:

  • 对象的存储值被允许类型(6.5)的lvalue以外的值访问。
票数 3
EN

Stack Overflow用户

发布于 2022-04-04 15:52:37

标准从未打算完全、准确和明确地划分那些已经定义了行为的程序和那些没有定义(*)的程序,而是依赖于编译器编写人员来行使一定的常识。

(*)如本条例草案是为该目的而拟备的,则该条例草案的失败是可悲的,这点可从由此产生的混乱程度证明。

考虑以下两个代码片段:

代码语言:javascript
复制
/* Assume suitable declarations of u are available everywhere */
union test { uint32_t ww[4]; float ff[4]; } u;

/* Snippet #1 */
uint32_t proc1(int i, int j)
{
  u.ww[i] = 1;
  u.ff[j] = 2.0f;
  return u.ww[i];
}

/* Snippet #2, part 1, in one compilation unit */
uint32_t proc2a(uint32_t *p1, float *p2)
{
  *p1 = 1;
  *p2 = 2.0f;
  return *p1;
}

/* Snippet #2, part 2, in another compilation unit */
uint32_t proc2(int i, int j)
{
  return proc2a(u.ww+i, u.ff+j);
}

很明显,标准的作者打算在有意义的平台上对代码的第一个版本进行有意义的处理,但同样清楚的是,至少一些C99和以后版本的作者并不打算要求对第二个版本进行同样的处理( C89的一些作者可能曾打算,“严格的混叠规则”只适用于这样的情况:直接命名的对象将通过另一种类型的指针进行访问,如所公布的理由中的示例所示;基本原理中没有任何表示希望更广泛地应用它)。

另一方面,标准定义[]运算符的方式使proc1在语义上等价于:

代码语言:javascript
复制
uint32_t proc3(int i, int j)
{
  *(u.ww+i) = 1;
  *(u.ff+j) = 2.0f;
  return *(u.ww+i);
}

而且,标准中没有任何东西可以暗示proc()不应该具有相同的语义。gcc和clang所做的似乎是特例,[]操作符的含义不同于指针取消引用,但标准中没有任何东西能做出这样的区分。一致解释标准的唯一方法是认识到,带有[]的表单属于标准不要求实现有意义地处理的操作类别,但无论如何都要依赖它们来处理。

例如,使用直接转换指针访问与原始指针类型的对象关联的存储的构造属于类似的构造类别,至少有一些标准的作者希望(而且如果他们没有预料到)编译器能够可靠地处理,不管是否有授权,因为没有任何可以想象的理由,高质量的编译器也会这样做。然而,从那时起,clang和gcc就开始挑战这种期望。即使clang和gcc通常会为一个函数生成有用的机器代码,他们也试图执行激进的过程间优化,从而无法预测什么构造是100%可靠的。不像一些编译器避免应用潜在的优化转换,除非它们能够证明它们是健全的,clang和gcc试图执行无法被证明会影响程序行为的转换。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71724929

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档