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

发布于 2024-08-14 20:00:22 字数 555 浏览 12 评论 0原文

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

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

代码的复杂性是一个明显的因素:遍历数组或链表很简单,向结构添加 *next 也不需要额外的工作;在这些情况下,不尝试重用结构和代码是有意义的。但对于更复杂的结构,答案并不那么明显。

还有模块化和可测试性:将类型及其操作与使用该结构的代码分开可以使测试更容易。反之亦然:在结构上测试某些代码的迭代,同时尝试做其他事情会变得混乱。

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

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

Coming from OO (C#, Java, Scala) I value very highly the principles of both code reuse and type-safety. Type arguments in the above languages do the job and enable generic data structures which are both type-safe and don't 'waste' code.

As I get stuck into C, I'm aware that I have to make a compromise and I'd like it to be the right one. Either my data structures have a void * in each node / element and I lose type safety or I have to re-write my structures and code for each type I want to use them with.

The complexity of the code is an obvious factor: iterating through an array or a linked-list is trivial and adding a *next to a struct is no extra effort; in these cases it makes sense not to try and re-use structures and code. But for more complicated structures the answer isn't so obvious.

There's also modularity and testability: separating out the type and its operations from the code that uses the structure makes testing it easier. The inverse is also true: testing the iteration of some code over a structure whilst it's trying to do other things gets messy.

So what's your advice? void * and reuse or type-safety and duplicated code? Are there any general principles? Am I trying to force OO onto procedural when it won't fit?

Edit: Please don't recommend C++, my question is about C!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

迟到的我 2024-08-21 20:00:22

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

尽可能多地从 glib 获取提示,我发现它们的数据结构非常好且简单使用起来,并且由于类型安全性的丧失而遇到了一些麻烦。

I would say use void * so you can re-use the code. It's more work to re-implement e.g. a linked list, than to make sure you get/set the data in the list properly.

Take as many hints from glib as possible, I find their data structures very nice and easy to use, and have had little trouble because of the loss of type safety.

故人的歌 2024-08-21 20:00:22

我认为你必须在两者之间取得平衡,就像你建议的那样。如果代码只有几行而且很琐碎,我会复制它,但如果它更复杂,我会考虑使用 void* 以避免在多个地方进行任何潜在的错误修复和维护,并且以减少代码大小。

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

I think you'll have to strike a balance between the two, just as you suggest. If the code is only a few lines and trivial I would duplicate it but if it's more complex, I would consider working with void* to avoid having to do any potential bug fixing and maintenance in several places and also to reduce the code size.

If you look at the C runtime library, there's several "generic" functions that work with void*, one common example is sorting with qsort. It would be madness to duplicate this code for every type you'd like to sort.

莫多说 2024-08-21 20:00:22

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

There's nothing wrong with using void pointers. You don't even have to cast them when assigning them to a variable of type of pointer since the conversion is done internally. It migtht be worth having a look at this: http://www.cpax.org.uk/prg/writings/casting.php

夏末染殇 2024-08-21 20:00:22

这个问题的答案与在 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);

等等等等

The answer this question is the same as getting efficient templates for link list in C++.

a) Create an abstract version of the algorithm that uses void* or some Abstracted Type

b) Create a light weight public interface to call the Abstracted Type algorithms and caste between them.

For example.

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);

etc etc.

情深已缘浅 2024-08-21 20:00:22

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

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

要点是仅当您不关心类型时才使用 void *。

现在,如果您有 50 个具有相同基本结构的类型(比方说,int a; int b; 作为所有类型的第一个成员),并且想要一个函数对这些类型进行操作,只需将公共第一个成员本身设置为一个类型即可,然后让函数接受这个,并传递 object->ab 或 (AB*)object 如果你的类型是不透明的,如果 ab 是结构中的第一个字段,则两者都可以工作。

We use OO in C a lot here, but only for encapsulation and abstraction, no polymorphism or so.

Which means we have specific types, like FooBar(Foo a, ...) but, for our collection "classes", we use void *. Just use void * where multiple types could be used, BUT, by doing so, ensure you don't need the argument to be of a specific type. As per collection, having void * is alright, because the collection doesn't care about the type. But if your function can accept type a and type b but none other, make two variants, one for a and one for b.

The main point is to use a void * only when you don't care about the type.

Now, if you have 50 types with the same base structure (let's say, int a; int b; as first members of all types), and want a function to act upon those types, just make the common first members a type by itself, then make the function accept this, and pass object->ab or (AB*)object is your type is opaque, both will work if ab is the first field in your struct.

呆萌少年 2024-08-21 20:00:22

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

我问了这个关于泛型函数的问题一些不久前,答案可以帮助你。

You can use macros, they will work with any type and the compiler will check statically the expanded code. The downside is that the code density (in the binary) will worsen and they are more difficult to debug.

I asked this question about generic functions some time ago and the answers could help you.

衣神在巴黎 2024-08-21 20:00:22

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

You can efficiently add type information, inheritance and polymorphism to C data structures, that's what C++ does. (http://www.embedded.com/97/fe29712.htm)

抹茶夏天i‖ 2024-08-21 20:00:22

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

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

Definitely generic void*, never duplicate code!

Take into account that this dilemma was considered by many a C programmer, and many major C projects. All serious C projects I've ever encountered, whether open-source or commercial, picked the generic void*. When used carefully and wrapped into a good API, it is barely a burden on the user of the library. Moreover, void* is idiomatic C, recommended directly in K&R2. It is the way people expect code to be written, and anything else would be surprising and badly accepted.

你没皮卡萌 2024-08-21 20:00:22

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

编辑:好的...如果您坚持认为我们不推荐 C++,我建议您不要在 C 中进行 OO。高兴吗?就您的面向对象习惯而言,您可能应该从“对象”的角度来思考,但将继承和多态性排除在您的实现策略之外。应谨慎使用通用性(使用函数指针)。

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

You can build a (sort of) OO framework using C, but you miss out on a lot of the benefits ... like an OO type system that the compiler understands. If you insist on doing OO in a C-like language, C++ is a better choice. It is more complicated than vanilla C, but at least you get proper linguistic support for OO.

EDIT: Ok ... if you insist that we don't recommend C++, I recommend that you don't do OO in C. Happy? As far as your OO habits are concerned, you should probably think in terms of "objects", but leave inheritance and polymorphism out of your implementation strategy. Genericity (using function pointers) should be used sparingly.

EDIT 2: Actually, I think that use of void * in a generic C list is reasonable. It is just trying to build an mock OO framework using macros, function pointers, dispatching and that kind of nonsense that I think is a bad idea.

策马西风 2024-08-21 20:00:22

在 Java 中,java.util 包中的所有集合实际上都持有等价的 void* 指针(Object)。

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

因此,我认为当您使用 void* 作为通用框架类型时,并没有犯下面向对象的罪行。

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

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

In Java all collections from java.util package in effect hold equivalent of void* pointer ( the Object ).

Yes, generics ( introduced in 1.5 ) add syntactic sugar and prevent you from coding unsafe assignments, however the storage type remains Object.

So, I think there is no OO crime commited when you use void* for generic framework type.

I would also add type-specific inlines or macro wrappers that assign/retrieve data from the generic structures if you do this often in your code.

P.S. The one thing that you should NOT do is to use void** to return allocated/reallocated generic types. If you check the signatures of malloc/realloc you will see that you can achieve correct memory allocations without dreaded void** pointer. I am only telling this because I've seen this in some open-source project, that I do not wish to name here.

时光是把杀猪刀 2024-08-21 20:00:22

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

/* genericimplementation */

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); 

来自可用于实例化 deque 的特定包装类型的标头 deque.h

#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);
}

A generic container can be wrapped with a little work so that it can be instantiated in type-safe versions. Here is an example, full headers linked below:

/* generic implementation */

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); 

From the header that can be used to instantiate specific wrapped types of deque

#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);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文