【问题标题】:"Private" struct members in C with constC中带有const的“私有”结构成员
【发布时间】:2012-12-12 04:23:06
【问题描述】:

为了有一个干净的代码,使用一些 OO 概念可能很有用,即使在 C 中也是如此。 我经常编写由一对 .h 和 .c 文件组成的模块。问题是模块的用户必须小心,因为私有成员在 C 中不存在。使用 pimpl 习惯用法或抽象数据类型是可以的,但它添加了一些代码和/或文件,并且需要更重的代码。我讨厌在不需要访问器时使用访问器。

这里有一个想法,它提供了一种让编译器抱怨对“私人”成员的无效访问的方法,只需要一些额外的代码。这个想法是定义两次相同的结构,但为模块的用户添加了一些额外的“const”。

当然,使用演员表仍然可以写入“私人”成员。但重点只是避免模块用户出错,而不是安全地保护内存。

/*** 2DPoint.h module interface ***/
#ifndef H_2D_POINT
#define H_2D_POINT

/* 2D_POINT_IMPL need to be defined in implementation files before #include */
#ifdef 2D_POINT_IMPL
#define _cst_
#else
#define _cst_ const
#endif

typedef struct 2DPoint
{
    /* public members: read and write for user */
    int x;
    
    /* private members: read only for user */
    _cst_ int y;
} 2DPoint;

2DPoint *new_2dPoint(void);
void delete_2dPoint(2DPoint **pt);
void set_y(2DPoint *pt, int newVal);


/*** 2dPoint.c module implementation ***/
#define 2D_POINT_IMPL
#include "2dPoint.h"
#include <stdlib.h>
#include <string.h>

2DPoint *new_2dPoint(void)
{
    2DPoint *pt = malloc(sizeof(2DPoint));
    pt->x = 42;
    pt->y = 666;

    return pt;
}

void delete_2dPoint(2DPoint **pt)
{
    free(*pt);
    *pt = NULL;
}

void set_y(2DPoint *pt, int newVal)
{
    pt->y = newVal;
}

#endif /* H_2D_POINT */


/*** main.c user's file ***/
#include "2dPoint.h"
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    2DPoint *pt = new_2dPoint();

    pt->x = 10;     /* ok */
    pt->y = 20;     /* Invalid access, y is "private" */    
    set_y(pt, 30);  /* accessor needed */
    printf("pt.x = %d, pt.y = %d\n", pt->x, pt->y);  /* no accessor needed for reading "private" members */

    delete_2dPoint(&pt);
    
    return EXIT_SUCCESS;
}

现在,问题来了:这个技巧是否符合 C 标准? 它在 GCC 上运行良好,编译器不会抱怨任何事情,即使有一些严格的标志,但我怎么能确定这真的没问题?

【问题讨论】:

  • 有趣的方法。我不知道这是否是明确定义的行为。我建议不要这样做,因为它与惯用的 C 相去甚远……要么使用不透明的结构(在 .c 文件中定义)并提供访问器,要么记录不分配字段的文档。
  • 我认为 Thomas 的答案应该是一个“真实”的答案——也许有几个例子。
  • 顺便问一下,2DPoint 是如何形成有效标识符的?
  • 如果您想更好地了解不透明结构的工作原理,我在 H2CO3 的回答下详细阐述了 Thomas 的回答(无意中,没有看到评论)。

标签: c struct constants private


【解决方案1】:

用 Bjarne Stroustrup 的话来说:C 不是为支持 OOP 而设计的,虽然它启用 OOP,这意味着可以用 C 编写 OOP 程序,但只能非常很难这样做。因此,如果您必须用 C 编写 OOP 代码,使用这种方法似乎没有什么问题,但最好使用更适合该目的的语言。

通过尝试用 C 编写 OOP 代码,您已经进入了一个必须覆盖“常识”的领域,因此只要您负责正确使用这种方法就可以了。您还需要确保它被彻底和严格地记录在案,并且与代码有关的每个人都知道它。

编辑哦,您可能需要使用演员表才能绕过const。我不记得 C 风格的演员表是否可以像 C++ const_cast 一样使用。

【讨论】:

  • 写入一个常量已经被抛弃的对象无论如何都是UB。
  • 哦,我明白了。我似乎记得曾经读过 constness 可以安全地抛弃,但我可能弄错了。我会再看一遍。谢谢。
  • 啊,我明白了问题所在。我认为问题是关于是否可以使用这种方法。我忽略了关于它是否与 gcc 以外的其他编译器一起安全的部分。对此感到抱歉。
【解决方案2】:

这几乎可以肯定是未定义的行为。

禁止编写/修改声明为 const 的对象,这样做会导致 UB。此外,您采用的方法将struct 2DPoint 重新声明为两种技术上不同的类型,这也是不允许的。

请注意,这(通常是未定义的行为)并不意味着它“肯定不会工作”或“它必须崩溃”。事实上,我觉得它很合乎逻辑,因为如果一个人聪明地阅读源代码,他可能很容易发现它的目的是什么以及为什么它可能被认为是正确的。然而,编译器并不智能——充其量,它是一个有限自动机,不知道代码应该做什么;它只(或多或少)遵守语法的句法和语义规则。

【讨论】:

  • 这是肯定未定义的行为(几乎不是);)
  • @H2CO3,不是我,但也被诱惑了。您的回答在虚假的边界上是不精确的。 C 中的“标识符”当然没有类型或者可以是const-qualified。对象有一个类型,可以是const 限定的,而UB 可以访问这样一个const 限定类型的对象。因此,仅此规则不会使问题 UB 中的方法。埃里克给出了正确的答案。
  • @H2CO3 错误。对象的类型是在没有const的编译单元中确定的,所以对象的类型不是const-qualified。因此,将这样的对象传回该编译单元以对其进行修改是有效的。再说一遍,你的想法为什么这是 UB 是错误的,Eric 给出了正确的答案,而你的现在正在复制那个,没有提到 Eric 的。
  • @H2CO3:C 标准中没有 const 限定对象这样的东西。有 const 限定的类型。有一条规则禁止访问使用具有非 const 限定类型的左值的 const 限定类型定义的对象,但这不适用于此处,因为该对象将使用非 const 类型定义,而 const 类型将仅用于访问。引用 C 中违反的确切规则。
  • [更正错字。] @H2CO3:您是否撤回了您的声明,即“由于对象是 const 限定的并且它是 [modified],这就是调用 UB”的原因?因为对象不是 const 限定的(因为没有这样的东西),并且对象不是由 const 限定的类型定义的,所以修改它本身并不是未定义的行为。如果您不撤回,请说明违反的 C 标准的具体条款。
【解决方案3】:

这违反了 C 2011 6.2.7 1。

6.2.7 1 要求同一结构在不同翻译单元中的两个定义具有兼容的类型。不允许有const 在一个而不是另一个。

在一个模块中,您可能有对这些对象之一的引用,并且这些成员对于编译器来说似乎是 const。当编译器编写对其他模块中的函数的调用时,它可能会将来自 const 成员的值保存在寄存器或其他缓存中,或者保存在源代码后面的部分或完全评估的表达式中,而不是函数调用。那么,当函数修改成员并返回时,原来的模块不会有改变的值。更糟糕的是,它可能会使用更改后的值和旧值的某种组合。

这是非常不恰当的编程。

【讨论】:

  • @H2CO3,你能解释清楚一点吗?与您的答案相反,这里给出了一个正确答案,包括对标准的参考,说明为什么这是 UB。
  • @JensGustedt 看看编辑。最初 Eric 提供了一个关于 C++ 的答案。从那时起,他删除了他的 cmets 并编辑了他的答案。我也删除了我现在已经过时的评论。
  • @H2CO3,即使以前这是 C++ 的答案,但在这两种语言中,关于这个的想法似乎是相似的,只是措辞不同。无论如何,现在这个答案在这里使用了正确的措辞,它给出了一个很好的动机,不仅是为什么这是 UB,而且为什么这两种不同的类型可能真的有不同的布局。
  • @JensGustedt 是的,这就是我删除评论的原因。以前是你在抱怨措辞,所以我们不要继续这种毫无意义的讨论。我是对的,埃里克也是对的,你不再纠缠任何人,就会有和平。再见。
【解决方案4】:

您可以使用不同的方法 - 声明两个 structs,一个用于没有私有成员(在标题中)的用户,另一个用于在您的实现单元中内部使用的私有成员。所有私有成员都应该放在公共成员之后。

您总是将指针传递给struct,并在需要时将其转换为内部使用,如下所示:

/* user code */
struct foo {
    int public;
};

int bar(void) {
    struct foo *foo = new_foo();
    foo->public = 10;
}

/* implementation */
struct foo_internal {
    int public;
    int private;
};

struct foo *new_foo(void) {
    struct foo_internal *foo == malloc(sizeof(*foo));
    foo->public = 1;
    foo->private = 2;
    return (struct foo*)foo;  // to suppress warning
}

C11 允许unnamed structure fields(GCC 有一段时间支持它),所以如果使用 GCC(或符合 C11 的编译器),您可以将内部结构声明为:

struct foo_internal {
    struct foo;
    int private;
};

因此不需要额外的努力来保持结构定义的同步。

【讨论】:

  • 这并没有实现类的“非朋友”可以读但不能写成员的特性。
  • @EricPostpischil 同意,但您可以使用 setter/getter。我认为它是最接近的,因此没有理由投反对票
  • 问题表明要避免使用访问器:“我讨厌在不需要访问器时使用访问器。”这个答案并不能解决提出的问题。
  • @EricPostpischil 我的回答确实不能解决问题,但这个问题无法解决,因此我提供了最接近的解决方案
猜你喜欢
  • 2012-03-02
  • 2023-03-22
  • 2011-04-11
  • 1970-01-01
  • 1970-01-01
  • 2012-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多