【问题标题】:Using macros to implement a generic vector in C. Is this a good idea?使用宏在 C 中实现泛型向量。这是个好主意吗?
【发布时间】:2015-01-11 18:17:49
【问题描述】:

我是一名同时了解 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 编写的相同代码的两倍以上 并且 生成的可执行文件更小! (使用 GCC 和 G++ 编译,在两种情况下都使用 -O3)。

我的问题是:

  • 这种方法是否存在严重缺陷?
  • 您是否建议在严肃的项目中使用它?
  • 如果不是,那么我希望您解释一下为什么,并告诉我有什么更好的选择。

【问题讨论】:

  • 如果不知道如何您的宏是如何实现的以及您的预期用例是什么,就不可能回答您的问题。
  • 你为什么不写函数呢?使用宏并没有真正的优势,除非在非常有限的情况下。
  • 我不使用函数的原因是因为函数在 C 中不能是“通用的”。包含源代码是个好主意。我会这样做的,请稍等!
  • I [...]know both C and C++. [...] I do not know which one I prefer. 如果您可以在CC++ 之间进行选择,请使用C++ 对此没有任何疑问。
  • 您的VECTOR_BACK 宏包含一个小而重要的缺陷。

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


【解决方案1】:

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

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

  1. g++ 使用的标准 C++ 库中 new 的实现不是最理想的。如果您将void* operator new (size_t size) 实现为对malloc() 的调用,您将获得比内置版本更好的性能。

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

  3. realloc() 函数是标准 C 库的一部分,它动态链接到您的可执行文件。而std::vector&lt;&gt;生成的内存管理代码恰恰是:生成的。因此,它必须是可执行文件的一部分。

这种方法有什么严重的缺陷吗?

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

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

您会建议在严肃的项目中使用它吗?

我想,我不会打扰。 realloc() 代码很简单,一些复制不会造成太大伤害。但同样,您的里程可能会有所不同。

如果不是,那么我希望您解释原因并告诉我更好的选择。

正如我之前所说,我不会费心尝试以std::vector&lt;&gt; 的风格编写一个通用容器类,既不是通过(ab)使用预处理器,也不是通过(ab)使用void*

但我会仔细研究我编写的系统上的内存处理:对于许多内核,您极不可能从malloc()/@987654351 中获得NULL 的返回值@ 称呼。他们过度投入记忆,做出他们无法确定能够兑现的承诺。当他们意识到他们无法兑现他们的承诺时,他们开始通过 OOM-killer 进行射击。在这样的系统上(Linux 就是其中之一),您的错误处理毫无意义。它永远不会被执行。因此,您可以避免添加它并将其复制到需要动态增长数组的所有位置的痛苦。

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

if(size == capacity) {
    array = realloc(array, (capacity *= 2) * sizeof(*array));
}
array[size++] = ...;

如果没有无功能的错误处理代码,它是如此之短,以至于可以安全地复制它需要的次数。

【讨论】:

  • "我不会费心尝试以 std::vector 的风格编写一个通用容器类"" 你有什么建议呢?如果您想要一个可以包含 int 的容器和另一个可以包含例如用户定义的结构的容器,您会使用什么?
  • @EarlofLemongrab 我会使用我所展示的内容。包含 realloc() 调用和后续 array[size++]if() 语句。这是 C 语言中非常惯用的代码。我还详细说明了为什么我认为这对于最后几段中的绝大多数情况来说已经足够了。
  • 那么更复杂的数据结构呢?
  • @EarlofLemongrab 只要您不在某处存储指向数组元素的指针,realloc() 就可以在任何类型的元素数组上发挥作用。这确实是realloc() 的唯一缺点,它使任何指向可能移动区域的指针无效。就像std::vector&lt;&gt;::push_back() 一样,它会使所有迭代器/指针/对其元素的引用无效。另请注意,我使用*array 作为sizeof() 的参数:这直接从数组的元素类型派生大小。您可以更改array 的类型,而无需更改realloc() 调用。
【解决方案2】:

这种方法有什么严重的缺陷吗?

您正在以一种与 C 的类型系统交互不佳的方式重新设计模板。例如,您的 VECTOR 类型是匿名的,所以我不能编写一个将 VECTOR_OF(int) 作为参数的函数。

即使您确实以某种方式命名您的类型,我也无法编写一个通用的函数---将VECTOR_OF(T) 用于任意T 并对其进行处理的东西.

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

您会建议在严肃的项目中使用它吗?

当然;您可以使用这样的容器类型开发一个严肃的项目,它们甚至不一定会出现在您的面前。您可能需要通过 void * 来传递这些东西,这会导致一些容易出错的转换。

【讨论】:

  • 好答案。我要补充一点,您可以在函数参数中使用typedef VECTOR_OF(blah) Blah_vector;Blah_vector。不过确实很丑。
  • @elias: 但是Blah_vector 与声明为VECTOR_OF(blah) 的东西不兼容。因此,您需要提前为您关心的每种类型“实例化模板”。如果您对自己的名字不小心并且您有多个翻译单元,这可能会导致编译器无法检测到的细微错误。 (基本上,C++ 中存在模板是有原因的,就是这样。)
【解决方案3】:

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

是的,您正在尝试重塑 the wheel

您会建议在严肃的项目中使用它吗?

不,特别是因为您的加速通常表明您可能会错过一些安全检查。

如果不是,那么我希望您解释原因并告诉我更好的选择。

VPool 从上面,或其他类似的东西。如果您搜索“C 可增长缓冲区”,您会在 stackoverflow 和谷歌上找到一些提示

【讨论】:

  • 请注意我的反对意见,但是:bsdua.org 的 vpool 是面向字节的抽象,而问题中提供的向量代码处理字节流以外的其他类型。这并不是说 vpool 不能使用;它只是不等同于上面的矢量代码。
  • 我怀着沉重的心情接受你的反对,@JonathanLeffler ;) 你所说的绝对正确;所以他坚持使用其他实现之一
  • 请原谅——'Note' 应该是 'Not'(哎呀!)。不过,其余部分仍然准确。
  • 嗯,那是一种解脱,然后 :) 我希望 OP 找到一个经过良好测试的实现来满足遇到的需求——很确定在 linux 内核中甚至有几次类似的东西(其中至少会让它成为一种同行评审)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-02
  • 1970-01-01
  • 2010-09-08
  • 1970-01-01
  • 2011-11-07
  • 1970-01-01
相关资源
最近更新 更多