通过联合类型进行const强制转换是否属于未定义的行为?

17

与 C++ 不同,C 没有 const_cast 的概念。也就是说,没有有效的方式将带 const 限定符的指针转换为不带限定符的指针:

void const * p;
void * q = p;    // not good

首先: 这种类型转换是否真的是未定义行为?

无论如何,GCC都会对此发出警告。为了使需要const-cast的“干净”代码(即我可以保证不修改内容,但只有一个可变指针),我见过以下的“转换”技巧:

typedef union constcaster_
{
    void * mp;
    void const * cp;
} constcaster;

用法:u.cp = p; q = u.mp;

C语言对于通过这种联合体去除const限定的规则是什么?我对C语言只有一些零散的了解,但我听说C语言在联合体访问方面比C++宽松得多,因此虽然我对这个构造有些不好的感觉,但我想要一个来自标准(我想是C99,尽管如果在C11中已经改变了,就需要知道)的论据。


1
您是指第一个代码块中的 void* q = (void*)p 吗? - kennytm
@KennyTM: 是的 - 我需要显式转换吗?这样做有什么不同吗? - Kerrek SB
没有隐式转换,gcc会警告放弃限定符。但是即使有类型转换,gcc仍然会报错(“初始化元素不是常量”)。 - Some programmer dude
4个回答

10

这是实现定义的,参见C99 6.5.2.3/5:

如果在对象的最新存储是针对不同成员的情况下使用联合对象的成员的值,则其行为是实现定义的。

更新:@AaronMcDaid评论说这可能是明确定义的。

标准指定了以下内容6.2.5/27:

同样,指向兼容类型的合格或非合格版本的指针应具有相同的表示和对齐要求。27)

27) 相同的表示和对齐要求意味着作为函数参数,从函数返回值以及联合成员进行互换。

并且(6.7.2.1/14):

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

在这种特殊情况下,可能可以得出结论,只有一种方法可以访问联合中的元素。


1
不错!所以我不能保证它能做到我想要的吗? - Kerrek SB
1
“实现定义”意味着编译器提供者必须指定发生的情况。但是,您不能指望它适用于所有编译器。 - Lindydancer
联合体的成员在内存中的位置有保证吗?通过 constcaster u;,肯定 &(u.mp) == &(u.cp) 吧?或者这种情况会因编译器而异?如果我们确实有这个保证,并且还有其他保证 const 和 non-const 会被表示为相同的方式,那么在这种特定情况下,它肯定是明确定义的了? - Aaron McDaid
虽然这不如 Kenny 的回答详细,但我会接受这个回答,因为它直截了当,而且 Kenny 不需要声望。;-) - Kerrek SB
是的,可以得出这样的结论。我认为,实现中没有其他的空间去做其他事情。再次强调,这仅适用于联合体的特定用法,所有其他用法仍属于“实现定义”类别。无论如何,就所有实际目的而言,即使不能使用语言律师逻辑证明它,它也会起作用。;) - Lindydancer
显示剩余3条评论

3
我的理解是,只有在尝试修改const声明的对象时,才会出现UB。
因此,以下代码不会出现UB:
int x = 0;
const int *cp = &x;
int *p = (int*)cp;
*p = 1; /* OK: x is not a const object */

但这是 UB:
const int cx = 0;
const int *cp = &cx;
int *p = (int*)cp;
*p = 1; /* UB: cx is const */

使用联合(union)而不是强制转换在这里不应该有任何区别。
根据C99规范中的类型修饰符(6.7.3),如果尝试通过具有非const限定类型的lvalue来修改使用const限定类型定义的对象,则行为未定义。

1
你可以假设我从不进行任何变异。也就是说,我有一个非变异操作需要处理遗留的可变指针。我知道违反constness是UB(未定义行为)--我的问题主要是关于指针修改。在C++中,我只需要说 void * q = const_cast<void *>(p); ... - Kerrek SB
@KerrekSB const_cast<void*>不就是(void*)的语法糖吗(也许会有一些额外的检查,但行为相同)? - Christian Rau
2
@ChristianRau:不,它是语言和类型系统的一部分。如果你愿意,整个类型系统只是汇编语言上的语法糖,因此在这里区分很重要... - Kerrek SB
@KerrekSB 我知道这是语言的一部分,但这只是一种更安全的编写(void*)的方式。但好吧,我看到我们正在标准领域中移动,在那里说“应该表现为”是不够的。 - Christian Rau
@ChristianRau:const_cast的重要部分是额外的检查。 - Mooing Duck
@MooingDuck 是的,但是如果检查成功,行为仍然与(void*)相同。而这些检查并不能防止您修改转换存储,不是吗。 - Christian Rau

3
初始化肯定不会导致未定义行为。在§6.3.2.3/2(n1570(C11))中明确允许限定指针类型之间的转换。然而,后续使用指针中的内容会导致未定义行为(请参见@rodrigo的回答)。
但是,如果需要将void*转换为const void*,则需要显式进行强制类型转换,因为简单赋值的约束仍要求LHS上的所有限定符出现在RHS上。 §6.7.9/11: ... 对象的初始值是表达式的值(经过转换);应用与简单赋值相同的类型限制和转换,其中标量的类型为其声明类型的未限定版本。 §6.5.16.1/1:(简单赋值/约束)
  • ... 两个操作数都是指向兼容类型的限定或未限定版本的指针,并且左侧指向的类型具有右侧指向的类型的所有限定符;
  • ... 一个操作数是对象类型的指针,另一个操作数是指向限定或未限定版本的void的指针,并且左侧指向的类型具有右侧指向的类型的所有限定符;

我不知道为什么gcc只会给出警告。


关于联合体的技巧,虽然它不是未定义行为,但结果可能是未指定的。

§6.5.2.3/3 fn 95: 如果用于读取联合体对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型的对象表示,如6.2.6所述(有时称为“类型游戏”)。这可能是一个陷阱表示。

§6.2.6.1/7: 当一个值被存储在联合类型对象的成员中时,不对应于该成员但对应于其他成员的对象表示的字节采用未指定的值。(*注意:另请参见§6.5.2.3/6的例外情况,但它在这里不适用)


n1124(C99)中相应的章节为

  • C11 §6.3.2.3/2 = C99 §6.3.2.3/2
  • C11 §6.7.9/11 = C99 §6.7.8/11
  • C11 §6.5.16.1/1 = C99 §6.5.16.1/1
  • C11 §6.5.2.3/3 fn 95 = 缺失(C99中没有出现“类型转换”)
  • C11 §6.2.6.1/7 = C99 §6.2.6.1/7

太好了,谢谢!那么你认为编写 C 代码的最佳方式是在必要时进行显式转换,并将 -Wno-cast-qual 传递给 GCC 吗? - Kerrek SB
@KerrekSB:我肯定倾向于更严格地警告隐式转换可能会在未来引起问题😊,而最好使用#pragma GCC diagnostic来减少禁用警告的区域。 - kennytm
尽管这是最佳答案,但您支持我接受一个声望较低的答案吗?您知道,这关乎声望、动机和效用 :-) - Kerrek SB
这也值得为C11到C99的映射增加许多额外的+1! - Kerrek SB

0
不要进行类型转换。这是一个指向const的指针,这意味着尝试修改数据是不允许的,在许多实现中,如果指针指向不能修改的内存,则会导致程序崩溃。即使您知道内存可以被修改,也可能有其他指向它的指针不希望它改变,例如,如果它是逻辑不可变字符串的存储部分。
警告是有充分理由存在的。
如果需要修改const指针的内容,则可移植且安全的方法是首先复制其指向的内存,然后对其进行修改。

1
@Kerrek SB, C允许您将非const指针传递给const参数。const char* p = q;其中q是char*也没问题。 - JeremyP
1
@KerrekSB 你需要使用const_cast来将非常量转换为常量吗? - Random832
1
@Christian:不是。我的回答是针对所给问题的正确通用答案。不要将const转换为非const,原因在于...... - JeremyP
1
@Christian:“不要这样做”怎么不是对于联合风格转换问题的回答呢? - JeremyP
2
也许你的误解在于你说,“如果你需要修改一个常量指针的内容...”。Kerrek从未声明或暗示过他要修改指向常量的指针的引用,所以也许你的反对是针对Kerrek没有提出的另一个问题。 - Steve Jessop
显示剩余13条评论

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