C++中的STL非常实用,很多人都觉得非常好用,甚至有不少人将C++视为C + STL来使用而不是当初的C with class。但是是不是为了使用STL就必须要使用C++呢?我这里讲述一种在C中利用匿名结构体来实现容器的方法,这样就没有必要单纯为了容器而“使用”使用C++。本文主要讲如何利用匿名结构体实现vector容器,但是对于其它的容器(list、deque等)其实参照本文介绍的方法也很容易实现,就不再一一赘述了。
首先我们要了解的是匿名结构体。
匿名结构体其实就是没有名字的结构体,也就是定义struct的时候没有给出struct的名字。虽然这是句废话,但介于匿名结构体这个技巧在实际中很少见,所以还是要专门说明。
接下来我们就用匿名结构体来定义vector:
#define VECTOR(type) struct { type *vec; size_t sz, cnt; }
这里的type在实际使用中可以是任意类型,包括匿名结构体定义的类型,所以我们可以使用嵌套的vector。并且由于在C中struct是可以赋值的,而且相当于“浅拷贝”。
和具名结构体的变量访问其中的数据域方式一样,匿名结构体的变量也只需要给出相应数据域的名称即可访问。这仍是句废话,但介于匿名结构体这个技巧在实际中很少见,所以还是要专门说明。
这里有一点需要指出:VECTOR定义时type的值实际上已经被编译器“存”在了vec上,所以之后再也不需要从外部传入type了,这也是采用匿名结构体来实现容器相比于纯粹使用void *来实现的好处,这样就和C++容器使用方法的风格一致。(写到这里我就不得不吐槽一下OpenCV里Mat的at还要传入矩阵元素的类型)
然后要出场的就是“构造”、和“析构”函数了:
#define VECTOR_INIT_SZ 1 #define MAKE_VECTOR(v) \ do { \ (v).vec = malloc(VECTOR_INIT_SZ * sizeof((v).vec[0])); \ (v).sz = VECTOR_INIT_SZ; \ (v).cnt = 0; \ } while (0) #define DESTROY_VECTOR(v) \ do { \ free((v).vec); \ (v).vec = NULL; \ (v).sz = VECTOR_INIT_SZ; \ (v).cnt = 0; \ } while (0)
用do { ... } while (0)将实际操作的语句包裹起来是一种常见的保证宏行为符合预期的方法。显然我们写的宏都只能直接用于VECTOR对应的值,而不能用于对VECTOR的指针。
这里之所以引入VECTOR_INIT_SZ是为了方便实现之后的PUSH等操作,这里先不细讲。
再要写的就是用于访问VECTOR内部数据的工具宏函数,以在提供对VECTOR内部数据信息完整的访问能力的同时避免暴露VECTOR的数据结构。
对于VECTOR,通常来说只要有了begin()、end()、rbegin()、rend()、at()、elem_cnt()这6个函数就好了:
#define VECTOR_ITERATOR(v) __typeof((v).vec) #define VECTOR_BEGIN(v) (&(v).vec[0]) #define VECTOR_END(v) (&(v).vec[(v).cnt]) #define VECTOR_RBEGIN(v) (&(v).vec[(v).cnt - 1]) #define VECTOR_REND(v) (&(v).vec[-1]) #define VECTOR_AT(v, i) ((v).vec[i]) #define VECTOR_ELEM_CNT(v) ((v).cnt)
为了能够使用变量来迭代vector,我们不仅要定义begin()、end()、rbegin()、rend(),当然还需要定义迭代器的类型。所以这里用了__typeof()这个GNU的C扩展。不过这个扩展也被clang兼容,所以实际上并不会带来太大的困扰。
注意到VECTOR_AT()可以作为左值,所以不仅可以用来获取相应的值,也可以用于设置相应的值。不过对于VECTOR_ELEM_CNT(),最好别用作左值,如果需要对vector的元素个数进行操作,还是用之后介绍的PUSH等操作来吧。
现在就来介绍用于修改vector元素个数的操作,通常来说就是push()、rpush()、pop()、rpop()、resize()、clear()这6个。push()和rpush()一个是从尾部追加元素,一个是从头部追加元素,所以出于性能方面考虑,要慎用vector的rpush()。pop()和rpop()也是类似的。
#define MIN(a, b) ({ __typeof(a) _a = a; __typeof(b) _b = b; _a < _b ? _a : _b; })
#define ELEM_TYPE(v) __typeof(*(v).vec)
#define VECTOR_PUSH(v, value) \
do { \
if (VECTOR_ELEM_CNT(v) == (v).sz) { \
(v).sz <<= 1; \
(v).vec = realloc((v).vec, (v).sz * ELEM_SZ(v)); \
assert((v).vec != NULL); \
} \
VECTOR_AT((v), (v).cnt) = value; \
(v).cnt++; \
} while (0)
#define VECTOR_RPUSH(v, value) \
do { \
if (VECTOR_ELEM_CNT(v) == (v).sz) { \
(v).sz <<= 1; \
(v).vec = realloc((v).vec, (v).sz * ELEM_SZ(v)); \
assert((v).vec != NULL); \
} \
for (size_t i = (v).cnt; i >= 1; i--) { \
VECTOR_AT((v), i) = VECTOR_AT((v), .i - 1); \
} \
VECTOR_AT((v), 0) = value; \
(v).cnt++; \
} while (0)
#define VECTOR_POP(v) \
({ \
assert(VECTOR_ELEM_CNT(v) > 0); \
ELEM_TYPE(v) temp = VECTOR_AT((v), (v).cnt - 1); \
(v).cnt--; \
temp; \
})
#define VECTOR_RPOP(v) \
({ \
assert(VECTOR_ELEM_CNT(v) > 0); \
ELEM_TYPE(v) temp = VECTOR_BEGIN(v); \
for (size_t i = 0; i < (v).cnt - 1; i++) { \
VECTOR_AT((v), i) = VECTOR_AT((v), i + 1); \
} \
temp; \
})
#define VECTOR_RESIZE(v, new_sz) \
do { \
assert(new_sz >= VECTOR_INIT_SZ); \
(v).sz = new_sz; \
(v).vec = realloc((v).vec, (v).sz * ELEM_SZ(v)); \
assert((v).vec != NULL); \
(v).cnt = MIN((v).cnt, (v).sz); \
} while (0)
#define VECTOR_CLEAR(v) \
do { \
(v).sz = VECTOR_INIT_SZ; \
(v).vec = realloc((v).vec, (v).sz * ELEM_SZ(v)); \
assert((v).vec != NULL); \
(v).cnt = 0; \
} while (0)
使用({ ... })来包住一系列的语句用于获取返回值和__typeof()一样是GNU对C进行的扩展,且同样被clang所兼容。
这样我们就基本完成了一个c_vector.h。不过更为完善的做法是为这些宏再写出对应的C++实现,然后用#ifdef __cplusplus和#else将这两种实现分开,最后别忘了#endif。这样的好处是:当你开始一个项目的时候,你可以采用C并配合c_vector.h甚至还有c_list.h、c_deque.h等来编写程序,但是你保留了切换到“原生”C++的能力,将来如果项目需要改用C++,你只需要将那些.c改为.cc/.cpp/.cxx并修改默认的编译器,于是天然的你的程序就变成了使用C++ STL的程序。
这种做法当然不是我的首创,在很多开源项目里都可以看到类似的实现,比如:h2o。