Here's a proof of concept I've been working on that leans heavily on void * and callbacks. It's not perfeshunal kwality by any stretch, but it should be illustrative.
First, the node type itself:
enum linkType { LINK_PREV, LINK_NEXT, LINK_LEFT, LINK_RIGHT, LINK_PARENT, NUM_LINKS }; struct node { void *key; void *data; struct node *link[NUM_LINKS]; };
The intent is that this node type can be used for any type of container - list, tree, queue, stack, etc., so instead of distinct members for next, previous, left, right, etc., I just create a bunch of enumeration constants to identify the link type and array of links indexed by those enumerations. If I need to add more link types, I just update the enumeration and rebuild. Even through "prev/next" and "left/right" are semantically similar, I leave them distinct. As I play with this I may change that.
This node type is not directly exposed to any client code; I keep the implementation hidden behind the following interface:
#ifndef NODE_H #define NODE_H #include <stdio.h> typedef struct node Node; Node *nodeCreate( const void *key, const void *data, void *(*kcopy)(const void *), void *(*dcopy)(const void *) ); void nodeDestroy( Node *n, void (*kfree)(void *), void (*dfree)(void *) ); /** * Instead of exposing the node definition directly, I only expose functions * to operate on nodes. * * As I play with more container types I will likely add more traversal functions, * but this is as far as I've gotten. */ Node *nodeNext( const Node *n ); Node *nodePrev( const Node *n ); Node *nodeLeft( const Node *n ); Node *nodeRight( const Node *n ); Node *nodeParent( const Node *n ); Node *nodeInsertBefore( Node *toInsert, Node *current ); Node *nodeInsertAfter( Node *toInsert, Node *current ); Node *nodeInsertLeft( Node *toInsert, Node *parent ); Node *nodeInsertRight( Node *toInsert, Node *parent ); Node *nodeRemoveBefore( Node *current ); Node *nodeRemoveAfter( Node *current ); Node *nodeRemoveLeft( Node *current ); Node *nodeRemoveRight( Node *current ); const void *nodeKey( const Node *n ); void *nodeData( const Node *n ); void nodeDump( const Node *n, FILE *stream ); #endif
This code relies on you supplying the following callbacks:
kcopy - allocates memory for and assigns contents of your key
dcopy - allocates memory for and assigns contents of your data
kfree - deallocates memory for the key
dfree - deallocates memory for your data.
The node creation function is pretty simple:
Node *nodeCreate( const void *key, const void *data, void *(*kcopy)(const void *), void *(*dcopy)(const void *) ) { assert( key && "nodeCreate: key parameter cannot be NULL" ); assert( kcopy && "nodeCreate: kcopy parameter cannot be NULL" ); Node *n = malloc( sizeof *n ); if ( n ) { n->key = kcopy( key ); if ( data && dcopy ) n->data = dcopy( data ); memset( n->link, 0, sizeof *n->link * NUM_LINKS ); } return n; }
The resources allocated for the node, node key, and node data are owned by the container.
I've similarly built a list type; in addition to tracking the list head and tail nodes, it stores all the callbacks necessary to create, free, and compare nodes:
struct list { Node *head, *tail; size_t count; void *(*kcpy)(const void *); void *(*dcpy)(const void *); void (*kfree)(void *); void (*dfree)(void *); int (*kcmp)(const void *, const void *); };
In addition to the key and data allocation and deallocation functions, we need a callback to compare key values; by convention, it will return an integer value < 0 if the first argument is "less than" (ordered before) the second argument, > 0 if the first argument is "greater than" (ordered after) the second argument, or = 0 if the arguments are "equal" (have the same ordering).
Again, like the node type, I do not directly expose this list type, I hide it behind the following interface:
#ifndef LIST_H #define LIST_H #include <stdbool.h> #include <stdio.h> #include "node.h" typedef struct list List; List *listCreate( void *(*kcopy)(const void *), void *(*dcopy)(const void *), void (*kfree)(void *), void (*dfree)(void *), int (*kcmp)(const void *, const void *) ); void listClear( List *l ); void listDestroy( List *l ); size_t listSize( List *l ); Node *listInsert( List *l, const void *key, const void *data ); Node *listInsertAt( List *l, const void *key, const void *data, size_t pos ); Node *listPush( List *l, const void *key, const void *data ); Node *listAppend( List *l, const void *key, const void *data ); bool listFindPos( List *l, const void *key, size_t *pos ); bool listContains( List *l, const void *key ); Node *listFind( List *l, const void *key ); Node *listFindAt( List *l, size_t pos ); Node *listRemove( List *l, const void *key ); Node *listRemoveAt( List *l, size_t pos ); Node *listPop( List *l ); Node *listPopTail( List *l ); void listDump( List *l, FILE *stream ); #endif
The listCreate function allocates memory for the list object and stores all the associated callbacks:
List *listCreate( void *(*kcpy)(const void *), void *(*dcpy)(const void *), void (*kfree)(void *), void (*dfree)(void *), int (*kcmp)(const void *, const void * ) ) { assert( kcpy && "listCreate: kcpy cannot be NULL" ); assert( kfree && "listCreate: kfree cannot be NULL" ); assert( kcmp && "listCreate: kcmp cannot be NULL" ); List *l = malloc( sizeof *l ); if ( l ) { l->count = 0; l->kcpy = kcpy; l->dcpy = dcpy; l->kfree = kfree; l->dfree = dfree; l->kcmp = kcmp; int i = 0; /** * head and tail are dummy nodes that don't actually store * any data, they're just there to mark the beginning * and ending of the list. They store an int key by default, * regardless of what actual key type is going to be. */ l->head = nodeCreate( &i, NULL, createInt, NULL ); l->tail = nodeCreate( &i, NULL, createInt, NULL ); nodeInsertBefore( l->head, l->tail ); } return l; }
The various insertion, push, and append routines create the node using the specified key and data and the stored callbacks:
/** * Inserts new item in key order */ Node *listInsert( List *l, const void *key, const void *data ) { assert( l && "listInsert: l cannot be NULL" ); assert( key && "listInsert: key cannot be NULL" ); Node *n = nodeCreate( key, data, l->kcpy, l->dcpy ); if ( n ) { Node *cur; for ( cur = nodeNext( l->head ); cur != l->tail && l->kcmp( nodeKey( cur ), key ) < 0; cur = nodeNext( cur ) ) ; if ( !nodeInsertBefore( n, cur ) ) { nodeDestroy( n, l->kfree, l->dfree ); n = NULL; } else l->count++; } return n; } /** * Inserts new item at specified location; fails if location is greater * than list size. */ Node *listInsertAt( List *l, const void *key, const void *data, size_t pos ) { assert( l && "listInsertAt: l cannot be NULL" ); assert( key && "listInsertAt: key cannot be NULL" ); if ( pos > l->count ) return NULL; /** * Uses the callbacks stored with the list object to create copies of the key * and data. */ Node *n = nodeCreate( key, data, l->kcpy, l->dcpy ); if ( n ) { Node *cur = l->head; for ( size_t i = 0; i < pos; i++ ) cur = nodeNext( cur ); if ( !nodeInsertBefore( n, cur ) ) { nodeDestroy( n, l->kfree, l->dfree ); n = NULL; } else l->count++; } return n; }
Small proof of concept - I create several lists:
one that only stores integers; the key is the data, so the data callbacks are left NULL;
one that stores strings keyed by integers;
one that stores integers keyed by strings;
So I need to create the callbacks:
void *copyInt( const void *data ) { const int *ldata = data; int *p = malloc( sizeof *p ); if ( p ) *p = *ldata; return p; } int cmpInt( const void *l, const void *r ) { const int *ll = l; const int *lr = r; if ( *ll < *lr ) return -1; else if ( *ll > *lr ) return 1; return 0; } void *copyStr( const void *s ) { char *ls = malloc( strlen( (const char *) s ) + 1 ); if ( ls ) strcpy( ls, (const char *) s ); return ls; } int cmpStr( const void *l, const void *r ) { return strcmp( (const char *) l, (const char *) r ); }
Then create my various lists:
List *l = listCreate( copyInt, NULL, free, NULL, cmpInt ); List *li = listCreate( copyInt, copyStr, free, free, cmpInt ); List *ls = listCreate( copyStr, copyInt, free, free, cmpStr );
I generate some random values and strings, load them into the lists in key order:
for ( size_t i = 0; i < count; i++ ) { int val = rand() % 1000; char buf[16]; Node *n = listInsert( l, &val, NULL ); Node *ni = listInsert( li, &val, randomStr( buf, sizeof buf ) ); Node *ns = listInsert( ls, buf, &val ); if ( !n || !ni || !ns ) { fputs( " Error inserting value\n", stderr ); break; } }
Then print the elements out, popping them off the lists as I go:
Node *n = NULL; puts( " l list contents: " ); while ( (n = listPop(l)) ) { printf( "\t value: %5d\n", *(int *) nodeKey( n ) ); nodeDestroy( n, free, NULL ); } puts( " li list contents: " ); while( (n = listPop(li)) ) { printf( "\t key: %5d; value \"%s\"\n", *(int *) nodeKey( n ), (char *) nodeData( n ) ); nodeDestroy( n, free, free ); }; puts( " ls list contents: " ); while ( (n = listPop(ls)) ) { printf( "\t key: \"%s\"; value: %d\n", (char *) nodeKey( n ), *(int *) nodeData( n ) ); nodeDestroy( n, free, free ); }
Output:
$ ./listtest l list size: 10 li list size: 10 ls list size: 10 l list contents: value: 13 value: 54 value: 60 value: 211 value: 245 value: 303 value: 383 value: 478 value: 607 value: 731 li list contents: key: 13; value "g agadabedfeee " key: 54; value "gfe bgb c e efe" key: 60; value "bbdcddffbbfecf " key: 211; value "daddgbfgc ecgab" key: 245; value "bafbaddebac ffe" key: 303; value "eag b cbec cag" key: 383; value "gcbagffbgabf gd" key: 478; value "baaff d bceeafd" key: 607; value "gdcgfafbace d e" key: 731; value " ea facfefeec" ls list contents: key: " ea facfefeec"; value: 731 key: "baaff d bceeafd"; value: 478 key: "bafbaddebac ffe"; value: 245 key: "bbdcddffbbfecf "; value: 60 key: "daddgbfgc ecgab"; value: 211 key: "eag b cbec cag"; value: 303 key: "g agadabedfeee "; value: 13 key: "gcbagffbgabf gd"; value: 383 key: "gdcgfafbace d e"; value: 607 key: "gfe bgb c e efe"; value: 54
So, in theory, this code could handle any arbitrary key and data types; you just need to supply the appropriate callbacks.
One drawback (of many) with this approach is you can't pass literal values to the various insertion or search functions; you must always create some kind of object and pass its address. You'd probably want to create a type-aware front end for your specific use case and have it call this code:
Node *myListInsert( List *l, int key, char *data ) { return listInsert( l, &key, data ); } ... n = myListInsert( mylist, 42, "spam spam spam spam spam" );
With all the callbacks and the unused link types in every node this isn't the most efficient approach, but if you wanted efficient you wouldn't be creating a generic container type in the first place.