在C语言中是否可以创建通用函数?

6

我重新开始使用C语言,但是我已经被其他语言中的泛型所宠坏了。在我实现可调整大小的数组时,我已经写出了以下的代码:

typdef struct {
  void** array;
  int length;
  int capacity;
  size_t type_size;
} Vector;

void vector_add(Vector* v, void* entry) {
  // ... code for adding to the array and resizing
}

int main() {
  Vector* vector = vector_create(5, sizeof(int));
  vector_add(vector, 4); // This is erroneous...
  // ...
}

在我试图让这个代码通用化的过程中,我现在无法将整数添加到向量中而不将其存储在其他地方的内存中。是否有办法使这个代码运行(无论是原样还是可能的更好的泛型方法)?

1
@Jason:据我所知,C 是类型安全的,除了类型转换和联合体。他可以通过“邪恶”的宏使用来实现这一点。 - SLaks
1
你的 type_size 字段似乎是多余的,因为你只能存储 void* 大小的数据。你可以选择存储任意固定一致大小的内存块,而不是存储指针。 - Ben Voigt
@BenVoigt 谢谢,我没有意识到。 - sdasdadas
@Stu 这样的问题是我了解语言边界的方式。你的评论没有帮助(在几个方面上)。 - sdasdadas
如果你只是把这个当做一个学术练习,我很抱歉。我刚刚度过了一天非常糟糕的时间,调试了一个由一个边学边写 C++ 的人编写的 DLL(并且失败得很惨)。因此,我的第一反应是:“如果我在生产代码中看到这个,我会找到你并伤害你。” - Stu
显示剩余2条评论
9个回答

7
对于我的回答,我假设您不熟悉内存部分(即使用存储池的方式)。
在尝试使其通用化时,如果要将整数添加到向量中,则无法直接实现,必须将其存储到其他位置的内存中
如果您想创建一个通用结构体(如您所做),则需要使用void指针。因此,从使用void指针开始,您需要在内存池上存储每个字段的值,或者不常见的是在堆栈上存储。请注意,该结构体由void指针组成,因此仅包含内存地址,指向内存中的其他位置, 其中包含这些值。
如果您将它们声明在堆栈上,请注意一旦从调用堆栈中弹出堆栈帧,这些内存地址就不再有效,因此可能被另一个堆栈帧使用(覆盖在该内存地址集合内已有的值)。 附:如果您迁移到C ++,则可以考虑使用C++模板。

我无法指出单独错误的句子,但你所推断的结论是错误的。 值需要被存储在某个地方,但这不一定是“别处”。 - Ben Voigt
@BenVoigt,我的回答是假设他不理解内存的部分。我看到你的困惑了,但我已经让它更清晰了。 - Jacob Pollack

2
是的,您可以采用格林斯潘第十法则,在C语言中开发一个完整的动态语言,并在此过程中开发一个相对干净的C运行时,可从C语言内部使用。
这个项目中,我就是这样做的,之前也有其他人这样做过。
在该项目的C运行时中,会从C数字创建一个通用数字,如下所示:
val n = num(42);

由于val的表示方式,它只占用一个机器字。几位类型标记被用来区分数字、指针、字符等。
另外还有这个:
val n = num_fast(42);

这是英文原文的翻译:

这个宏(位操作宏)要快得多,因为它没有进行任何特殊检查以确保数字42适合“fixnum”范围;它用于小整数。

一个将其参数添加到向量的每个元素的函数可以像这样编写(非常低效):

val vector_add(val vec, val delta)
{
   val iter;
   for (iter = zero; lt(iter, length(vec)); iter = plus(iter, one)) {
      val *pelem = vecref_l(vec, iter);
      *pelem = plus(*pelem, delta);
   }
   return nil;
}

由于plus是通用的,因此它可以与fixnums、bignums和实数一起使用,也可以与字符一起使用,因为通过plus可以向字符添加整数位移。
类型不匹配错误将被低级函数捕获并转换为异常。例如,如果vec不是可以应用length的东西,length将抛出异常。
带有_l后缀的函数返回一个位置。而vecref(v, i)返回向量v中偏移量i处的值,vecref_l(v, i)返回存储该值的val类型位置的指针。
这都是C语言,只不过ISO C规则有点弯曲:在严格符合C规范的情况下,无法有效地创建像val这样的类型,但您可以在支持的体系结构和编译器中相当可移植地完成。
我们的vector_add不够通用。有可能做得更好:
val sequence_add(val vec, val delta)
{
   val iter;
   for (iter = zero; lt(iter, length(vec)); iter = plus(iter, one)) {
      val elem = ref(vec, iter);
      refset(vec, iter, plus(elem, delta));
   }
   return nil;
}

通过使用通用的refrefset,现在它也适用于列表和字符串,而不仅仅是向量。我们可以这样做:
val str = string(L"abcd");
sequence_add(str, num(2));

变量 str 的内容将会发生改变,因为每个字符都会被原地加上位移量 2,最终结果为 cdef

1

使用一些非标准的GNU C扩展,可以定义具有推断参数类型的通用函数。该宏在语句表达式中定义了一个嵌套函数并使用typeof推断参数类型:

#include <stdio.h>

#define fib(n1) ({\
        typeof(n1) func(typeof(n1) n){\
            if (n <= 1)\
              return n;\
            return func(n-1) + func(n-2);\
        }\
        func(n1);\
    })

int main()
{
    printf("%d\n",fib(3));
    printf("%f\n",fib(3.0));
    return 0;
}

1
你的想法可以实现:

int *new_int = (int*)malloc(sizeof(int));
*new_int = 4;
vector_add(vector, new_int);

自然而然,编写一个int *create_int(int x)函数或类似函数是一个好主意:

int *create_int(int x)
{
    int *n = (int*)malloc(sizeof(int));
    *n = 4;
    return n;
}
//...
vector_add(vector, create_int(4));

如果您的环境允许,您可以考虑使用一个经过充分测试、广泛使用的库来管理所有这些,例如Glib。或者甚至是C++。

1
不要强制转换malloc的返回值。 - Kevin
谢谢,但这对于存储一个数字来说开销太大了(对于程序员而言)。这只是一个学习练习,但有时候挑战一下语言的限制也是好事。 - sdasdadas

1

您可以通过存储数据而不是指向数据的指针来避免许多小的分配,例如:

typedef struct {
  char* array;
  int length;
  int capacity;
  size_t type_size;
} Vector;

bool vector_add(Vector* v, void* entry)
{
    if (v->length < v->capacity || vector_expand(v)) {
        char* location = v->array + (v->length++)*(v->type_size);
        memcpy(location, entry, v->type_size);
        return 1;
    }
    return 0; // didn't fit
}

int main()
{
    Vector* vector = vector_create(5, sizeof(int));
    int value = 4;
    vector_add(vector, &value); // pointer to local is ok because the pointer isn't stored, only used for memcpy
}

请注意,在 C 语言中,我们习惯将星号与对象放在一起,而不是类型:bool vector_add(Vector *v, void *entry) - Jens
@Jens:我使用了与问题相同的约定。 - Ben Voigt
@BenVoigt 这对于存储大对象可能不是很好,对吧? - sdasdadas
@sdasdadas:这实际上取决于向量需要增长的频率。如果您可以预估大小,或者您只填充一次并且之后会经常读取它,那么按顺序存储对象可能比使用指针具有更好的性能,因为缓存可以更有效地使用。 - Ben Voigt

1

是的,这是我的一个实现(与你的类似),可能会有所帮助。它使用可以用函数调用包装的宏来处理即时值。

#ifndef VECTOR_H
# define VECTOR_H

# include <stddef.h>
# include <string.h>

# define VECTOR_HEADROOM 4

/* A simple library for dynamic
 * string/array manipulation
 *
 * Written by: Taylor Holberton
 * During: July 2013
 */

struct vector {
    void * data;
    size_t  size, len;
    size_t  headroom;
};

int vector_init (struct vector *);

size_t vector_addc  (struct vector *, int index, char c);
size_t vector_subc  (struct vector *, int index);

// these ones are just for strings (I haven't yet generalized them)
size_t vector_adds (struct vector *, int index, int iend, const char * c);
size_t vector_subs (struct vector *, int ibegin, int iend);

size_t vector_addi (struct vector *, int index, int i);
size_t vector_subi (struct vector *, int index);

# define vector_addm(v, index, datatype, element)                        \
do {                                                                    \
    if (!v) return 0;                                               \
                                                                    \
    if (!v->size){                                                  \
            v->data = calloc (v->headroom, sizeof (datatype));      \
            v->size = v->headroom;                                  \
    }                                                               \
                                                                    \
    datatype * p = v->data;                                         \
                                                                    \
    if (v->len >= (v->size - 2)){                                   \
            v->data = realloc (v->data,                             \
                    (v->size + v->headroom) * sizeof (datatype));   \
            p = v->data;                                            \
            memset (&p[v->size], 0, v->headroom * sizeof(datatype));\
            v->size += v->headroom;                                 \
    }                                                               \
                                                                    \
    if ((index < 0) || (index > v->len)){                           \
            index = v->len;                                         \
    }                                                               \
                                                                    \
    for (int i = v->len; i >= index; i--){                          \
            p[i + 1] = p[i];                                        \
    }                                                               \
                                                                    \
    p[index] = element;                                             \
                                                                    \
    v->len++;                                                       \
                                                                    \
} while (0)


# define vector_subm(v, index, datatype)                                 \
do {                                                                    \
    if (!v || !v->len){                                             \
            return 0;                                               \
    }                                                               \
                                                                    \
    if ((index < 0) || (index > (v->len - 1))){                     \
            index = v->len - 1;                                     \
    }                                                               \
                                                                    \
    datatype * p = v->data;                                         \
                                                                    \
    for (int i = index; i < v->len; i++){                           \
            p[i] = p[i + 1];                                        \
    }                                                               \
                                                                    \
    v->len--;                                                       \
                                                                    \
    if ((v->size - v->len) > v->headroom){                          \
            v->data = realloc (v->data, ((v->size - v->headroom) + 1) * sizeof (datatype));\
            v->size -= v->headroom;                                 \
    }                                                               \
                                                                    \
} while (0)

#endif

通常我会像这样包装它们:

size_t vector_addi (struct vector * v, int index, int i){
    vector_addm (v, index, int, i);
    return v->len;
}

我还没有进行代码审查,但是我已经在我正在编写的一个大程序中使用它,并且没有出现内存错误(使用 valgrind)。
唯一真正缺少的东西(我一直想要添加)是能够将数组加减到数组中。 编辑: 我相信你也可以使用stdarg.h来做同样的事情,但我从未尝试过。

可能我理解有误,但是看起来你仍然有不同的函数用于添加字符和整数。 - sdasdadas
1
@sdasdadas 函数名不同,但它们使用相同的宏。您可以选择使用宏而不是函数调用,但这可能会占用更多的内存。 - tay10r
1
@sdasdadas 你可能也想要了解一下 stdarg.h,因为我认为你也可以用它来实现。它允许你使用任意数量的参数而没有特定的类型。 - tay10r

1
你想要更好的方法?这里有一个:https://github.com/m-e-leypold/glitzersachen-demos/tree/master/generix/v0-2011(披露:这是我的代码)。
让我简单地解释一下:
  • 我希望有类型安全的通用容器(其他语言会提供正确的泛型(Ada)或参数多态(OCaml))。这是 C 语言中最缺失的功能。

  • 宏无法实现这一点(我不打算详细解释。简单地说:模板扩展或通用实例化的结果应该是一个独立的模块:在 C 中,这意味着有预处理器符号导出或可用于模块配置(如 -DUSE_PROCESS_QUEUE_DEBUGCODE),如果使用 C 宏生成实例,就无法做到这一点。

  • 通过将元素大小和所有相关操作移入描述性结构来抽象元素类型。这将传递给每个通用代码调用。请注意,描述符描述元素类型,因此每个通用实例需要一个描述符实例。

  • 我正在使用模板处理器创建一个薄的类型安全前端模块,用于通用代码。

例如:

这是检索元素的通用代码原型:

void fifo_get ( fifo_DESCRIPTOR* inst, fifo* , void* var );

这是描述符类型:
typedef struct fifo_DESCRIPTOR {
  size_t maxindex;
  size_t element_size;
} fifo_DESCRIPTOR;

这是类型安全包装器模板中的模板代码:
<<eT>>  <<>>get  ( <<T>>* f ) { 
   <<eT>> e; fifo_get( &DESCRIPTOR, (fifo*) f, (void*) &e ); return e; 
}

这是模板扩展器(实例化一个通用模板)从模板中生成的内容:
float   floatq_get  ( floatq* f ) { 
    float e; fifo_get( &DESCRIPTOR, (fifo*) f, (void*) &e ); return e; 
}

所有这些都有很好的集成,但在实例化时几乎没有类型安全。每个错误只在使用cc编译时出现。
我目前无法证明为什么要坚持使用C中的源文本模板而不是迁移到C ++。对我来说,这只是一个实验。
问候。

1
这种方法可能会让你感到恐惧,但如果你不需要任何类型特定的逻辑,它可以被实现。
// vector.h
#ifndef VECTOR_H
#define VECTOR_H

#define VECTOR_IMP(itemType) \
   typedef struct {          \
      itemType * array;      \
      int length;            \
      int capacity;          \
   } itemType##_Vector;      \
                             \
   static inline void itemType##_vector_add(itemType##_Vector* v, itemType v) { \
      // implementation of adding an itemType object to the array goes here     \
   }                                                                            \
                                                                                \
   [... other static-inline generic vector methods would go here ...]           \

// Now we can "instantiate" versions of the Vector struct and methods for
// whatever types we want to use.
VECTOR_IMP(int);
VECTOR_IMP(float);
VECTOR_IMP(char);

#endif

"...以及一些调用代码示例:"
#include "vector.h"

int main(int argc, char ** argv)
{
   float_Vector fv = {0};
   int_Vector iv = {0};
   char_Vector cv = {0};

   int_vector_add(&iv, 5);
   float_vector_add(&fv, 3.14f);
   char_vector_add(&cv, 'A');

   return 0;
}

0

不要让向量类存储添加的对象,而是可以返回指针以便调用者可以将其存储到指定位置:

typdef struct {
    char *buffer;
    size_t length;
    size_t capacity;
    size_t type_size;
} Vector;

void *vector_add(Vector* v)
{
    if (v->length == v->capacity) {
        // ... increase capacity by at least one
        // ... realloc buffer to capacity * type_size
    }
    return v->buffer + v->type_size * v->length++;
}

// in main:
*(int*)vector_add(v) = 4;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接