【问题标题】:Is const-casting via a union undefined behaviour?是否通过联合未定义行为进行常量转换?
【发布时间】:2012-02-08 19:42:20
【问题描述】:

与 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 语言通过这样的联合抛弃常量的规则是什么?我对 C 的了解非常零碎,但我听说 C 在联合访问方面比 C++ 宽松得多,所以虽然我对这种结构有不好的感觉,但我想从标准中得到一个论据(我想是 C99,不过如果这在 C11 中发生了变化,那就太好了)。

【问题讨论】:

  • 你的意思是第一个代码块中的void* q = (void*)p
  • @KennyTM:是的——我需要明确的演员表吗,和/或有什么不同吗?
  • 没有隐式转换 gcc 会警告丢弃限定符。但是 gcc 仍然报告错误(“初始化元素不是常量”),即使使用强制转换。

标签: c constants language-lawyer unions


【解决方案1】:

它的实现定义,见 C99 6.5.2.3/5:

如果联合对象的成员的值在最 最近存储到对象的是不同的成员,行为是 实现定义。

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

标准规定如下 6.2.5/27:

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

27) 相同的表示和对齐要求旨在 暗示可互换性作为函数的参数,返回值来自 职能和工会成员。

和(6.7.2.1/14):

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

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

【讨论】:

  • 不错!所以我不能保证这真的会像我想的那样吗?
  • 嗯,“实现定义”意味着编译器提供者必须指定会发生什么。但是,您不能依赖它为 所有 编译器工作。
  • 工会成员在内存中的位置是否有任何保证? constcaster u;,肯定是&(u.mp) == &(u.cp)?或者这是否允许从编译器到编译器的这种变化?如果我们确实有这个保证,以及其他一些保证 const 和 non-const 表示相同的说法,那么在这种特殊情况下它肯定会得到很好的定义吗?
  • @AaronMcDaid:我相信所有联合成员都保证有相同的地址......我很确定在 C++ 中它直接来自“联合的行为就像一个只包含活动成员的结构",而 C 必须有类似的东西......
  • 虽然这不像肯尼的回答那么详细,但我会接受,因为它直截了当,而且肯尼不需要代表 ;-)
【解决方案2】:

我的理解是,只有在您尝试修改 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 */

在这里使用联合而不是强制转换应该没有任何区别。

来自 C99 规范(6.7.3 类型限定符):

如果尝试通过使用来修改使用 const 限定类型定义的对象 具有非 const 限定类型的左值,行为未定义。

【讨论】:

  • 你可能会认为我从不变异任何东西。也就是说,我有一个非变异操作需要处理遗留的可变指针。我知道违反 constness 是 UB——我的问题主要是关于指针杂耍。在 C++ 中,我只想说 void * q = const_cast<void *>(p);...
  • @KerrekSB @KerrekSB const_cast<void*> 不只是 (void*) 的语法糖吗(可能有一些额外的检查但行为相同)?
  • @ChristianRau:不,它是语言和类型系统的一部分。如果你愿意,整个类型系统只是汇编器上的语法糖,所以这里的区别很重要......
  • @KerrekSB 我知道这是语言的一部分,但它只是编写(void*) 的一种更安全的方式。但是好的,我看到我们正在这里进入标准领域,说“应该表现得像”是不够的。
  • @ChristianRau:const_cast 的重要部分附加检查。
【解决方案3】:

初始化肯定不会引起UB。 §6.3.2.3/2 (n1570 (C11)) 明确允许限定指针类型之间的转换。之后是该指针中内容的使用导致了 UB(请参阅@rodrigo 的答案)。

但是,您需要显式转换才能将 void* 转换为 const void*,因为简单赋值的约束仍然要求 LHS 上的所有限定符都出现在 RHS 上。

§6.7.9/11: ...对象的初始值是表达式的初始值(转换后);应用与简单赋值相同的类型约束和转换,将标量的类型作为其声明类型的非限定版本。

§6.5.16.1/1:(简单赋值/约束)

  • ...两个操作数都是 指向兼容类型的合格或非合格版本的指针,以及指向的类型 to by left 具有所有由 right 指向的类型的限定符;
  • ...一个操作数是一个指针 指向一个对象类型,另一个是指向一个合格或不合格版本的指针 void,而左边指向的类型具有所指向类型的所有限定符 靠右;

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


对于联合技巧,是的,它不是 UB,但结果可能仍不确定。

§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 吗?
  • @KerrekSB:我当然赞成对隐式转换发出更严格的警告,这可能会在未来导致问题☺,并且可能更好地使用#pragma GCC diagnostic 来最小化禁用警告的区域。
  • 即使这是最好的答案,你会支持我接受一个较低代表的答案吗?这是关于声誉-动机-效用的,你看:-)
  • 对于 C11 到 C99 的映射,这也值得多加 +1!
【解决方案4】:

根本不要施放它。它是指向 const 的指针,这意味着不允许尝试修改数据,并且在许多实现中,如果指针指向不可修改的内存,则会导致程序崩溃。即使您知道可以修改内存,也可能有其他指向它的指针不希望它改变,例如如果它是逻辑不可变字符串存储的一部分。

警告是有充分理由的。

如果您需要修改 const 指针的内容,可移植的安全方法是首先复制它指向的内存,然后对其进行修改。

【讨论】:

  • @Kerrek SB C 允许您将非 const 指针传递给 const 参数。 const char* p = q; 其中 q 是 char* 很好。
  • @KerrekSB 您需要 const_cast 才能从非常量变为 const?
  • @Christian:不。我的答案是对所给问题的正确一般答案。 不要将 const 转换为非常量,这就是为什么等等。
  • @Christian:“不要这样做”怎么不是工会风格演员问题的答案?
  • 也许误解是你说的“如果你需要修改 const 指针的内容......”。 Kerrek 从未声明或建议他要修改指向 const 的指针的引用,所以您的反对意见可能是针对 Kerrek 没有提出的另一个问题。
猜你喜欢
  • 1970-01-01
  • 2011-01-09
  • 2015-10-03
  • 2011-05-16
  • 1970-01-01
  • 2011-02-23
  • 1970-01-01
  • 2012-07-23
相关资源
最近更新 更多