指向可变类型修饰符的指针运算

4
以下的C代码是否有效?(godbolt)
#include <stddef.h>

ptrdiff_t f(size_t n, void *x, void *y)
{
    if (!n) return 0;
    typedef unsigned char element[n];
    element *a = x, *b = y;
    return a - b;
}

使用-Werror=pointer-arith,clang会大声抱怨

<source>:8:14: error: subtraction of pointers to type 'element' (aka 'unsigned char [n]') of zero size has undefined behavior [-Werror,-Wpointer-arith]
    return a - b;
           ~ ^ ~

当gcc编译代码时没有任何投诉。

clang认为发生了未定义的行为是什么?减法可能为零,因此不是数组元素的有效指针或其他情况?没有进行数组访问,对吧?那就不应该是这种情况...

如果代码确实表现出未定义的行为,是否有一种简单的方法可以修改代码以完全符合规范,同时仍然使用指向VM类型的指针?


3
clang 在处理可变长度数组方面似乎存在一个错误。typedef 不是问题所在。将 ab 声明为 unsigned char (*a)[n] = x, (*b)[n] = y; 会得到相同的结果。如果忽略警告(即删除 -Werror 标志),该程序可以生成有效的输出。 - user3386109
2
这可以通过 int f(void *x) { int n = 1; return (char (*)[n]) x - (char (*)[n]) x; } 来复现。这似乎是Clang中的一个错误。有趣的是,(char (*)[n]) x + 1 不会产生错误,因此Clang并不普遍认为 char (*)[n] 的大小为零。这支持了这个猜想是一个bug,因为任何关于 char (*)[n] 指针算术的C规则应该同样适用于添加指针和整数以及减去两个指针。 - Eric Postpischil
1
@user3386109:这里的OP和大多数评论者都不需要这样的指导;他们对指针算术非常熟悉。即使他们不熟悉,指导他们进行这种指针算术也无法达到目标,因为目标是确定在这个特定表达式中是否存在C语义问题或Clang是否存在某些错误。这就是问题所在:C语义对此有何规定,以及为什么Clang会以这种方式行事。问题不在于如何进行指针算术。请注意“语言律师”标签:主题是正式的C语义。 - Eric Postpischil
2
Clang -Werror=pointer-arith 似乎也无法通过标准中6.5.6/10的示例(“指针算术在指向可变长度数组类型的指针上是定义良好的。”),因此我不明白它怎么可能不是一个错误。 - rici
1
qsort示例的解决方案是从分区函数中返回j。递归调用变为qsort(base,j+1,bytes,cmp)qsort(base+j+1,num-1-j,bytes,cmp)。这消除了麻烦的指针减法。它可能也具有更好的性能,因为指针减法具有隐式除法,并且我认为在减去VLA指针时无法优化该除法。 - user3386109
显示剩余18条评论
1个回答

2
如果您的编译器支持VLA,那么发布的代码是有效的C代码。请注意,C99中引入的VLA在最新版本的C标准中已成为可选项。 gccclang都可以正确编译该代码,可以使用 Godbolt's compiler explorer进行验证。
然而,clang会发出有关潜在未定义行为的警告,如果n参数的值恰好为null,则无法识别已使用显式测试处理此情况。问题不在于减法的值,而在于类型的大小,如果n == 0,则大小将为0。这个警告并不是一个真正的错误,更多的是实现质量问题。
也有争议认为,只有当ab指向同一个数组或者刚好是其最后一个元素的下一个位置时,a - b才有定义。因此,xy必须满足这个限制,并且具有类型unsigned char (*)[n]或兼容的类型。对于将任何类型作为字符类型数组访问,有一个例外规则,因此传递指向相同int数组的指针是可以的,但以这种方式调用f是不正确的(尽管可能是无害的)。
int x, y;
ptrdiff_t dist = f(sizeof(int), &x, &y);

编译器可以发布诊断信息,吸引程序员对潜在问题的注意,实际上这些警告对于初学者和高级程序员来说都是救命稻草。编译器选项如-Wall-Werror-Weverything非常有用,但在这种特殊情况下,如果-Werror也处于活动状态,则需要添加-Wno-pointer-arith才能让clang编译此函数。
另请注意,使用C89函数也可以获得相同的结果:
ptrdiff_t f89(size_t n, void *x, void *y)
{
    if (!n) return 0;
    unsigned char *a = x, *b = y;
    return (a - b) / n;
}

为什么以那种方式调用 f 是无效的?难道 C99 中的 6.2.6/4 没有使得将任何对象类型 type 解释为大小为 sizeof(type)unsigned char 数组(即底层对象表示)成为有效操作吗?如果您查看问题的编辑历史记录,您会看到我对“元素”指针的使用意图:在具有类似于 qsortbsearch 的函数契约的函数内部。 - nebel
1
“it should be fine”是著名的最后一句话,考虑到我们正在讨论C语言。这绝不是权威性的答案...任何对标准的参考都将不胜感激。 - nebel
@nebel,是的,我确实会今晚修改答案。 - chqrlie
注意:int *unsigned char (*)[sizeof(int)] 是不兼容的指针。最好在赋值时将函数的参数明确转换为 element *,即使 f 在自己的 TU 中 (int a; unsigned char (*b)[sizeof a] = &a; 是无效的代码)。 - nebel
我仍然不太清楚是否允许根据C的类型系统别名规则进行显式转换。从我所了解的6.5 / 7(C99)中可以看出,只有通过该指针访问会产生UB,而不是仅构造一个指针进行指针算术运算。因此,6.2.6 / 4可能只对指针算术规则6.5.6 / 9有影响,即“两者都应指向同一数组对象的元素”。 - nebel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接