具有宏的类型安全的通用容器

时间:2012-02-22 19:55:30

标签: c macros containers generic-collections

我正在尝试使用宏在C中创建一个类型安全的通用链表。它应该与模板在C ++中的工作方式类似。例如,

LIST(int) *list = LIST_CREATE(int);

我的第一次尝试是#define LIST(TYPE)(我上面使用的宏)来定义struct _List_##TYPE {...}。但是,这不起作用,因为每次我声明一个新列表时都会重新定义结构。我通过这样做解决了这个问题:

/* You would first have to use this macro, which will define
   the `struct _List_##TYPE`...                               */
DEFINE_LIST(int);

int main(void)
{
    /* ... And this macro would just be an alias for the struct, it
       wouldn't actually define it.                                  */
    LIST(int) *list = LIST_CREATE(int);
    return 0;
}

/* This is how the macros look like */

#define DEFINE_LIST(TYPE)    \
    struct _List_##TYPE      \
    {                        \
        ...                  \
    }

#define LIST(TYPE)       \
    struct _List_##TYPE

但另一个问题是,当我有多个文件使用DEFINE_LIST(int)时,例如,其中一些文件相互包含,那么同一个结构体仍然会有多个定义。有没有办法让DEFINE_LIST检查结构是否已被定义?

/* one.h */
DEFINE_LIST(int);

/* two.h */
#include "one.h"
DEFINE_LIST(int); /* Error: already defined in one.h */ 

6 个答案:

答案 0 :(得分:9)

我在C ++获取模板之前用C解决了这个问题,我仍然 有代码。

您无法使用宏定义真正通用的类型安全容器-T模板 以一种完全局限于头文件的方式。标准预处理器 没有提供"推动"并且"弹出"您将要进行的宏分配 要求通过嵌套和顺序保持其完整性 扩张的背景。并且您将立即遇到嵌套的上下文 尝试通过定义容器容器T来吃自己的狗粮。

正如我们所见,事情可以做到,但正如@immortal所暗示的,它需要 为您需要的每个T值生成不同的.h.c文件。 例如,您可以使用宏定义一个完全通用的T列表 内联文件,例如list_type.inl,然后将list_type.inl包括在内 每一对小型封面包装 - list_float.hlist_float.c - 都是 将分别定义和实现float-float容器。同样 for list-of-int,list-of-list-of-float,list-of-list-of-double, 等等。

示意图将清楚地表明。但首先要完全衡量 吃你自己的狗粮挑战。

将这样的二阶容器视为一个列表的列表。我们想 能够通过为我们的宏设置T = list-of-thingummy来实例化这些 list-of-T解决方案。但绝不是列出的东西会成为POD 数据类型。无论是物品清单是我们自己的狗食还是其他人,它都是 将成为一个生活在堆上并被表示为的抽象数据类型 其用户通过typedef-ed指针类型。或者至少,它会发生 将动态组件保存在堆上。无论如何,不​​是POD。

这意味着我们的T列表解决方案仅仅被告知这还不够 T = list-of-thingummy。还必须告知T是否需要非POD 复制 - 构建和破坏,如果是这样,如何复制 - 构建和销毁 一。用C表示,这意味着:

  • 复制构造:如何以T大小创建给定T的副本 给定这样一个区域的地址的未提交内存区域。

  • 毁灭:如何在给定地址销毁T。

我们可以在不知道默认构造或构造的情况下完成 非T参数,因为我们可以合理地限制我们的T列表解决方案 从用户提供的原件复制的对象的包含。但是我们这样做 必须复制它们,我们必须处理我们的副本。

接下来,假设我们希望为T-set或T1-to-T2提供模板, 除了T列表。这些按键排序的数据类型添加了另一个参数 我们将不得不插入T或T1的任何非POD值,即如何订购 密钥类型的任何两个对象。实际上我们需要这个参数 memcmp()不会做的任何关键数据类型。

注意到这一点,我们将坚持使用更简单的T列表问题 示意图;为了进一步简化,我会忘记这种可取性 任何const API。

对于这个和任何其他模板容器类型,我们需要一些令牌粘贴 让我们方便地组装函数和类型的标识符的宏, 加上可能还有其他实用程序宏。这些都可以放在标题中,比如macro_kit.h, 如:

#ifndef MACRO_KIT_H
#define MACRO_KIT_H

/* macro_kit.h */

#define _CAT2(x,y) x##y

// Concatenate 2 tokens x and y
#define CAT2(x,y) _CAT2(x,y)
// Concatenate 3 tokens x, y and z
#define CAT3(x,y,z) CAT2(x,CAT2(y,z))

// Join 2 tokens x and y with '_' = x_y
#define JOIN2(x,y) CAT3(x,_,y)
// Join 3 tokens x, y and z with '_' = x_y_z
#define JOIN3(x,y,z) JOIN2(x,JOIN2(y,z))
// Compute the memory footprint of n T's
#define SPAN(n,T)   ((n) * sizeof(T))

#endif

现在转到list_type.inl

的示意结构
//! There is intentionally no idempotence guard on this file
#include "macro_kit.h"
#include <stddef.h>

#ifndef INCLUDE_LIST_TYPE_INL
#error This file should only be included from headers \
that define INCLUDE_LIST_TYPE_INL
#endif

#ifndef LIST_ELEMENT_TYPE
#error Need a definition for LIST_ELEMENT_TYPE
#endif

/* list_type.inl

    Defines and implements a generic list-of-T container
    for T the current values of the macros:

    - LIST_ELEMENT_TYPE: 
        - must have a definition = the datatype (or typedef alias) for \
        which a list container is required.

    - LIST_ELEMENT_COPY_INITOR:
        - If undefined, then LIST_ELEMENT_TYPE is assumed to be copy-
        initializable by the assignment operator. Otherwise must be defined
        as the name of a copy initialization function having a prototype of
        the form:

        LIST_ELEMENT_TYPE * copy_initor_name(LIST_ELEMENT_TYPE *pdest,
                                            LIST_ELEMENT_TYPE *psrc);

        that will attempt to copy the LIST_ELEMENT_TYPE at `psrc` into the
        uncommitted memory at `pdest`, returning `pdest` on success and NULL
        on failure.

        N.B. This file itself defines the copy initializor for the list-type
        that it generates.

    - LIST_ELEMENT_DISPOSE
        If undefined, then LIST_ELEMENT_TYPE is assumed to need no
        destruction. Otherwise the name of a destructor function having a
        protoype of the form:

        void dtor_name(LIST_ELEMENT_TYPE pt*);

        that appropriately destroys the LIST_ELEMENT_TYPE at `pt`.

        N.B. This file itself defines the destructor for the list-type that
        it generates.
*/

/* Define the names of the list-type to generate, 
    e.g. list_int, list_float
*/
#define LIST_TYPE JOIN2(list,LIST_ELEMENT_TYPE)

/* Define the function-names of the LIST_TYPE API.
    Each of the API macros LIST_XXXX generates a function name in
    which LIST becomes the value of LIST_TYPE and XXXX becomes lowercase,
    e.g list_int_new
*/
#define LIST_NEW JOIN2(LIST_TYPE,new)
#define LIST_NODE JOIN2(LIST_TYPE,node)
#define LIST_DISPOSE JOIN2(LIST_TYPE,dispose)
#define LIST_COPY_INIT JOIN2(LIST_TYPE,copy_init)
#define LIST_COPY JOIN2(LIST_TYPE,copy)
#define LIST_BEGIN JOIN2(LIST_TYPE,begin)
#define LIST_END JOIN2(LIST_TYPE,end)
#define LIST_SIZE JOIN2(LIST_TYPE,size)
#define LIST_INSERT_BEFORE JOIN3(LIST_TYPE,insert,before)
#define LIST_DELETE_BEFORE JOIN3(LIST_TYPE,delete,before)
#define LIST_PUSH_BACK JOIN3(LIST_TYPE,push,back)
#define LIST_PUSH_FRONT JOIN3(LIST_TYPE,push,front)
#define LIST_POP_BACK JOIN3(LIST_TYPE,pop,back)
#define LIST_POP_FRONT JOIN3(LIST_TYPE,pop,front)
#define LIST_NODE_GET JOIN2(LIST_NODE,get)
#define LIST_NODE_NEXT JOIN2(LIST_NODE,next)
#define LIST_NODE_PREV JOIN2(LIST_NODE,prev)

/* Define the name of the structure used to implement a LIST_TYPE.
    This structure is not exposed to user code.
*/
#define LIST_STRUCT JOIN2(LIST_TYPE,struct)

/* Define the name of the structure used to implement a node of a LIST_TYPE.
    This structure is not exposed to user code.
*/
#define LIST_NODE_STRUCT JOIN2(LIST_NODE,struct)

/* The LIST_TYPE API... */


// Define the abstract list type
typedef struct LIST_STRUCT * LIST_TYPE;

// Define the abstract list node type
typedef struct LIST_NODE_STRUCT * LIST_NODE;

/* Return a pointer to the LIST_ELEMENT_TYPE in a LIST_NODE `node`,
    or NULL if `node` is null
*/
extern LIST_ELEMENT_TYPE * LIST_NODE_GET(LIST_NODE node);

/* Return the LIST_NODE successor of a LIST_NODE `node`,
    or NULL if `node` is null.
*/ 
extern LIST_NODE LIST_NODE_NEXT(LIST_NODE node);

/* Return the LIST_NODE predecessor of a LIST_NODE `node`,
    or NULL if `node` is null.
*/
extern LIST_NODE LIST_NODE_PREV(LIST_NODE node);


/* Create a new LIST_TYPE optionally initialized with elements copied from
    `start` and until `end`.

    If `end` is null it is assumed == `start` + 1.

    If `start` is not NULL then elements will be appended to the
    LIST_TYPE until `end` or until an element cannot be successfully copied.
    The size of the LIST_TYPE will be the number of successfully copied
    elements. 
*/ 
extern LIST_TYPE LIST_NEW(LIST_ELEMENT_TYPE *start, LIST_ELEMENT_TYPE *end);

/* Dispose of a LIST_TYPE
    If the pointer to LIST_TYPE `plist` is not null and addresses
    a non-null LIST_TYPE then the LIST_TYPE it addresses is
    destroyed and set NULL.
*/ 
extern void LIST_DISPOSE(LIST_TYPE * plist);

/* Copy the LIST_TYPE at `psrc` into the LIST_TYPE-sized region at `pdest`,
    returning `pdest` on success, else NULL.

    If copying is unsuccessful the LIST_TYPE-sized region at `pdest is
    unchanged.
*/
extern LIST_TYPE * LIST_COPY_INIT(LIST_TYPE *pdest, LIST_TYPE *psrc);

/* Return a copy of the LIST_TYPE `src`, or NULL if `src` cannot be
    successfully copied.
*/
extern LIST_TYPE LIST_COPY(LIST_TYPE src);

/* Return a LIST_NODE referring to the  start of the
    LIST_TYPE `list`, or NULL if `list` is null.
*/
extern LIST_NODE LIST_BEGIN(LIST_TYPE list);

/* Return a LIST_NODE referring to the end of the
    LIST_TYPE `list`, or NULL if `list` is null.
*/
extern LIST_NODE LIST_END(LIST_TYPE list);

/* Return the number of LIST_ELEMENT_TYPEs in the LIST_TYPE `list`
    or 0 if `list` is null.
*/
extern size_t LIST_SIZE(LIST_TYPE list);

/* Etc. etc. - extern prototypes for all API functions.
    ...
    ...
*/

/* If LIST_IMPLEMENT is defined then the implementation of LIST_TYPE is
    compiled, otherwise skipped. #define LIST_IMPLEMENT to include this
    file in the .c file that implements LIST_TYPE. Leave it undefined
    to include this file in the .h file that defines the LIST_TYPE API.
*/
#ifdef LIST_IMPLEMENT
// Implementation code now included.

// Standard library #includes...?

// The heap structure of a list node
struct LIST_NODE_STRUCT {
    struct LIST_NODE_STRUCT * _next;
    struct LIST_NODE_STRUCT * _prev;
    LIST_ELEMENT_TYPE _data[1];
};

// The heap structure of a LIST_TYPE
struct LIST_STRUCT {
    size_t _size;
    struct LIST_NODE_STRUCT * _anchor;
};

/* Etc. etc. - implementations for all API functions
    ...
    ...
*/

/*  Undefine LIST_IMPLEMENT whenever it was defined.
    Should never fall through. 
*/
#undef LIST_IMPLEMENT

#endif // LIST_IMPLEMENT 

/*  Always undefine all the LIST_TYPE parameters.
    Should never fall through. 
*/
#undef LIST_ELEMENT_TYPE
#undef LIST_ELEMENT_COPY_INITOR
#undef LIST_ELEMENT_DISPOSE
/* Also undefine the "I really meant to include this" flag. */

#undef INCLUDE_LIST_TYPE_INL

请注意list_type.inl没有针对多重包含的宏观保护。你要 至少其中一些 - 至少是模板API - 每次都包括在内 可见

如果你阅读文件顶部的评论,你可以猜出你将如何编码 用于导入list-of-int容器类型的包装头。

#ifndef LIST_INT_H
#define LIST_INT_H

/* list_int.h*/

#define LIST_ELEMENT_TYPE int
#define INCLUDE_LIST_TYPE_INL
#include "list_type.inl"

#endif

同样如何编码包装头来导入list-of-list-of-int 容器类型:

#ifndef LIST_LIST_INT_H
#define LIST_LIST_INT_H

/* list_list_int.h*/

#define LIST_ELEMENT_TYPE list_int
#define LIST_ELEMENT_COPY_INIT list_int_copy_init
#define LIST_ELEMENT_DISPOSE list_int_dispose
#define INCLUDE_LIST_TYPE_INL
#include "list_type.inl"

#endif 

您的应用程序可以安全地包含此类包装器,例如

#include "list_int.h"
#include "list_list_int.h"

尽管事实上他们以相互冲突的方式定义LIST_ELEMENT_TYPE因为 list_type.inl始终#undefs参数化list-type的所有宏 当它完成它们时:查看文件的最后几行。

另请注意宏LIST_IMPLEMENT的使用。如果list_type.inl时未定义 解析然后只暴露模板API;模板实现是 跳过。如果定义了LIST_IMPLEMENT,则编译整个文件。因此我们的 通过不定义LIST_IMPLEMENT来包装标题,仅导入列表类型 API。

相反,对于我们的包装源文件list_int.clist_list_int.c,我们会 定义LIST_IMPLEMENT。在那之后,除了包括之外没什么可做的 相应的标题:

/* list_int.c */
#define LIST_IMPLEMENT
#include "list_int.h"

/* list_list_int.c*/
#include "list_int.h"
#define LIST_IMPLEMENT
#include "list_list_int.h"

现在,在您的应用程序中,不会出现列表模板宏。你的包装 标题解析为&#34;真实代码&#34;:

#include "list_int.h"
#include "list_list_int.h"
// etc.

int main(void)
{
    int idata[10] = {1,2,3,4,5,6,7,8,9,10};
    //...
    list_int lint = list_int_new(idata,idata + 10);
    //...
    list_list_int llint = list_list_int_new(&lint,0);
    //...
    list_int_dispose(&lint);
    //...
    list_list_int_dispose(&llint);
    //...
    exit(0);
}

为自己配备一个&#34; C模板库&#34;这种方式唯一(!)努力工作 是为每个容器类型编写.inl文件并进行测试 非常,非常彻底。然后,您可能会生成一个目标文件 和本机数据类型和容器类型的每个组合的标题 现成的链接,并快速淘汰.h.c包装 其他类型的需求。

毋庸置疑,一旦C ++萌发模板,我就会对出汗充满热情 他们以这种方式蒸发了。但它完全可以通过这种方式完成 一般来说,如果由于某种原因,C是唯一的选择。

答案 1 :(得分:1)

您总是可以向DEFINE_LIST宏添加第二个参数,以允许您“命名”列表。例如:

#define DEFINE_LIST(TYPE, NAME)          \
struct _List_##TYPE_##NAME               \
{                                        \
    TYPE member_1;                       \
    struct _List_##TYPE_##NAME* next;    \
}

然后你可以这样做:

DEFINE_LIST(int, my_list);
//... more code that uses the "my_list" type

当两个不同的头文件相互包含时,您必须限制自己不重复使用相同的列表“名称”,并且都使用DEFINE_LIST宏。使用LIST_CREATE等时,您还必须按名称引用列表

将列表传递给您编写的函数时,您始终可以创建用户定义的“命名”版本转换为“通用”类型。这不应该影响任何事情,因为struct中的实际信息保持不变,而“name”标签仅仅区分类型与声明而不是二进制立场。例如,这是一个获取存储int类型的列表对象的函数:

#define GENERIC_LIST_PTR(TYPE) struct _generic_list_type_##TYPE*
#define LIST_CAST_PTR(OBJ, TYPE) (GENERIC_LIST_PTR(TYPE))(OBJ)

void function(GENERIC_LIST_PTR(INT) list)
{
    //...use list as normal (i.e., access it's int data-member, etc.)
}

DEFINE_LIST(int, my_list);

int main()
{
    LIST(int, my_list)* list = LIST_CREATE(int, my_list);
    function(LIST_CAST_PTR(list, int));

    //...more code

    return 0;
}

我知道这不一定是最方便的事情,但这确实解决了命名问题,您可以控制在其他用户不会添加的某个私有头文件中创建的struct _generic_list_type_XXX版本to(除非他们希望为自己的类型这样做)...但它是一种将声明和泛型列表类型的定义与实际用户定义的列表类型分开的机制。

答案 2 :(得分:0)

你为什么不使用图书馆? 我喜欢使用GLib,但我讨厌代码中的void指针,为了获得GLib提供的数据类型的类型安全版本,我编写了一些非常简单的宏:

http://pastebin.com/Pc0KsadV

如果我想要一个Symbol *列表(假设它是我之前定义的类型),我只需要:

GLIST_INSTANCE(SymbolList, Symbol*, symbol_list);

如果您不想为简单的链表使用整个库(这可能是一种过度杀伤),请实现一个处理void *的列表,并创建一些函数来封装并进行正确的类型转换。 / p>

答案 3 :(得分:0)

如何创建list_template.h文件,然后为模板的每个实例创建list_TYPE.h文件和list_TYPE.c文件。当然,这些可以带有合适的头保护器。 然后,您只能包含模板标题,但请确保将所有.c文件添加到编译和链接过程中,它应该可以正常工作。

这基本上是C ++为您自动执行的操作...复制实例...

答案 4 :(得分:0)

我真的怀疑你可以在一个宏中检查存在和定义(一个结构)。在#ifndef之前添加另一个DEFINE_LIST(int)支票。这不是优雅,而是做你想要的。

答案 5 :(得分:0)

可以使用宏创建通用和类型安全的容器。从计算理论的角度来看,宏扩展生成的语言(代码)可以通过非确定性下推自动机来识别,这意味着它至多是一个无上下文的语法。前面提到的语句使我们的目标似乎无法实现,因为容器及其附属迭代器应该记住它们包含的类型,但这只能通过上下文敏感的语法来完成。但是,我们可以做一些技巧!

成功的关键在于编译过程,构建符号表。如果在编译器查询表并且没有发生不安全类型转换时可以识别变量类型,那么它被认为是类型安全的。因此,我们必须为每个struct提供一个特殊名称,因为如果在同一级别的范围内声明了两个或更多结构,则结构名称可能会发生冲突。最简单的方法是将当前行号附加到结构名称。自ANSI C(C89 / C90)起,标准C支持预定义的宏__LINE__和宏concatenation / stringification

然后,我们要做的是将一些属性隐藏到我们上面定义的结构中。如果要在运行时创建另一个列表记录,在结构中放置一个指向它自己的指针将实际解决问题。但是,这还不够。我们可能需要一个额外的变量来存储我们在运行时分配的列表记录数。这有助于我们弄清楚当程序员明确销毁列表时如何释放内存。此外,我们可以利用__typeof__()扩展,这在宏编程中被广泛使用。

我是OpenGC3的作者,旨在使用宏构建类型安全的通用容器,这是一个简短的示例,说明了这个库的工作原理:

ccxll(int) list;                      //  declare a list of type int
ccxll_init(list);                     //  initialize the list record

for (int cnt = 8; cnt-- > 0; )        //
    ccxll_push_back(list, rand());    //  insert "rand()" to the end

ccxll_sort(list);                     //  sort with comparator: XLEQ

CCXLL_INCR_AUTO(pnum, list)           //  traverse the list forward:
    printf("num = %d\n", *pnum);      //  access elems through iters

ccxll_free(list);                     //  destroy the list after use

它非常类似于STL的语法。声明list时确定列表类型。我们无需关心类型安全性,因为在列表中执行操作时没有不安全的类型转换。