开发者

Is there something in C like C++ templates? If not, how to re-use structures and functions for different data types?

开发者 https://www.devze.com 2023-04-04 20:40 出处:网络
I want to write a linked list that can have the data field store any build-in or user-define types. In C++ I would just use a template, but how do I accomplish this in C?

I want to write a linked list that can have the data field store any build-in or user-define types. In C++ I would just use a template, but how do I accomplish this in C?

Do I have to re-write the linked list struct and a bunch of operations of it for each data type I want it to store? Unions wouldn'开发者_如何学JAVAt work because what type can it store is predefined.


There's a reason people use languages other than C.... :-)

In C, you'd have your data structure operate with void* members, and you'd cast wherever you used them to the correct types. Macros can help with some of that noise.


There are different approaches to this problem:

  • using datatype void*: these means, you have pointers to memory locations whose type is not further specified. If you retrieve such a pointer, you can explicitly state what is inside it: *(int*)(mystruct->voidptr) tells the compiler: look at the memory location mystruct->voidptr and interpret the contents as int.

  • another thing can be tricky preprocessor directives. However, this is usually a very non-trivial issue:

  • I also found http://sglib.sourceforge.net/

Edit: For the preprocessor trick:

 #include <stdio.h>

 #define mytype(t) struct { t val; }

 int main(int argc, char *argv[]) {
   mytype(int) myint;
   myint.val=6;
   printf ("%d\n", myint.val);
   return 0;
 }

This would be a simple wrapper for types, but I think it can become quite complicated.


It's less comfortable in C (there's a reason C++ is called C incremented), but it can be done with generic pointers (void *) and the applocation handles the type management itself.

A very nice implementation of generic data structures in C can be found in ubiqx modules, the sources are definitely worth reading.


With some care, you can do this using macros that build and manipulate structs. One of the most well-tested examples of this is the BSD "queue" library. It works on every platform I've tried (Unix, Windows, VMS) and consists of a single header file (no C file).

It has the unfortunate downside of being a bit hard to use, but it preserves as much type-safety as it can in C.

The header file is here: http://www.openbsd.org/cgi-bin/cvsweb/src/sys/sys/queue.h?rev=1.34;content-type=text%2Fplain, and the documentation on how to use it is here: http://www.openbsd.org/cgi-bin/man.cgi?query=queue.

Beyond that, no, you're stuck with losing type-safety (using (void *) all over the place) or moving to the STL.


Here's an option that's very flexible but requires a lot of work.

In your list node, store a pointer to the data as a void *:

struct node {
  void *data;
  struct node *next;
};

Then you'd create a suite of functions for each type that handle tasks like comparison, assignment, duplication, etc.:

// create a new instance of the data item and copy the value
// of the parameter to it.
void *copyInt(void *src)
{
  int *p = malloc(sizeof *p);
  if (p) *p = *(int *)src;
  return p;
}

void assignInt(void *target, void *src)
{
  // we create a new instance for the assignment
  *(int *)target = copyInt(src);
}

// returns -1 if lhs < rhs, 0 if lhs == rhs, 1 if lhs > rhs
int testInt(void *lhs, void *rhs)
{
  if (*(int *)lhs < *(int *)rhs) return -1;
  else if (*(int *)lhs == *(int *)rhs) return 0;
  else return 1;
}

char *intToString(void *data)
{
  size_t digits = however_many_digits_in_an_int();
  char *s = malloc(digits + 2); // sign + digits + terminator
  sprintf(s, "%d", *(int *)data);
  return s;
}

Then you could create a list type that has pointers to these functions, such as

struct list {
  struct node *head;
  void *(*cpy)(void *);          // copy operation
  int   (*test)(void *, void *); // test operation
  void  (*asgn)(void *, void *); // assign operation
  char *(*toStr)(void *);        // get string representation
  ...
}

struct list myIntList;
struct list myDoubleList;

myIntList.cpy = copyInt;
myIntList.test = testInt;
myIntList.asgn = assignInt;
myIntList.toStr = intToString;

myDoubleList.cpy = copyDouble;
myDoubleList.test = testDouble;
myDoubleList.asgn = assignDouble;
myDoubleList.toStr = doubleToString;
...

Then, when you pass the list to an insert or search operation, you'd call the functions from the list object:

void addToList(struct list *l, void *value)
{
  struct node *new, *cur = l->head;
  while (cur->next != NULL && l->test(cur->data, value) <= 0)
    cur = cur->next;
  new = malloc(sizeof *new);
  if (!new)
  {
    // handle error here
  }
  else
  {
    new->data = l->cpy(value);
    new->next = cur->next;
    cur->next = new;
    if (logging)
    {
      char *s = l->toStr(new->data);
      fprintf(log, "Added value %s to list\n", s);
      free(s);
    }
  }
}

...
i = 1;
addToList(&myIntList, &i);
f = 3.4;
addToList(&myDoubleList, &f);

By delegating the type-aware operations to separate functions called through function pointers, you now have a list structure that can store values of any type. To add support for new types, you only need to implement new copy, assign, toString, etc., functions for that new type.

There are drawbacks. For one thing, you can't use constants as function parameters (e.g., you can't do something simple like addToList(&myIntList, 1);) -- you have to assign everything to a variable first, and pass the address of the variable (which is why you need to create new instances of the data member when you add it to the list; if you just assigned the address of the variable, every element in the list would wind up pointing to the same object, which may no longer exist depending on the context).

Secondly, you wind up doing a lot of memory management; you don't just create a new instance of the list node, but you also must create a new instance of the data member. You must remember to free the data member before freeing the node. Then you're creating a new string instance every time you want to display the data, and you have to remember to free that string when you're done with it.

Finally, this solution throws type safety right out the window and into oncoming traffic (after lighting it on fire). The delegate functions are counting on you to keep the types straight; there's nothing preventing you from passing the address of a double variable to one of the int handling functions.

Between the memory management and the fact that you must make a function call for just about every operation, performance is going to suffer. This isn't a fast solution.

Of course, this assumes that every element in the list is the same type; if you're wanting to store elements of different types in the same list, then you're going to have to do something different, such as associate the functions with each node, rather than the list overall.


I wrote a generic linked list "template" in C using the preprocessor, but it's pretty horrible to look at, and heavily pre-processed code is not easy to debug.

These days I think you'd be better off using some other code generation tool such as Python / Cog: http://www.python.org/about/success/cog/


I agree with JonathanPatschke's answer that you should look at sys/queue.h, although I've never tried it myself, as it is not on some of the platforms I work with. I also agree with Vicki's answer to use Python.

But I've found that five or six very simple C macros meet most of my garden-variety needs. These macros help clean up ugly, bug-prone code, without littering it with hidden void *'s, which destroy type-safety. Some of these macros are:

#define ADD_LINK_TO_END_OF_LIST(add, head, tail)                    \
   if (!(head))                                                     \
      (tail) = (head) = (add);                                      \
   else                                                             \
      (tail) = (tail)->next = (add)

#define ADD_DOUBLE_LINK_TO_END_OF_LIST(add, head, tail)             \
   if (!(head))                                                     \
      (tail) = (head) = (add);                                      \
   else                                                             \
      (tail) = ((add)->prev = (tail), (tail)->next = (add))

#define FREE_LINK_IN_LIST(p, dtor) do {  /* singly-linked */        \
   void *myLocalTemporaryPtr = (p)->next;                           \
   dtor(p);                                                         \
   (p) = myLocalTemporaryPtr;} while (0)

#define FREE_LINKED_LIST(p, dtor) do {                              \
   while (p)                                                        \
      FREE_LINK_IN_LIST(p, dtor);} while (0)

// copy "ctor" (shallow)
#define NEW_COPY(p) memcpy(myMalloc(sizeof *(p)), p, sizeof *(p))

// iterator
#define NEXT_IN_LIST(p, list)  ((p) ? (p)->next : (list))

So, for example:

struct MyContact {
   char *name;
   char *address;
   char *telephone;
   ...
   struct MyContact *next; 
} *myContactList = 0, *myContactTail; // the tail doesn't need to be init'd
...
struct MyContact newEntry = {};
...
ADD_LINK_TO_END_OF_LIST(NEW_COPY(newEntry), myContactList, myContactTail);
...
struct MyContact *i = 0;
while ((i = NEXT_IN_LIST(i, myContactList))) // iterate through list
   // ...

The next and prev members have hard-coded names. They don't need to be void *, which avoids problems with strict anti-aliasing. They do need to be zeroed when the data item is created.

The dtor argument for FREE_LINK_IN_LIST would typically be a function like free, or (void) to do nothing, or another macro such as:

#define MY_CONTACT_ENTRY_DTOR(p)                                    \
     do { if (p) {                                                  \
        free((p)->name);                                            \
        free((p)->address);                                         \
        free((p)->telephone);                                       \
        free(p);                                                    \
     }} while (0)

So for example, FREE_LINKED_LIST(myContactList, MY_CONTACT_ENTRY_DTOR) would free all the members of the (duck-typed) list headed by myContactList.

There is one void * here, but perhaps it could be removed via gcc's typeof.


If you need a list that can hold elements of different types simultaneously, e.g. an int followed by three char * followed by a struct tm, then using void * for the data is the solution. But if you only need multiple list types with identical methods, the best solution depends on if you want to avoid generating many instances of almost identical machine code, or just avoid typing source code.

A struct declaration doesn't generate any machine code...

struct int_node {
    void *next;
    int data;
};

struct long_node {
    void *next;
    long data;
};

...and one single function which uses a void * parameter and/or return value, can handle them all.

struct generic_node {
    void *next;
};

void *insert(void *before_this, void *element, size_t element_sizes);
0

精彩评论

暂无评论...
验证码 换一张
取 消