我们能在联合体中使用va_arg吗?

8

我草拟的C99标准的第6.7.2.1段第14句有关共用体和指针的规定如下(重点始终加粗):

一个共用体的大小足以容纳其最大成员。最多只能在共用体对象中存储一个成员的值。合适转换的共用体对象指针指向其每个成员(如果成员是位域,则指向其所在单位),反之亦然。

这意味着做类似以下操作是合法的,可以将有符号或无符号int复制到共用体中,假设我们只想将它复制到相同类型的数据中:

union ints { int i; unsigned u; };

int i = 4;
union ints is = *(union ints *)&i;
int j = is.i; // legal
unsigned k = is.u; // not so much

7.15.1.1第2段有这样的描述:
va_arg宏扩展成一个表达式,该表达式具有指定类型和调用中下一个参数的值。参数ap必须已由va_start或va_copy宏(没有在同一ap上调用va_end宏)初始化。每次调用va_arg宏都会修改ap,以便按顺序返回连续参数的值。参数type应是指定的类型名称,以便可以通过将*后缀到type来获取指向具有指定类型的对象的指针的类型。如果没有实际的下一个参数,或者 type与实际下一个参数的类型(根据默认参数提升进行升级)不兼容,则行为是未定义的,但以下情况除外:
- 一个类型是有符号整数类型,另一个类型是相应的无符号整数类型,并且该值可以在两种类型中表示; - 一个类型是指向void的指针,另一个类型是指向字符类型的指针。
我不打算引用默认参数提升的部分。我的问题是:这是定义好的行为吗?
void func(int i, ...)
{
    va_list arg;
    va_start(arg, i);
    union ints is = va_arg(arg, union ints);
    va_end(arg);
}

int main(void)
{
    func(0, 1);
    return 0;
}

如果是这样的话,似乎可以通过一种相当困难且法律上无法做任何处理的方式来克服有符号/无符号整数转换的“值与两种类型兼容”的要求。如果不是这种情况,看起来在这种情况下只使用 unsigned 是安全的,但如果联合体中有更多元素具有不兼容的类型怎么办呢?如果我们可以保证不通过元素访问联合体(即我们只将其复制到另一个 union 或我们视为 union 的存储空间中),并且联合体的所有元素都具有相同的大小,那么varargs是否允许这样做?还是只允许使用指针?
实际上,我认为这段代码几乎永远不会失败,但我想知道它是否被定义为行为。我目前的猜测是它似乎没有被定义,但这似乎非常愚蠢。

1
现在我想想,我更关心的是 func(0, -1); func(0, UINT_MAX); 是否合法。func(0, 1) 可能仅仅因为 1 可以适应于 intunsigned 而合法。 - Chris Lutz
unsigned k = is.u; 在 C99 中是合法的。 - Dietrich Epp
@ Dietrich - 由于有符号/无符号的事情,是否存在异常情况? - Chris Lutz
这些调用都没有传递联合体。你可能需要一个复合字面量:func(1, (union ints) { .i = 0 }); func(2, (union ints) { .u = UINT_MAX });等等。或者使用联合变量。 - Jonathan Leffler
@Chris Lutz:正如 Pascal Cuoq 指出的那样,通过联合在 C99 TC3 中变得合法,可以进行任意类型的游走。生成的值是未定义的。 - Dietrich Epp
3个回答

3

您有几个地方出现了错误。

指向联合体对象的指针,经过适当转换后,指向其每个成员(或者如果成员是位域,则指向其所在单位),反之亦然。

这并不意味着类型是兼容的。实际上,它们是不兼容的。因此,以下代码是错误的:

func(0, 1); // undefined behavior

如果您想要传递一个联合体,请使用以下方法:
func(0, (union ints){ .u = BLAH });

你可以通过编写代码来进行检查,

union ints x;
x = 1;

在编译时,GCC会给出“错误:赋值时类型不兼容”的提示。

然而,大多数实现在这两种情况下“可能”都会做正确的事情。还有一些其他问题...

union ints {
    int i;
    unsigned u;
};

int i = 4;
union ints is = *(union ints *)&i; // Invalid
int j = is.i; // legal
unsigned k = is.u; // also legal (see note)

当您使用类型而非实际类型解除引用地址 *(union ints *)&i 时,行为有时是未定义的(查找参考资料,但我对此相当确定)。但在 C99 中允许访问联合成员而不是最近存储的联合成员(或者是 C1x 吗?),但这个值是实现定义的,可能是一个捕获表示。
关于通过联合进行类型判定:正如 Pascal Cuoq 所指出的那样,实际上是 TC3 定义了访问联合元素而非最近存储元素的行为。TC3 是针对 C99 的第三次更新。好消息是,TC3 的这部分实际上是对先前 C 的实践进行规范化,因此可以将其视为 TC3 之前 C 的一部分。

3
TC3明确规定C99中可以使用联合体进行类型转换,这是一个关于Defect Report 283的链接:http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm。 - Pascal Cuoq

2

标准规定:

参数类型应该是指定的类型名称,以便通过在类型后缀处添加 * 来获取指向具有指定类型的对象的指针的类型。

对于联合体 union ints,该条件得到了满足。因为 union ints * 是指向 union ints 的指针的完全良好的表示形式,所以在那个句子中没有任何阻止其被用于收集作为 union 推送到堆栈上的值的内容。

如果你作弊并尝试使用普通的 intunsigned int 代替联合体,则会引发未定义的行为。 因此,你可以使用以下内容:

union ints u1 = ...;

func(0, (union ints) { .i = 0 });
func(1, (union ints) { .u = UINT_MAX });
func(2, u1);

您不能使用:

func(1, 0);

这些参数不是联合类型。


0

我不明白为什么你认为代码在实践中永远不会失败。在任何整数类型通过寄存器传递但聚合类型(即使很小)通过堆栈传递的实现中,它都会失败,而且我在标准中看不到禁止这样的实现。包含一个 int 的联合体与 int 不兼容,即使它们的大小相同。

回到你的第一个代码片段,它也有问题:

union ints is = *(union ints *)&i;

这是一种别名违规行为,会引发未定义的行为。您可以通过使用memcpy来避免它,我想那样就合法了。

我对您在此处的评论也有些困惑:

unsigned k = is.u; // not so much

由于值4在有符号类型和无符号类型中都有表示,所以这应该是合法的,除非它作为一个特殊情况被明确禁止。

如果这个回答没有解决你的问题,也许你可以详细说明一下你试图解决的(尽管是理论上的)问题。


你首先通过 int 类型的 lvalue 访问对象,然后通过 union ints 类型之一访问。编译器可能会认为它们不彼此别名。 - R.. GitHub STOP HELPING ICE
编译器是否可以假定在传递给省略号(...)的参数上使用restrict关键字? - Jonathan Leffler
编译器如何假设变长参数是根据它们的类型在不同位置传递的? - Chris Lutz
@wnoise:我认为这可能不是你所想的意思。如果是这样,通过指向union { int i; char b[HUGE]; }的指针访问int以复制整个联合类型将是有效的,即使它显然会读取对象的末尾之后的内容... - R.. GitHub STOP HELPING ICE
@Chris:我有一个消息告诉你,它确实可以在基本上任何架构上运行,但不包括x86。例如,在x86_64上,整数和指针通过通用寄存器传递,浮点数和双精度浮点数则通过sse寄存器传递,如果结构体适合,则分割在寄存器之间,否则放在堆栈上。编译器为支持此功能所生成的va_start/va_arg的膨胀量令人震惊,基本上设计ABI的人只是为了让int main() { printf("hello, world\n"); }(不包括stdio.h)能够工作... - R.. GitHub STOP HELPING ICE
显示剩余5条评论

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