6

我正在尝试使用宏在 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 */ 
4

6 回答 6

11

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

您不能以完全限于头文件的方式使用宏定义真正通用的类型安全容器 T 模板。标准预处理器不提供“推送”和“弹出”您需要的宏分配的方法,以便通过嵌套和顺序的扩展上下文保持它们的完整性。一旦你通过定义一个 container-of-containers-of-T 来尝试吃你自己的狗粮,你就会遇到嵌套的上下文。

正如我们将看到的,这件事可以完成,但正如@immortal 建议的那样,它需要为您需要的每个 T 值生成不同的.h和文件。.c例如,您可以使用内联文件中的宏定义一个完全通用的 T 列表,例如 ,list_type.inl然后包含list_type.inl在一对小的设置包装器中 -list_float.hlist_float.c- 将分别定义和实现列表- 浮动容器。类似地,对于 list-of-int、list-of-list-of-float、list-of-vector-of-list-of-double 等等。

一个示意性的例子会让一切都清楚。但首先要全面了解自己吃狗粮的挑战。

考虑这样一个二阶容器作为 list-of-lists-of-thingummy。我们希望能够通过为我们的宏 list-of-T 解决方案设置 T = list-of-thingummy 来实例化这些。但是 list-of-thingummy 绝不会成为 POD 数据类型。无论 list-of-thingummy 是我们自己的狗粮还是其他人的,它都将是一种抽象数据类型,它存在于堆上,并通过 typedef ed 指针类型表示给它的用户。或者至少,它将在堆上保存动态组件。无论如何,不​​是 POD。

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

  • 复制构造:如何在给定区域的地址的情况下,在 T 大小的未提交内存区域中创建给定 T 的副本。

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

我们可以不知道默认构造或非 T 参数构造,因为我们可以合理地将 T 列表解决方案限制为包含从用户提供的原件复制的对象。但我们确实必须复制它们,而且我们必须处理掉我们的副本。

接下来,假设我们希望除了 list-of-T 之外,还希望为 set-of-T 或 map-of-T1-to-T2 提供一个模板。这些键排序的数据类型添加了另一个参数,我们必须为 T 或 T1 的任何非 POD 值插入,即如何对键类型的任何两个对象进行排序。事实上,我们将需要该参数用于任何memcmp()不会执行的关键数据类型。

注意到这一点后,我们将坚持使用更简单的 T 列表问题作为示意图示例;为了更简单,我会忘记任何constAPI 的可取性。

对于这个和任何其他模板容器类型,我们需要一些标记粘贴宏,让我们方便地组装函数和类型的标识符,可能还有其他实用宏。这些都可以放在标题中,例如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。

如果您阅读文件顶部的注释,您可以猜到您将如何编写一个包装头来导入一个 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_IMPLEMENT。如果它的 undefined whenlist_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"

现在在您的应用程序中,没有出现列表模板宏。您的包装标题解析为“真实代码”:

#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);
}

要以这种方式为自己配备“C 模板库”,唯一的(!)艰苦的工作就是为.inl您想要的每种容器类型编写文件并非常非常彻底地测试它。然后,您可能会为本地数据类型和容器类型的每种组合生成一个目标文件和标头以用于现成的链接,并根据需要为其他类型快速淘汰.h.c包装器。

不用说,当 C++ 出现模板时,我对以这种方式将它们出汗的热情就烟消云散了。但是,如果出于某种原因,C 是唯一的选择,则可以完全通用地这样做。

于 2012-05-03T11:57:00.767 回答
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保持不变,并且“名称”标签只是将类型与声明区分开来,而不是从二进制的角度来看。例如,下面是一个函数,它采用存储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在其他用户不会添加的某些私有头文件中创建的版本(除非他们希望这样做所以对于他们自己的类型)......但这将是一种将通用列表类型的声明和定义与实际用户定义的列表类型分开的机制。

于 2012-02-22T20:12:32.173 回答
0

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

http://pastebin.com/Pc0KsadV

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

GLIST_INSTANCE(SymbolList, Symbol*, symbol_list);

如果您不想将整个库(这将是一种矫枉过正)用于简单的链表,请实现一个处理 void* 的列表并创建一些函数来封装并进行正确的类型转换。

于 2012-02-22T20:05:28.583 回答
0

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

这基本上是 C++ 自动为您所做的......复制实例......

于 2012-02-22T20:07:31.447 回答
0

我真的怀疑您是否可以在一个宏中检查存在和定义(结构)。再把#ifndef支票放在前面DEFINE_LIST(int)。它并不优雅,但可以满足您的需求。

于 2012-02-22T21:30:02.493 回答
0

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

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

然后,我们要做的就是将一些属性隐藏到我们上面定义的结构体中。如果你想在运行时创建另一个列表记录,在结构中放置一个指向自身的指针实际上可以解决问题。然而,这还不够。我们可能需要一个额外的变量来存储我们在运行时分配了多少列表记录。这有助于我们弄清楚当列表被程序员显式销毁时如何释放内存。此外,我们可以利用__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声明时确定。我们不需要关心类型安全,因为在对列表执行操作时没有不安全的类型转换。

于 2017-01-01T17:12:25.813 回答