【问题标题】:Union with const and non-const members与 const 和 non-const 成员联合
【发布时间】:2021-03-03 14:30:20
【问题描述】:

我正在编写一些库代码,向用户公开一个 const 指针,但在某些操作期间,我需要更改此指针指向的位置(幕后 switcheroo 技巧)。我必须在不遇到 UB 或严格混叠违规的情况下解决此问题的一个想法是使用带有 const 成员的联合:

// the pointed-to objects (in production code, these are actually malloc'd blocks of mem)
int x = 0, y = 7;

typedef union { int * const cp; int * p; } onion;
onion o = { .cp = &x };
printf("%d\n", *o.cp);   //  <---------------------- prints: 0
o.p = &y;
printf("%d\n", *o.cp);   //  <---------------------- prints: 7

但我不知道这是否定义明确......有人知道它是否(或不是)以及为什么?


编辑:我想我提到我正在建立一个图书馆,因为很多人要求澄清这方面的细节,而不是回答我想要的更简单的问题。

下面,我通过将类型从 int* 更改为 int 来简化代码,现在我的问题很简单:以下定义明确吗?

typedef union { int const cp; int p; } onion;
onion o = { .cp = 0 };
printf("%d\n", o.cp);   //  <---------------------- prints: 0
o.p = 7;
printf("%d\n", o.cp);   //  <---------------------- prints: 7

【问题讨论】:

  • 向第 2 位回答并不得不隐藏他们的答案的人道歉:我的帖子中有一个错字,大大改变了问题
  • 当您使用“常量指针”时,您是专门使用int * const cp 来表示cpconst 还是您想要const int * cp 表示cp 指向const 数据?
  • 我的意思是:int * const cp(错字是:const int * cp
  • 请在您的问题中显示您的库如何公开指针。我猜你想实现指针只能在库内部修改,而不能由库的用户修改。我建议使用将指针返回给库用户的 getter 函数,而不是提供全局指针变量。您应该考虑到,当您在内部更改旧指针值时,使用该库的代码可能仍具有旧指针值的副本。这也可能由于编译器优化而发生,因为const 告诉编译器该值以后不会更改。
  • @Bodo 好问题:我知道指向结构或联合的指针可以转换为指向其第一个成员的指针,反之亦然(这让我意识到联合成员的顺序是颠倒的...我将在此评论后解决此问题)。我在想我可以将工会的地址传递给第一个成员的地址。不知道那会是什么样子......也许:(int * const *)&amp;myunion

标签: c pointers language-lawyer undefined-behavior unions


【解决方案1】:

我认为这是根据 C11 6.7.3 未定义的(等效段落在标准的所有版本中):

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

o.cp 无疑是使用 const 限定类型定义的对象。

o.p 的修改在我看来确实算作修改 o.cp 的尝试,因为这正是我们这样做的原因!

【讨论】:

  • 谢谢@M.M. C99呢? (我应该在问题中明确说明我正在使用的标准版本......而且很明显我需要提高我的问题写作技巧!)
  • @textral 在所有版本中都是一样的,我只是以此作为具体参考
  • union U { int i; float f; } u; u.i = 0; UB 因为在赋值给u.i的时候,我们也修改了u.f,我们通过一个int类型的左值来做到这一点,从而违反了所谓的严格别名规则[ s]?
  • @LanguageLawyer 与本问答无关;如果找不到重复的问题,请作为新问题发布
  • 和你的回答有关。由于您没有透露为什么 o.p 的修改在我看来确实算作修改 o.cp 的尝试,我想知道何时修改一个工会成员算作修改其他成员 [s] 以及何时不修改。
【解决方案2】:

我读过的每一本编程书都告诉我以下内容。

static const int x = 7;
int *px = (int *)&x;

没有定义,但是

static int x = 7;
const int *px1 = &x;
int *px2 = (int *)px1;

已定义。也就是说,如果原始指针(此处为 &amp;x)不是 const,您始终可以丢弃 const-ness。

在这里,我倾向于没有来自任何质量来源的相反意见,并且不费心查找标准(我不会为此付费)。

但是,您尝试导出不是 constconst。这实际上是有效的。该语言允许

extern const * int p;

在幕后是可写的。将其切换到带有定义的文件的方法看不到它const 是将其定义为int *p; 并且小心不要在包含定义的文件中包含声明。这使您可以不受惩罚地抛弃const。写入它看起来像:

int x;

    *((int **)&p) = &x;

过去的旧编译器拒绝extern const volatile machine_register;,但现代编译器没问题。

【讨论】:

  • 将 const 转换为 non-const 很好。问题来自通过非限定类型访问 const 限定对象的存储值。
  • @user694733:做了一些调整。
  • 这个答案似乎根本没有解决问题(这是关于union的行为)
  • @M.M:问题是如何在内部能够写入的同时导出 const 变量,并建议使用联合作为一种可能的答案。我不建议将工会作为答案,因为我认为他试图利用工会来强制进行不需要它的类型转换,并且无法提出工会不完全成熟的途径。跨度>
  • @Joshua:在通用平台上,一种实现指定它根据平台记录的应用程序二进制接口处理导出和导入的定义,而不关心如何使用导出的符号或实际定义导入的符号, 因此将扩展语言的语义以允许在没有 const 限定符的情况下定义和导出符号,但其他编译单元通过包含 const 限定符的声明导入。因为某些平台对只读内容使用不同类型的链接器符号...
【解决方案3】:

如果接口是 const 声明的指针,例如 int *const(就像您在评论中指出的那样),那么您无法更改不会触发 UB。

如果您将int * 存储在某处(例如,作为static int *ip;)并通过int *const* 指针(例如int *const* ipcp = &amp;ip;)公开其地址,那么您可以简单地重新转换为@ 987654327@(我给出的示例中 &amp;ip 的原始类型)并使用它来访问 int* 指针。

【讨论】:

    【解决方案4】:

    标准使用术语“对象”来指代许多概念,包括:

    1. 静态、自动或线程持续时间存储区域与“独立”命名标识符的独占关联,除非使用左值或从它派生的指针。

    2. 由左值标识的任何存储区域。

    在块范围内,声明struct s1 { int x,y; } v1; 将导致创建一个名为v1 的对象,它满足上面的第一个定义。在v1 的生命周期内,没有其他满足该定义的命名对象将与同一存储明显关联。像v1.x 这样的左值表达式将标识满足第二个定义但不是第一个定义的对象,因为它会标识不仅与左值表达式v1.x 相关联的存储,而且还与命名的独立对象@987654326 相关联@。

    我认为标准的作者没有充分考虑或就规则描述“对象”的哪个含义的问题达成任何有意义的共识:

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

    如果第一种对象是用 const 限定符定义的,那么尝试修改它的代码行为将超出标准的管辖范围,这当然是有道理的。如果将规则解释为更广泛地适用于其他类型的对象,那么在其生命周期内修改此类对象的行为也将超出标准的管辖范围,但标准确实没有有意义地描述第二种对象的生命周期类型为基础存储的生命周期以外的任何内容。

    将引用的文本解释为仅适用于第一类对象会产生清晰有用的语义;试图将其应用于其他类型的对象会产生更模糊的语义。也许这样的语义可能对某些目的有用,但我认为与将文本应用于第一类对象相比没有任何优势。

    【讨论】:

      猜你喜欢
      • 2020-07-09
      • 2019-04-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-29
      相关资源
      最近更新 更多