使用宏在C中实现泛型向量。这是一个好主意吗?

时间:2015-01-11 18:17:49

标签: c vector data-structures macros c-preprocessor

我是一名了解C和C ++的程序员。我在自己的项目中使用了这两种语言,但我不知道我喜欢哪种语言。

当我在C中编程时,我最想从C ++中获取的功能是来自STL(标准模板库)的std::vector

我仍然无法弄清楚我应该如何在C中代表不断增长的数组。到目前为止,我在我的项目中重复了我的内存分配代码。我不喜欢代码重复,我知道这是不好的做法所以这对我来说似乎不是一个很好的解决方案。

前段时间我考虑过这个问题,想出了使用预处理器宏实现泛型向量的想法。

这是实现的方式:

#ifndef VECTOR_H_
#define VECTOR_H_

#include <stdlib.h>
#include <stdio.h>

/* Declare a vector of type `TYPE`. */
#define VECTOR_OF(TYPE) struct { \
    TYPE *data; \
    size_t size; \
    size_t capacity; \
}

/* Initialize `VEC` with `N` capacity. */
#define VECTOR_INIT_CAPACITY(VEC, N) do { \
    (VEC).data = malloc((N) * sizeof(*(VEC).data)); \
    if (!(VEC).data) { \
        fputs("malloc failed!\n", stderr); \
        abort(); \
    } \
    (VEC).size = 0; \
    (VEC).capacity = (N); \
} while (0)

/* Initialize `VEC` with zero elements. */
#define VECTOR_INIT(VEC) VECTOR_INIT_CAPACITY(VEC, 1)

/* Get the amount of elements in `VEC`. */
#define VECTOR_SIZE(VEC) (VEC).size

/* Get the amount of elements that are allocated for `VEC`. */
#define VECTOR_CAPACITY(VEC) (VEC).capacity

/* Test if `VEC` is empty. */
#define VECTOR_EMPTY(VEC) ((VEC).size == 0)

/* Push `VAL` at the back of the vector. This function will reallocate the buffer if
   necessary. */
#define VECTOR_PUSH_BACK(VEC, VAL) do { \
    if ((VEC).size + 1 > (VEC).capacity) { \
        size_t n = (VEC).capacity * 2; \
        void *p = realloc((VEC).data, n * sizeof(*(VEC).data)); \
        if (!p) { \
            fputs("realloc failed!\n", stderr); \
            abort(); \
        } \
        (VEC).data = p; \
        (VEC).capacity = n; \
    } \
    (VEC).data[VECTOR_SIZE(VEC)] = (VAL); \
    (VEC).size += 1; \
} while (0)

/* Get the value of `VEC` at `INDEX`. */
#define VECTOR_AT(VEC, INDEX) (VEC).data[INDEX]

/* Get the value at the front of `VEC`. */
#define VECTOR_FRONT(VEC) (VEC).data[0]

/* Get the value at the back of `VEC`. */
#define VECTOR_BACK(VEC) (VEC).data[VECTOR_SIZE(VEC) - 1]

#define VECTOR_FREE(VEC) do { \
    (VEC).size = 0; \
    (VEC).capacity = 0; \
    free((VEC).data); \
} while(0)

#endif /* !defined VECTOR_H_ */

此代码位于名为"vector.h"的头文件中。

请注意,它确实错过了一些功能(例如VECTOR_INSERTVECTOR_ERASE),但我认为展示我的概念已经足够了。

矢量的使用如下所示:

int main()
{
    VECTOR_OF(int) int_vec;
    VECTOR_OF(double) dbl_vec;
    int i;

    VECTOR_INIT(int_vec);
    VECTOR_INIT(dbl_vec);

    for (i = 0; i < 100000000; ++i) {
        VECTOR_PUSH_BACK(int_vec, i);
        VECTOR_PUSH_BACK(dbl_vec, i);
    }

    for (i = 0; i < 100; ++i) {
        printf("int_vec[%d] = %d\n", i, VECTOR_AT(int_vec, i));
        printf("dbl_vec[%d] = %f\n", i, VECTOR_AT(dbl_vec, i));
    }

    VECTOR_FREE(int_vec);
    VECTOR_FREE(dbl_vec);

    return 0;
}

它使用与std::vector相同的分配规则(大小以1开头,然后每次需要时加倍)。

令我惊讶的是,我发现此代码的运行速度是使用std::vector 编写的相同代码的两倍,而生成的可执行文件更小! (在两种情况下都使用-O3使用GCC和G ++编译)。

我的问题是:

  • 这种做法是否存在严重缺陷?
  • 您是否建议在严肃的项目中使用此功能?
  • 如果没有,那么我希望您解释为什么并告诉我更好的替代方案。

3 个答案:

答案 0 :(得分:10)

  

令我惊讶的是,我发现这段代码的运行速度是使用std :: vector编写的相同代码的两倍,并生成一个较小的可执行文件! (在两种情况下使用-O3使用GCC和G ++编译)。

C版本比C ++版本更快/更小的原因有三个:

  1. new使用的标准C ++库中g++的实现不是最理想的。如果您将void* operator new (size_t size)实施为malloc()的号召,则效果会比使用内置版本更好。

  2. 如果realloc()必须使用新的内存块,它会以memmove()的方式移动旧数据,i。即它忽略了数据的逻辑结构,只是简单地移动位。该操作可以很容易地加速到内存总线是瓶颈的程度。另一方面,std::vector<>必须正确地调用构造函数/析构函数,它只能调用memmove()。如果intdouble归结为一次移动数据int / double,则循环位于为{{1}生成的代码中}}。这不是太糟糕,但它比使用良好std::vector<>实现的SSE指令更糟糕。

  3. memmove()函数是标准C库的一部分,它与您的可执行文件动态链接。但是,realloc()生成的内存管理代码恰恰是:生成的。因此,它必须是可执行文件的一部分。

  4.   

    这种方法有严重缺陷吗?

    这是一个品味问题,但我认为,这种方法很臭:你的宏定义远离它们的用途,并且它们并不都像简单的常量或内联函数。事实上,它们可疑地像编程语言的元素(即模板),这对预处理器宏来说不是一件好事。尝试使用预处理器修改语言通常是个坏主意。

    您的宏实现也存在严重问题:std::vector<>宏四次评估其VECTOR_INIT_CAPACITY(VEC, N)参数,两次评估VEC参数。现在考虑一下如果进行调用会发生什么N:存储在新向量的VECTOR_INIT_CAPACITY(foo, baz++)字段中的大小将大于为其分配的内存大小。 capacity调用的行将增加malloc()变量,并且在baz第二次递增之前,新值将存储在capacity成员中。您应该以一种只评估其参数一次的方式编写所有宏。

      

    您是否建议在严肃的项目中使用此功能?

    我想,我不会打扰。 baz代码非常简单,以至于某些复制不会造成太大的伤害。但同样,你的里程可能会有所不同。

      

    如果没有,那么我希望你解释原因并告诉我什么是更好的选择。

    正如我之前所说,我不打算尝试以realloc()的方式编写一般的容器类,既不使用(ab)使用预处理器,也不使用(ab)使用{{1 }}

    但是我会仔细研究一下我编写的系统上的内存处理:对于很多内核,你很可能从std::vector<>获得void*的返回值} / NULL来电。他们过度承诺他们的记忆,做出他们无法肯定能够实现的承诺。当他们意识到他们无法支持他们的承诺时,他们开始通过OOM杀手开始射击过程。在这样的系统上(linux就是其中之一),你的错误处理毫无意义。它永远不会被执行。因此,您可以避免添加它并将其复制到需要动态增长数组的所有位置的痛苦。

    我在C中的内存重新分配代码通常如下所示:

    malloc()

    如果没有无功能的错误处理代码,这个代码很短,可以根据需要安全地复制多次。

答案 1 :(得分:5)

  

这种方法有严重缺陷吗?

您正在以与C&#type型系统交互不良的方式重新发明模板。例如,您的VECTOR类型是匿名的,因此我无法编写以VECTOR_OF(int)作为参数的函数。

即使你以某种方式命名你的类型,我也不能写一个通用的函数 - 一个VECTOR_OF(T)任意T的东西用它做点什么。

这些可能不是严重的错误,但是我在C中看到的每一种使用宏的方法都有这样的一个小缺点。这一切都是因为语言没有出现尝试支持通用编程。

  

您是否建议在严肃的项目中使用此功能?

不确定;你可以使用像这样的容器类型开发一个严肃的项目,他们甚至不一定会在你的脸上。您可能需要在void *进行流量传递这些内容,这会导致某些投射容易出错。

答案 2 :(得分:0)

  

我的问题是:      这种方法有严重缺陷吗?

是的,您正在尝试重新发明the wheel

  

您是否建议在严肃的项目中使用此功能?

不,尤其是因为您的加速通常表示您可能缺少某些安全检查。

  

如果没有,那么我希望你解释原因并告诉我什么是更好的选择。

来自上面的VPool,或类似的东西。如果你搜索“C growable buffer”,你会在stackoverflow和google上找到几个提示