I am coding in a framework written in C and not compatible with C++. When implementing a data structure for sets as doubly-linked lists, I used macros like these to achieve generics.
#pragma once
/* Interface protocol:
* 1. Do not modify contents of nodes in a list. Results should be regarded
* as returned instead of modified in the arguments. The old list shall
* not be used.
* 2. Use RA_DECL_DL (T) to declare the doubly linked list and related oper
* for type T. If RA_DECL_DL (T) is used in an `.h' file, put it in
* #define GUARD_RA_FL_<T> #ifdef GUARD_RA_FL_<T> ... #endif
* to prevent re-declarations.
* 3. A function int _<T>_eq (T x, T y) must be declared in that file, before
* RA_DECL_DL and RA_DEFN_DL.
* 4. An RA_DL (T) can be used both as a set or a doubly linked list, but not
* simultaneously for a particular instance. */
#define RA_DECL_DL(T) \
typedef struct _##T##_DL_ *T##_DL_t; \
struct _##T##_DL_ \
{ \
T val; \
T##_DL_t next; \
T##_DL_t prev; \
}; \
T##_DL_t _##T##_DL_empty (); \
T##_DL_t _##T##_DL_insert (T x, T##_DL_t lst); \
T##_DL_t _##T##_DL_remove (T x, T##_DL_t lst); \
int _##T##_DL_isIn (T x, T##_DL_t lst); \
int _##T##_DL_isEmpty (T##_DL_t lst); \
T##_DL_t _##T##_DL_union (T##_DL_t l1, T##_DL_t l2); \
T##_DL_t _##T##_DL_intersect (T##_DL_t l1, T##_DL_t l2); \
T##_DL_t _##T##_DL_diff (T##_DL_t l1, T##_DL_t l2);
#define RA_DEFN_DL(T) \
T##_DL_t _##T##_DL_empty () { return NULL; } \
T##_DL_t _##T##_DL_insert (T x, T##_DL_t lst) \
{ \
T##_DL_t ret = checked_malloc (sizeof (struct _##T##_DL_)); \
if (lst == NULL) \
{ \
ret->val = x; \
ret->next = NULL; \
ret->prev = NULL; \
return ret; \
} \
else \
{ \
for (T##_DL_t iter = lst; iter != NULL; iter = iter->next) \
{ \
if (_##T##_eq (iter->val, x)) \
return lst; \
} \
ret->val = x; \
ret->prev = NULL; \
ret->next = lst; \
lst->prev = ret; \
return ret; \
} \
} \
T##_DL_t _##T##_DL_remove (T x, T##_DL_t lst) \
{ \
T##_DL_t iter; \
for (iter = lst; iter != NULL; iter = iter->next) \
{ \
if (_##T##_eq (iter->val, x)) \
break; \
} \
if (iter == NULL) \
return lst; \
if (iter->prev != NULL) \
iter->prev->next = iter->next; \
if (iter->next != NULL) \
iter->next->prev = iter->prev; \
return iter->prev == NULL ? lst->next : lst; \
} \
int _##T##_DL_isIn (T x, T##_DL_t lst) \
{ \
for (T##_DL_t iter = lst; iter != NULL; iter = iter->next) \
{ \
if (_##T##_eq (iter->val, x)) \
return 1; \
} \
return 0; \
} \
T##_DL_t _##T##_DL_union (T##_DL_t l1, T##_DL_t l2) \
{ \
T##_DL_t ret = _##T##_DL_empty (); \
for (T##_DL_t iter = l1; iter != NULL; iter = iter->next) \
ret = _##T##_DL_insert (iter->val, ret); \
for (T##_DL_t iter = l2; iter != NULL; iter = iter->next) \
ret = _##T##_DL_insert (iter->val, ret); \
return ret; \
} \
T##_DL_t _##T##_DL_intersect (T##_DL_t l1, T##_DL_t l2) \
{ \
T##_DL_t ret = _##T##_DL_empty (); \
for (T##_DL_t iter = l1; iter != NULL; iter = iter->next) \
{ \
if (_##T##_DL_isIn (iter->val, l2)) \
ret = _##T##_DL_insert (iter->val, ret); \
} \
return ret; \
} \
T##_DL_t _##T##_DL_diff (T##_DL_t l1, T##_DL_t l2) \
{ \
T##_DL_t ret = _##T##_DL_empty (); \
for (T##_DL_t iter = l1; iter != NULL; iter = iter->next) \
{ \
if (!_##T##_DL_isIn (iter->val, l2)) \
ret = _##T##_DL_insert (iter->val, ret); \
} \
return ret; \
}
#define RA_DL(T) T##_DL_t
#define RA_DL_empty(T) _##T##_DL_empty ()
#define RA_DL_insert(T, x, lst) _##T##_DL_insert (x, lst)
#define RA_DL_remove(T, x, lst) _##T##_DL_remove (x, lst)
#define RA_DL_isIn(T, x, lst) _##T##_DL_isIn (x, lst)
#define RA_DL_union(T, l1, l2) _##T##_DL_union (l1, l2)
#define RA_DL_intersect(T, l1, l2) _##T##_DL_intersect (l1, l2)
#define RA_DL_diff(T, l1, l2) _##T##_DL_diff (l1, l2)
The problem is: if for two files, say f1.c
and f2.c
, I write RA_DECL_DL(int)
and RA_DEFN_DL(int)
in both files, then, for example, the function _int_DL_insert
is defined twice globally. This causes a conflict between identifiers, though they are actually identical.
Using #ifdef
guard macros does not solve this problem because macros are not seen across .c
files. Using static
prevents me using the functions in multiple files, which is sometimes needed.
I concur with @Lundin that this is all much too much macros. As I'm sure you know a generic implementation using C++ templates is simple - the template container
type std::set
is ready-made - but you say your
framework is "not compatible" with C++. However a template-instantiating C++
implementation can expose a C linkage interface using the extern C {....}
language-linkage specifier, and you
need only be able use the same toolchain for C and C++ to be ABI safe.
If C++ with C linkage is for some reason ruled out, let's say your macro header file is DL_macros.h
.
If you are going to control the inventory of types for which your macros are instantiated then you can do what follows.
If you want a user of your macros to be able to employ them for unforeseen
types then you can provide a README
that tells them to do what follows.
For each type Type
for which you want to instantiate the macros,
create a header file with some suitable name, DL_Type.h
like:
#pragma once
#include <Type.h> // Maybe
#include <DL_macros.h>
int _Type_eq (Type x, Type y); // Per your protocol item #3
RA_DECL_DL(Type)
where Type.h
will be the relevant header file defining type Type
if it is a defined and not a fundamental type.
Also create a source file DL_Type.c
like:
#include <DL_Type.h>
int _Type_eq (Type x, Type y)
{
return x == y; // Or whatever is right.
}
RA_DEFN_DL(Type)
Then compile the file DL_Type.c
to an object file DL_Type.o
. Link that object file
with client code that requires the API declared in DL_Type.h
. Such code will #include <DL_Type.h>
.
Client code will reference the unique definitions provided in DL_Type.o
Optionally, you or your users may make a static library from object files instantiating the macro definitions for various types. From such a static library a linker will only link member object files that are referenced by client code.
You might also avoid exposing the RA_DEFN_DL(T)
macro to client code by placing
the RA_DECL_DL(T)
macro alone in the client-facing header, say, DL_macros_decl.h
,
placing the RA_DEFN_DL(T)
in another, say, DL_macros_defn.h
, and including
the latter only in DL_Type.c
:
#include <DL_Type.h>
#include <DL_macros_defn.h>
Optionally too, you might provide conditional support for toolchains, like GCC's,
that recognise weak symbols. The linker
will tolerate multiple definitions of a weak symbol and pick one them arbitrarily (in practice,
the first one seen), so for these toolchains it would be unnecessary for you or your users
to compile and link a distinct DL_Type.o
for each Type
: you could RA_DECL_DL(Type)
and
RA_DEFN_DL(Type)
in the same translation unit, as you first envisaged if you
think that is significantly desirable.
For this option, DL_macros.h
would be modified on the lines:
#ifdef __GNUC__
#define WEAK __attribute__((weak))
#else
#define WEAK
#endif
#define RA_DECL_DL(T) \
typedef struct _##T##_DL_ *T##_DL_t; \
struct _##T##_DL_ \
{ \
T val; \
T##_DL_t next; \
T##_DL_t prev; \
}; \
T##_DL_t _##T##_DL_empty () WEAK; \
T##_DL_t _##T##_DL_insert (T x, T##_DL_t lst) WEAK; \
T##_DL_t _##T##_DL_remove (T x, T##_DL_t lst) WEAK; \
int _##T##_DL_isIn (T x, T##_DL_t lst) WEAK; \
int _##T##_DL_isEmpty (T##_DL_t lst) WEAK; \
T##_DL_t _##T##_DL_union (T##_DL_t l1, T##_DL_t l2) WEAK; \
T##_DL_t _##T##_DL_intersect (T##_DL_t l1, T##_DL_t l2) WEAK; \
T##_DL_t _##T##_DL_diff (T##_DL_t l1, T##_DL_t l2) WEAK;
and the required declaration for the _Type_eq
function would be
int _Type_eq (Type x, Type y) WEAK;
My own preference would be not to bother with differentiating toolchains
that recognize weak symbols and stick universally with compiling a distinct
DL_Type.o
.
@Jonathan Leffler's caution about using names beginning with underscores refers to the fact that names beginning with a double-underscore or an underscore and capital letter are reserved by the compiler implementor in C. Your macros can give rise to names that breach this reservation.