【问题标题】:Is it worthwhile changing a C declaration to add type safety?是否值得更改 C 声明以增加类型安全?
【发布时间】:2019-07-12 08:16:05
【问题描述】:

我想知道为了设置特定类型而重写标题声明是否合理或值得。优先于 void * 之类的东西,它不会增加类型安全性。

例如,如果您有一个通用存储函数,它将传感器读数添加到循环缓冲区:

int add_reading(void *);

为了通用,函数必须定义为 void *。但是,在头文件中,您可以将函数声明为:

int add_reading(my_reading_t *);

这将在 void 指针上增加一定程度的类型安全性。在通用标头中,您可以使用默认为 void 的 #define 设置类型。因此可以在#include 之前定义覆盖类型。

这似乎是一种不必要的 hack,但是对于不透明指针也可以这样争论——使用 opaque_type_t * 而不是 void *。但这至少是定义的行为。我想知道这种类型的混乱是否会调用 UB(未定义的行为)?

【问题讨论】:

  • 如果你打算在你的标题之前有一个#define #include,那么我认为这是一个非常糟糕的主意。不是最具可扩展性的概念。如果你想在同一个文件中使用不同类型的函数怎么办?
  • 呃。您是对的 - 代码中确实存在一个基本假设,即该函数仅用于一种类型。实际上有一条评论“只有其中一个,所以本地静态结构保持状态”:-)
  • 如果你有一个通用函数,你可能应该传递你正在操作的任何东西的大小以及指向数据的指针。除非,也许,您存储的是指针本身,而没有对其进行任何解释。我会选择类型安全的覆盖函数:static inline int add_my_reading(my_reading_t *r) { return add_reading(r); } 就足够了(假设您只存储指针并且不需要大小)。对于每种不同的类型,您可能还需要不同的循环缓冲区,在这种情况下,接口需要更多的升级工作,以包含“环形缓冲区句柄指针”。
  • DeiDei的评论是正确答案。这不是一个可扩展的概念。它实际上是在一个地方使用的代码,现在在多个地方使用 - 这意味着它应该正确编写或复制/粘贴并单独调整。不是半途而废。我很懒...
  • 如果你小心的话,你的头部可以定义覆盖函数,并在后面加上#define add_reading(r) do not call add_reading directly,以防止意外使用原始函数。就目前而言,有人可以故意使用:(add_reading)(r); 会调用原始函数,尽管有类似函数的宏。使用#define add_reading do not use add_reading directly 也可以防止这种误用。 #undef 仍然可以使用——但有一点是你试图防止恶意而不是意外,最好让编码人员遵守手册和规则。

标签: c type-conversion header-files void-pointers


【解决方案1】:

int add_reading(void *) 声明的函数与用int add_reading(my_reading_t *) 定义的函数不兼容。根据 C 2018 6.5.2.2 9,C 标准不会定义使用用前者声明的标识符(或具有该类型的其他函数指示符)调用用后者定义的函数的行为:

如果函数定义的类型与表示被调用函数的表达式所指向的(表达式的)类型不兼容,则行为未定义。

每 6.7.6.1 2:

对于要兼容的两个指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。

显然,参数类型void *my_reading_t * 不是指向兼容类型的指针(假设my_reading_t 是结构类型,不是void 的别名)。

每 6.7.6.3 15:

为了让两个函数类型兼容,……对应的参数应该有兼容的类型……

【讨论】:

  • @ChristianGibbons:不。我的回答中引用了 C 标准的相关段落。指向对象的指针可以与void * 相互转换,但这并不意味着它们是兼容的类型。 C 6.2.5 说指向 void 的指针和指向字符的指针必须具有相同的表示和对齐要求,并且脚注说这意味着它们是可互换的,显然覆盖了其他兼容性规则。同样,指向结构的指针必须彼此相同,但不能作为指向 void 的指针。指向联合的指针形成另一个类,与结构不同。
  • 是的,旧的未定义行为。心里常说“看,有地址,没问题”。但这并不意味着它是正确的。对 void * 的转换与“让我们假设这是一个 void * 并确定,没关系”不同。直到 gcc 优化某些东西,这一切都结束了。有时我对带有零和指针的 memset 有同样的感觉(NULL 不一定是全位 0)但是嘿,你知道,无论如何 :-)
  • 我相信 6.7.6.1.2 与挑剔的不同之处在于优化编译器。我在想 gcc -O2 可以假设不兼容的类型不指向相同的地址并基于此优化访问。可能会导致奇怪的错误。
【解决方案2】:

你的提议看起来是个坏主意。如果你想提升类型安全,如果你尝试传入错误的类型,它就无法编译,你可以尝试使用 C11 的_Generic

int add_reading (void *);
#define ADD_READING_HELPER(X) _Generic((X), \
    my_reader_t *: add_reading((X)) \
)

int main(void) {
    my_reader_t good;
    printf("%d\n", ADD_READING_HELPER(&good)); // works because _Generic has a rule for dealing with (my_reader_t *)
    int bad;
    printf("%d\n", ADD_READING_HELPER(&bad)); // fails to compile because the _Generic does not have a rule for dealing with (int *)
}

int add_reading (void *arg) {
    // whatever the function does
}

基本上_Generic 允许您根据传递给它的表达式的控制类型执行不同的操作,这一切都在编译时确定。我们在这里所做的是为my_reader_t * 创建一个规则,但没有其他类型,因此尝试将my_reader_t * 以外的任何内容传递给_Generic 将阻止程序编译,因为它没有任何规则如何处理该类型。

【讨论】:

  • 这是对泛型选择的巧妙运用。我以前从未想过以这种方式使用它。
  • @JohnBollinger 希望我能把这个想法归功于我,但我以前见过其他人这样做。
  • 好主意。此外,您可以将宏命名为相同的 #define add_reading(X) _Generic((X), my_reader_t *: add_reading((X)) ,以便调用是类型安全的,但使用 add_reading 仍然可以使用 int (*)(void *)
  • @PSkocik 这很有趣。它最初对我不起作用,因为我将所有测试代码都放在一个文件中,并且通过将函数定义放在 #define ... 之后,它用 _Generic 的东西替换了实际的函数定义,它不喜欢那样。 error: expected identifier or '('.
  • @ChristianGibbons 你也可以用括号来防止扩展。 int (add_reading)(void *X) { /*definition*/ } 不会将 add_reading 扩展为类似函数的宏。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-11
  • 2019-04-02
  • 1970-01-01
相关资源
最近更新 更多