【问题标题】:Is it possible to create generic functions in C?是否可以在 C 中创建泛型函数?
【发布时间】:2013-08-08 20:04:08
【问题描述】:

我正在重新开始使用 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...
  // ...
}

为了使这个通用化,我现在无法将整数添加到向量而不将其存储在其他地方的内存中。

有什么方法可以使这项工作(原样,或者可能是更好的泛型方法)?

【问题讨论】:

  • @Jason:AFAIK,C 是类型安全的,除了强制转换和联合。他可以通过邪恶使用宏来实现这一点。
  • @Jason 我已经使用宏完成了它(正如 SLaks 所暗示的那样),但是如果我非常需要它,我宁愿选择使用 C++
  • 您的type_size 字段似乎是多余的,因为您只能存储void* 大小的数据。您当然可以存储任意固定且一致大小的内存块,而不是存储指针。
  • @BenVoigt 谢谢,我还没有意识到。
  • @SLaks:抱歉,我的评论方式太简洁了。我的意思是使用void*

标签: c arrays generics


【解决方案1】:

对于我的回答,我假设您不熟悉内存的各个部分(即内存池的使用)。

为了使这个通用化,我现在无法在不将整数存储在内存中的其他位置的情况下将整数添加到向量中。

如果您想创建一个通用结构(如您所做的那样),那么您将需要使用 void 指针。因此,通过使用 void 指针,您将需要将每个字段的值存储在内存池中,或者在不常见的情况下存储在堆栈中。请注意,该结构由 void 指针组成,因此 只有 内存地址包含在该结构中,指向内存中值所在的 其他 位置。

如果您在堆栈上声明它们请小心,因为一旦您的堆栈帧从调用堆栈中弹出,这些内存地址就被认为是无效的,因此可能会被另一个堆栈帧使用(覆盖该集合中的现有值)内存地址)。

旁白:如果您迁移到 C++,那么您可以考虑使用 C++ 模板。

【讨论】:

  • 我不能单独指出一个错误的句子,但你推断的结论是错误的。值需要存储在某个地方,但不一定是“其他地方”。
  • @BenVoigt,我的回答是假设他不理解内存部分。我看到你的困惑,但我说得更清楚了。
【解决方案2】:

是的;你可以拥抱Greenspun's Tenth Rule 并用 C 开发一门成熟的动态语言,并在此过程中开发一个可以在 C 内部使用的相对干净的 C 运行时。

this project 中,我就是这样做的,就像我之前的其他人一样。

在这个项目的 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 和reals 以及字符,因为可以通过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 的内容将更改为cdef,因为2 的位移被原地添加到每个字符。

【讨论】:

    【解决方案3】:

    你的想法可以实现:

    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++。

    【讨论】:

    【解决方案4】:

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

    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:我使用了与问题相同的约定。
    • @BenVoigt 这可能不太适合存储大型对象?
    • @Jens 谢谢你,注意到了。
    • @sdasdadas:这实际上取决于向量必须增长的频率。如果可以提前预估大小,或者填充一次然后大量读取,顺序存储对象可以比指针有更好的性能,因为缓存的使用效率更高。
    【解决方案5】:

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

    #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 函数名称不同,但它们调用使用相同的宏。您也可以使用宏而不是函数调用,但它可能会占用更多内存。
    • @sdasdadas 您可能还想阅读stdarg.h,因为我认为您也可以这样做。它允许您使用任意数量的没有特定类型的参数。
    【解决方案6】:

    您要求更好的方法?这里是:https://github.com/m-e-leypold/glitzersachen-demos/tree/master/generix/v0-2011(披露:这是我的代码)。

    让我简短地解释一下:

    • 我想要类型安全的泛型容器(在其他语言中将由适当的泛型 (Ada) 或参数多态性 (OCaml) 提供。这是 C 中最缺少的特性。

    • 宏无法做到(我不是 将详细解释。可以说:模板扩展的结果或 泛型实例化本身应该是一个模块:在 C 中,这意味着,有 pre 分别导出的处理器符号可用于模块配置(如 -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; 
    }
    

    所有这些都具有很好的 make 集成,但在实例化中几乎没有任何类型安全性。每个错误只有在使用 cc 编译时才会出现。

    目前我无法证明为什么要坚持使用 C 中的源文本模板而不是迁移到 C++。对我来说,这只是一个实验。

    问候。

    【讨论】:

      【解决方案7】:

      这种方法可能会让你感到害怕,但如果你不需要任何类型专用逻辑,它就可以工作:

      // 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;
      }
      

      【讨论】:

        【解决方案8】:

        您可以只返回一个指向调用者可以存储它的位置的指针,而不是让向量类存储添加的对象:

        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;
        

        【讨论】:

          【解决方案9】:

          使用一些非标准的GNU C extensions,可以定义具有推断参数类型的泛型函数。该宏在statement expression 中定义了nested function,并使用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;
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-04-25
            • 2020-03-31
            • 1970-01-01
            相关资源
            最近更新 更多