【问题标题】:Can we use va_arg with unions?我们可以在联合中使用 va_arg 吗?
【发布时间】:2011-09-14 04:25:56
【问题描述】:

我的 C99 标准草案的 6.7.2.1 第 14 段对联合和指针有这样的说法(强调,一如既往,添加):

联合的规模足以容纳其最大的成员。的值 大多数成员可以随时存储在联合对象中。 指向 a 的指针 union 对象,经过适当转换,指向它的每个成员(或者如果一个成员是位 字段,然后到它所在的单元),反之亦然。

一切都好,这意味着执行以下操作将有符号或无符号 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_startva_copy 宏(没有对相同ap 的 va_end 宏的介入调用)。每次调用va_arg 宏都会修改ap,以便依次返回连续参数的值。参数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 或我们将其视为union 的存储空间)并且联合的所有元素的大小相同,这是否允许使用可变参数?还是只允许使用指针?

在实践中,我希望这段代码几乎永远不会失败,但我想知道它是否是已定义的行为。我目前的猜测是它似乎没有被定义,但这似乎非常愚蠢。

【问题讨论】:

  • 现在想想,我想我更感兴趣的是func(0, -1); func(0, UINT_MAX); 是否合法。 func(0, 1) 可能是合法的,仅仅是因为 1 适合 intunsigned
  • unsigned k = is.u; 在 C99 中是合法的。
  • @Dietrich - 由于已签名/未签名的事情是否存在异常?
  • 这些调用都没有通过联合。您可能需要一个复合文字:func(1, (union ints) { .i = 0 }); func(2, (union ints) { .u = UINT_MAX }); 等。或者联合变量。
  • @Chris Lutz:正如 Pascal Cuoq 所指出的,在 C99 TC3 中通过联合进行任意类型双关语是合法的。结果值未定义。

标签: c variadic-functions language-lawyer unions


【解决方案1】:

你有几件事要解决。

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

这并不意味着这些类型是兼容的。事实上,它们并不兼容。所以下面的代码是错误的:

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)

当您使用不同于其实际类型*(uinon ints *)&i 的类型取消引用类型的地址时的行为有时是未定义的(查找引用,但我对此非常确定)。但是,在 C99 中,允许访问除最近存储的联合成员之外的联合成员(或者是 C1x?),但该值是实现定义的,并且可能是陷阱表示。

关于通过联合的类型双关语: 正如 Pascal Cuoq 所指出的,实际上是 TC3 定义了访问联合元素而不是最近存储的元素的行为。 TC3 是 C99 的第三次更新。好消息是 TC3 的这一部分实际上是在编纂现有的实践——因此可以将其视为 TC3 之前 C 的事实上的一部分。

【讨论】:

【解决方案2】:

既然标准说:

参数类型应该是一个指定的类型名称,这样指向具有指定类型的对象的指针的类型可以简单地通过在类型后面加上 * 来获得。

对于union ints,该条件已满足。由于union ints * 是指向union ints 的指针的完美表示,因此该句子中没有任何内容可以防止它被用来收集作为联合压入堆栈的值。

如果您作弊并尝试传递一个普通的 intunsigned int 来代替联合,那么您将调用未定义的行为。因此,您可以使用:

union ints u1 = ...;

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

你不能使用:

func(1, 0);

参数不是联合类型。

【讨论】:

    【解决方案3】:

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

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

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

    这是一个别名违规并调用未定义的行为。您可以通过使用memcpy 来避免它,我想这将是合法的..

    我也对你在这里的评论感到有些困惑:

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

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

    如果这不能回答您的问题,也许您可​​以详细说明您要解决的(尽管是理论上的)问题。

    【讨论】:

    • 您能否详细说明这是如何违反别名的?
    • 您首先通过int 类型的左值访问对象,然后通过union ints 类型之一访问对象。编译器可能会假设它们不会相互别名。
    • 是否允许编译器假定 restrict 代替省略号 (...) 传递的参数。
    • 编译器如何假设可变参数根据它们的类型在不同的地方传递?
    • 我发现“别名”看起来相关的唯一地方是 6.5 p 7 的脚注:“对象的存储值只能由具有以下类型之一的左值表达式访问: ... - 在其成员中包含上述类型之一的聚合或联合类型”。你的意思还有其他的禁令吗?
    猜你喜欢
    • 2011-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-31
    • 2020-03-21
    • 2011-08-21
    • 2023-03-14
    相关资源
    最近更新 更多