0

I'm working on some data structures in C. I have created a queue data structure that can take any type of data. This is currently being done by a macro which is default initialized to int type.

#ifndef DATATYPE
#define DATATYPE int
#endif

The queue header is being included in another data structure - the binary search tree, and I am using the queue for a breadth-first-search implementation. In the Makefile, I have modified the DATATYPE macro from an int to a binary_tree_node_t * type.

binary_tree: DEFS=-DDATATYPE="struct BINARY_TREE_NODE *"

My question is, is there a better way to do this using typedefs? Can I define a type DATATYPE as an int in the queue implementation, but have it get modified in a different header file? Or is it possible to implement a queue that can take any datatype?

Here are the source files (redacted for sake of brevity) for reference:

queue.h

#ifndef _QUEUE_H
#define _QUEUE_H 

#ifndef DATATYPE
#define DATATYPE int
#endif

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>


typedef struct LL_NODE {
  DATATYPE data;
  struct LL_NODE* next;
} node_t;


typedef struct QUEUE {
  node_t* head;
  node_t* tail;
  int size;
  
} queue_t;

queue_t* init_queue();

void destroy(queue_t* queue);

bool is_empty(queue_t* queue);

int size(queue_t* queue);

void enqueue(queue_t* queue, DATATYPE data);
DATATYPE dequeue(queue_t* queue);

DATATYPE peek(queue_t* queue);

#endif

binary_search_tree.c

#include "binary_search_tree.h"

void bfs_trav(binary_tree_node_t* root) {
  queue_t* queue = init_queue();
  binary_tree_node_t* temp = root;
  
  enqueue(queue, root);
  while (!is_empty(queue)) {
    temp = dequeue(queue);
    printf("%d ", temp->data);
    if (temp->left) {
      enqueue(queue, temp->left);
    }
    if (temp->right) {
      enqueue(queue, temp->right);
    }
  }
  destroy(queue);
  return;
}

binary_search_tree.h

#ifndef _BINARY_SEARCH_TREE_H
#define _BINARY_SEARCH_TREE_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#include "../queue/queue.h"

typedef struct BINARY_TREE_NODE {
  int data;
  struct BINARY_TREE_NODE *left;
  struct BINARY_TREE_NODE *right;
} binary_tree_node_t;

void bfs_trav(binary_tree_node_t* root);

#endif
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Maslin
  • 139
  • 6
  • Note that you should not, in general, create function, variable, tag or macro names that start with an underscore. Part of [C11 §7.1.3 Reserved identifiers](https://port70.net/~nsz/c/c11/n1570.html#7.1.3) says: — _All identifiers that begin with an underscore and either an uppercase letter or another underscore are always reserved for any use._ — _All identifiers that begin with an underscore are always reserved for use as identifiers with file scope in both the ordinary and tag name spaces._ See also [What does double underscore (`__const`) mean in C?](https://stackoverflow.com/q/1449181) – Jonathan Leffler Jul 17 '22 at 22:12
  • System headers use the 'leading underscore' convention because they're required to do so — they should not trample on the namespaces reserved for users. You should not simulate them because you are breaching the boundaries that allow the system to do what's necessary and that allow you to do what's necessary. – Jonathan Leffler Jul 17 '22 at 22:13
  • You should not include `` or `` in the `queue.h` header; AFAICS, nothing in the header depends on anything in those headers. Contrariwise, you should (and do!) include `` because you use the `bool` type. Your header should be self-contained, idempotent, and minimal. The `` and `` headers make it non-minimal. Similar comments apply to `binary_search_tree.h`, except that it is not clear it should include ``, though it will be included via ``. – Jonathan Leffler Jul 17 '22 at 22:16
  • Also consider the discussion in [What are the benefits of a relative path such as `"../include/header.h"` for a header?](https://stackoverflow.com/q/597318/15168) TL;DR — there aren't many, and you should generally avoid the `../` part. – Jonathan Leffler Jul 17 '22 at 22:20
  • See also [Should I use `#include` in headers?](https://stackoverflow.com/q/1804486/15168) and [Self-sufficient header files in C and C++?](https://stackoverflow.com/q/1892043/15168) – Jonathan Leffler Jul 17 '22 at 22:31
  • And all those comments are somewhat tangential to your main question, which is why they're comments rather than parts of an answer. – Jonathan Leffler Jul 17 '22 at 22:32
  • Shouldn't the line `int data;` in `binary_search_tree.h` use `DATATYPE` instead of `int`? – Jonathan Leffler Jul 17 '22 at 22:34
  • And another side-note (sorry): You have `queue_t* init_queue();`, but that doesn't declare a function that takes zero arguments — it defines a function that takes an indeterminate but fixed number of arguments (it isn't a variadic function with ellipsis `...` in the prototype). To declare a function that takes no arguments, explicitly use `queue_t* init_queue(void);`. (C++ is different, but C is not C++.) – Jonathan Leffler Jul 17 '22 at 22:50
  • For some ideas, see my answer: [write a generic struct-print in C](https://stackoverflow.com/a/65621483/5382650) – Craig Estey Jul 17 '22 at 23:32

2 Answers2

2

One popular way to do what you're trying to do is to nest your data structures and thereby make "polymorphic" types. In your case, that would mean removing the payload from the LL_NODE struct entirely and declare various "derived" structs as needed. For example:

typedef struct LL_NODE {
  struct LL_NODE* next;
} node_t;

typedef struct INT_NODE {
  node_t node;
  int payload;
} int_node_t;

typedef struct TREE_NODE {
  node_t node;
  struct BINARY_TREE_NODE *payload;
} tree_node_t;

Any time you want to work with the linked list simply pass a pointer to the node struct member for any derived type.

Of course for this to be useful, you'll need a way to "downcast" from a linked list node to a derived type. This is usually done with a macro that will look roughly as follows:

#define LIST_NODE_DOWNCAST(ptr, derivedType, nodeName) \
   (derivedType *)((ptrdiff_t)ptr - (ptrdiff_t)&((derivedType *)0)->nodeName)

So for example, given a node_t pointer called list_ptr, that you know refers to an int_node_t object, you can recover the int_node_t object as follows:

int_node_t *derived = LIST_NODE_DOWNCAST(list_ptr, int_node_t, node);

Note that this is the primary method used in the Linux kernel for managing queues and lists. I answered a related question a while ago where you can see a specific example in usage in the kernel.

Jon Reeves
  • 2,426
  • 3
  • 14
0

I would suggest (quite strongly) that the makefile is the wrong place to make that change. You should be setting the data type in the BST code before including queue.h:

…
#define DATATYPE struct BINARY_TREE_NODE *

#include "queue/queue.h"
…

Your binary tree code knows what it wants to use; it should not rely on the vagaries of the makefile to ensure it gets what it needs.

Whether you should add #undef DATATYPE before defining it is trickier; I'd not do it unless it was proven necessary — and I'd also debate why it's necessary and how you are going to have two sets of functions with the same names but working with different types, given that this is C and C++. On the whole, not using #undef is safer; you will be told if there is a problem which using #undef will conceal — and the concealed problems will be hell to debug!

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278