泛型与类型安全?在 C 中使用 void*

Posted

技术标签:

【中文标题】泛型与类型安全?在 C 中使用 void*【英文标题】:Genericity vs type-safety? Using void* in C 【发布时间】:2010-12-26 08:58:48 【问题描述】:

来自 OO(C#、Java、Scala) 我非常重视代码重用和类型安全的原则。上述语言中的类型参数可以完成这项工作并启用类型安全且不会“浪费”代码的通用数据结构。

当我陷入 C 中时,我意识到我必须做出妥协,我希望它是正确的。要么我的数据结构在每个节点/元素中都有一个 void * 并且我失去了类型安全性,要么我必须为我想要使用它们的每种类型重新编写我的结构和代码。

代码的复杂性是一个明显的因素:遍历数组或链表是微不足道的,将*next 添加到结构中也不需要额外的努力;在这些情况下,不要尝试和重用结构和代码是有意义的。但对于更复杂的结构,答案就不那么明显了。

还有模块化和可测试性:将类型及其操作从使用该结构的代码中分离出来,使测试变得更容易。反之亦然:在一个结构上测试一些代码的迭代,同时它试图做其他事情会变得混乱。

那么你的建议是什么? void * 和重用或类型安全和重复代码?有什么一般原则吗?当它不适合时,我是否试图强制 OO 进入程序?

编辑:请不要推荐C++,我的问题是关于C的!

【问题讨论】:

嘘你,尼尔。 C 是与 C++ 不同的语言。在这个特定的时间,C++ 可能比 C 更容易让 OP 学习,但这并不会使 C 成为一种糟糕的语言,不会使这个问题的有效性降低,也不会使辩论的信息量减少。这只是一个不回答的问题,扼杀了一个讨论 C 语言优缺点的有趣机会。 我差点说“请不要推荐 C++,因为这不是问题所在”... 我绝不建议 C 是一种糟糕的语言。但是,如果 OP 可以选择其中任何一个,那么显而易见的选择是 C++,即使他只想编写程序代码。我同意这是一个非答案,这正是我发表评论的原因。 感谢您的评论!不,这必须是 C。另外,我要做的对 C 来说绝不是不寻常的。 【参考方案1】:

我会说使用void *,这样您就可以重复使用代码。重新实现例如更多的工作链接列表,而不是确保您正确获取/设置列表中的数据。

尽可能多地从glib 那里获得提示,我发现他们的数据结构非常好用且易于使用,并且由于失去类型安全而几乎没有遇到任何麻烦。

【讨论】:

感谢您提供的链接,看来我可以从该项目中学到很多东西(并且可能会用到其中的一些)。【参考方案2】:

我认为您必须按照您的建议在两者之间取得平衡。如果代码只有几行且微不足道,我会复制它,但如果它更复杂,我会考虑使用void*,以避免在多个地方进行任何潜在的错误修复和维护,并减少代码大小。

如果您查看 C 运行时库,有几个“通用”函数可以与 void* 一起使用,一个常见的示例是使用 qsort 进行排序。为您想要排序的每种类型复制此代码是很疯狂的。

【讨论】:

【参考方案3】:

使用 void 指针并没有错。在将它们分配给指针类型的变量时,您甚至不必强制转换它们,因为转换是在内部完成的。可能值得一看:http://www.cpax.org.uk/prg/writings/casting.php

【讨论】:

【参考方案4】:

这个问题的答案与在 C++ 中获取有效的链接列表模板相同。

a) 创建使用 void* 或一些抽象类型的算法的抽象版本

b) 创建一个轻量级的公共接口来调用抽象类型算法和它们之间的种姓。

例如。

typedef struct simple_list
  
  struct simple_list* next;
   SimpleList;

void add_to_list( SimpleList* listTop, SimpleList* element );
SimpleList* get_from_top( SimpleList* listTop );
// the rest

#define ListType(x) \
    void add_ ## x ( x* l, x* e ) \
         add_to_list( (SimpleList*)l, (SimpleList*)x );  \
    void get_ ## x ( x* l, x* e ) \
         return (x*) get_from_to( (SimpleList*)l );  \
   /* the rest */

typedef struct my_struct
  
  struct my_struct* next;
  /* rest of my stuff */
   MyStruct;

ListType(MyStruct)

MyStruct a;
MyStruct b;

add_MyStruct( &a, &b );
MyStruct* c = get_MyStruct(&a);

等等等等

【讨论】:

查看我的回答,了解如何做类似的事情,但方式是安全的 w.r.t 别名规则。【参考方案5】:

这里我们在 C 中大量使用 OO,但只是为了封装和抽象,没有多态之类的。

这意味着我们有特定的类型,例如 FooBar(Foo a, ...) 但是,对于我们的集合“类”,我们使用 void *。只需在可以使用多种类型的地方使用 void *,但这样做,确保您不需要参数是特定类型。根据集合,有 void * 是可以的,因为集合不关心类型。但是,如果您的函数可以接受类型 a 和类型 b,但不能接受其他类型,请创建两个变体,一个用于 a,一个用于 b。

主要的一点是只有在你不关心类型时才使用 void *。

现在,如果您有 50 个具有相同基本结构的类型(比如说,int a;int b;作为所有类型的第一个成员),并且想要一个函数作用于这些类型,只需将常见的第一个成员设为 a自己输入,然后让函数接受这个,并传递 object->ab 或 (AB*)object 是你的类型是不透明的,如果 ab 是你的结构中的第一个字段,两者都将起作用。

【讨论】:

【参考方案6】:

您可以使用宏,它们适用于任何类型,编译器将静态检查扩展代码。缺点是代码密度(二进制)会变差,而且更难调试。

我前段时间问过这个question about generic functions,答案可以帮助你。

【讨论】:

【参考方案7】:

您可以有效地将类型信息、继承和多态性添加到 C 数据结构中,这正是 C++ 所做的。 (http://www.embedded.com/97/fe29712.htm)

【讨论】:

【参考方案8】:

绝对通用void*,绝不重复代码!

考虑到许多 C 程序员和许多主要的 C 项目都考虑过这种困境。我遇到的所有严肃的 C 项目,无论是开源的还是商业的,都选择了通用的void*。当仔细使用并封装到一个好的 API 中时,它对库的用户几乎没有负担。此外,void* 是惯用的 C,直接在 K&R2 中推荐。这是人们期望编写代码的方式,其他任何事情都会令人惊讶并被错误地接受。

【讨论】:

【参考方案9】:

您可以使用 C 构建(某种)OO 框架,但您会错过很多好处……例如编译器可以理解的 OO 类型系统。如果你坚持用类 C 语言做 OO,C++ 是更好的选择。它比 vanilla C 更复杂,但至少你得到了对 OO 的适当语言支持。

编辑:好的……如果你坚持我们不推荐 C++,我建议你不要在 C 中做 OO。快乐吗?就您的 OO 习惯而言,您可能应该考虑“对象”,但将继承和多态性排除在您的实施策略之外。应谨慎使用泛型(使用函数指针)。

编辑 2:实际上,我认为在通用 C 列表中使用 void * 是合理的。它只是尝试使用宏、函数指针、调度和我认为是个坏主意的那种废话来构建一个模拟 OO 框架。

【讨论】:

我并不是在建议我在 C 中实现 OO,而是在问“鉴于我目前的编码习惯,我应该如何考虑/解决这个问题?”。 @Stephen:当有人说你应该或不应该做某事时,总是有一个重要的问题需要回答:为什么? @Secure - 它隐含在答案中。但是要说清楚,因为您最终会使用假语法(宏)和许多非类型安全的东西,这会使您的代码更加脆弱和难以维护。【参考方案10】:

在 Java 中,来自 java.util 包的所有集合实际上都相当于 void* 指针(Object)。

是的,泛型(在 1.5 中引入)添加了语法糖并防止您编写不安全的分配,但是存储类型仍然是 Object

所以,我认为当您将void* 用于泛型框架类型时,不会犯下OO 罪行。

如果您经常在代码中执行此操作,我还会添加特定类型的内联或宏包装器,用于从通用结构中分配/检索数据。

附:您不应该做的一件事是使用 void** 返回已分配/重新分配的泛型类型。如果您检查malloc/realloc 的签名,您将看到您可以实现正确的内存分配,而无需担心void** 指针。我之所以这么说,是因为我在一些开源项目中看到了这一点,我不想在这里命名。

【讨论】:

【参考方案11】:

可以通过一些工作来包装通用容器,以便可以在类型安全的版本中对其进行实例化。这是一个示例,完整的标题链接如下:

/* 通用实现 */

struct deque *deque_next(struct deque *dq);

void *deque_value(const struct deque *dq);

/* Prepend a node carrying `value` to the deque `dq` which may
 * be NULL, in which case a new deque is created.
 * O(1)
 */
void deque_prepend(struct deque **dq, void *value); 

来自可用于实例化特定包装类型的双端队列的标头

#include "deque.h"

#ifndef DEQUE_TAG
#error "Must define DEQUE_TAG to use this header file"
#ifndef DEQUE_VALUE_TYPE
#error "Must define DEQUE_VALUE_TYPE to use this header file"
#endif
#else

#define DEQUE_GEN_PASTE_(x,y) x ## y
#define DEQUE_GEN_PASTE(x,y) DEQUE_GEN_PASTE_(x,y)
#define DQTAG(suffix) DEQUE_GEN_PASTE(DEQUE_TAG,suffix)

#define DQVALUE DEQUE_VALUE_TYPE

#define DQREF DQTAG(_ref_t)

typedef struct 
    deque_t *dq;
 DQREF;

static inline DQREF DQTAG(_next) (DQREF ref) 
    return (DQREF)deque_next(ref.dq);


static inline DQVALUE DQTAG(_value) (DQREF ref) 
    return deque_value(ref.dq);


static inline void DQTAG(_prepend) (DQREF *ref, DQVALUE val) 
    deque_prepend(&ref->dq, val);

deque.h:http://ideone.com/eDNBN deque_gen.h:http://ideone.com/IkJRq

【讨论】:

以上是关于泛型与类型安全?在 C 中使用 void*的主要内容,如果未能解决你的问题,请参考以下文章

java泛型与通配符

java的泛型与反射机制

Java认识泛型与容器

如何在 Kotlin 中将 TypeToken + 泛型与 Gson 一起使用

泛型与枚举

泛型与反射