Implementing a Generic Queue in C: It’s That Simple!

Have you ever wondered how that cup of Luckin coffee you order every morning is still served in order, even when dozens of people are placing orders at the same time? Or how the 12306 ticketing system handles thousands of concurrent requests while still ensuring first-come, first-served?

The answer is simple: Queue.

Today, we are not going to discuss high-end distributed message middleware or fancy tools like Kafka or RabbitMQ. Instead, we will return to the most primitive and hardcore state—using pure C language to create a Generic Queue, just like building a manual tractor for yourself. It may be basic, but those who understand it know how robust it is.

Moreover, we need to understand: why does a project on GitHub with only 10 stars, `kostakis/Generic-Queue`[1], contain so many technical details worth exploring? Is it just toy code, or is it truly industrial-grade design?

Don’t worry, let’s sip on our iced Americano while we dissect this thing from start to finish.

0x0001 | You Think a Queue is Just<span>int queue[100]</span>? Too Young, Too Simple!

When it comes to queues, many people’s first reaction is:

int queue[100];
int front = 0, rear = 0;

Then they write two functions, <span>enqueue()</span> and <span>dequeue()</span>, and that’s it. Indeed, that’s how textbooks teach it.

But this approach has a fatal flaw: It can only store int!

What if you want to store a <span>Person</span> structure tomorrow? For example:

typedef struct {
    char name[32];
    int age;
    float salary;
} Employee;

Are you going to write a whole new queue? Writing the same code three times would make your boss want to fire you.

So true experts do not settle for “it works”; they pursue “generality”.

The project called <span>kostakis</span> has done just that: allowing the same queue to store integers, strings, structures, and even pointers to photos of your cat.

How is this achieved? The core concept is two words: Generics.

Although C does not have syntax sugar like C++ templates or Java generics, we have a secret weapon—<span>void*</span> pointers!

void*: The “Universal Glue” in C Language

You can think of <span>void*</span> as a pointer that says, “I don’t know what is inside, but I know how big it is.” It is the soul of implementing generics in C.

For example:

void *data;  // I don't care if you're int*, char*, or struct Person*
size_t size; // But I need to know how many bytes you occupy!

As long as we know the size of the data, we can dynamically allocate memory, copy content, and safely pass it around—this lays the foundation for a “generic container”.

The <span>Generic-Queue</span> is built on this idea: when creating the queue, tell it how big each element is, and it will handle the rest.

0x0002 | Dissecting the Source Code: What Does This Queue Look Like?

Let’s first take a look at the overall structure of this project (don’t worry, I’ll guide you through it step by step):

Generic-Queue/
├── include/           # Header files directory
├── src/               # C source implementation
├── tests/             # Unit tests (written in C++ with Google Test)
├── external/          # Third-party dependencies (here is gtest)
├── main.c             # Example program
├── CMakeLists.txt     # Build script
└── README.md

It looks neat, like a proper project. Unlike some people who just upload a <span>.c</span> file and dare to call it a “library”.

Core API Overview

According to the documentation, its main interfaces are as follows:

Function Functionality
<span>createQueue(size_t)</span> Create a queue of specified element size
<span>destroyQueue(&q)</span> Destroy the queue and free memory
<span>enqueue(q, data)</span> Enqueue
<span>dequeue(q, out)</span> Dequeue and get value
<span>front(q, out)</span> View the front element
<span>getSize(q)</span> Get current length
<span>isEmpty(q)</span> Check if empty
<span>copyQueue(src)</span> Deep copy the entire queue
<span>reverse(q)</span> Reverse the order of the queue
<span>find(q, predicate)</span> Custom condition search
<span>findMem(q, data)</span> Memory comparison search

See? Not only are the basic operations complete, but it also supports deep copy, reversal, and search—these advanced features are not toy code; they are ready for serious work.

0x0003 | Hands-On Practice: Let’s Play with Real C Generic Programming

Talk is cheap; let’s implement a simplified version of a generic queue ourselves, following its style.

Step 1: Define the Queue Structure

// queue.h
#ifndef QUEUE_H
#define QUEUE_H

#include <stdbool.h>
#include <stddef.h>

typedef struct queue {
    void *data;         // Continuous memory block storing all elements
    size_t elem_size;   // Size of each element (in bytes)
    size_t capacity;    // Current maximum capacity
    size_t head;        // Head index
    size_t tail;        // Tail index
    size_t size;        // Current number of elements
} queue;

// Create queue
queue* createQueue(size_t elem_size);

// Destroy queue
void destroyQueue(queue** q);

// Enqueue
bool enqueue(queue* q, const void* data);

// Dequeue
bool dequeue(queue* q, void* out);

// View front
bool front(const queue* q, void* out);

// Get size
size_t getSize(const queue* q);

// Check if empty
bool isEmpty(const queue* q);

#endif

Note a few key points:

  • All data exists in a continuous memory block (<span>void *data</span>), similar to an array.
  • Using <span>head</span> and <span>tail</span> to implement a circular queue avoids frequent memory movement.
  • <span>elem_size</span> is the soul, determining what type of data you can store.

Step 2: Implement Core Logic (src/queue.c)

#include "queue.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define INITIAL_CAPACITY 8

queue* createQueue(size_t elem_size) {
    if (elem_size == 0) return NULL;

    queue* q = (queue*)malloc(sizeof(queue));
    if (!q) return NULL;

    q->elem_size = elem_size;
    q->capacity = INITIAL_CAPACITY;
    q->data = malloc(elem_size * q->capacity);
    if (!q->data) {
        free(q);
        return NULL;
    }

    q->head = 0;
    q->tail = 0;
    q->size = 0;

    return q;
}

void destroyQueue(queue** q) {
    if (!q || !*q) return;
    free((*q)->data);
    free(*q);
    *q = NULL;  // Prevent dangling pointer
}

Here are two key points:

  1. Initialize capacity to 8: Too small will cause frequent resizing, too large will waste memory. 8 is a good starting point.
  2. Double pointer for destruction: Passing <span>queue**</span> allows us to nullify the pointer inside the function to prevent misuse later.

Next is the enqueue operation:

static bool resize(queue* q) {
    size_t new_cap = q->capacity * 2;
    void* new_data = realloc(q->data, q->elem_size * new_cap);
    if (!new_data) return false;

    q->data = new_data;

    // If head < tail, data is continuous, no need to process
    // Otherwise, move data after tail to the end of the new space
    if (q->head >= q->tail && q->size > 0) {
        memcpy((char*)q->data + q->capacity * q->elem_size,
               (char*)q->data,
               q->tail * q->elem_size);
        q->tail += q->capacity;
    }
    q->capacity = new_cap;
    return true;
}

bool enqueue(queue* q, const void* data) {
    if (!q || !data) return false;

    if (q->size == q->capacity) {
        if (!resize(q)) return false;
    }

    // Copy data to tail position
    void* target = (char*)q->data + q->tail * q->elem_size;
    memcpy(target, data, q->elem_size);

    q->tail = (q->tail + 1) % q->capacity;
    q->size++;
    return true;
}

Did you see that? We used <span>memcpy</span> in <span>enqueue</span> to copy any type of data, which is the essence of “generics”!

Dequeue is also straightforward:

bool dequeue(queue* q, void* out) {
    if (!q || !out || q->size == 0) return false;

    void* src = (char*)q->data + q->head * q->elem_size;
    memcpy(out, src, q->elem_size);

    q->head = (q->head + 1) % q->capacity;
    q->size--;
    return true;
}

Note: We copy the data out to the user instead of returning a pointer. This is safer and avoids lifecycle issues.

Step 3: Test Our “Handcrafted Queue”

Let’s do a practical example to see if it works correctly:

// main.c
#include "queue.h"
#include <stdio.h>

typedef struct {
    int id;
    char name[32];
} Person;

int main() {
    queue* q = createQueue(sizeof(Person));
    if (!q) {
        fprintf(stderr, "Failed to create queue\n");
        return 1;
    }

    Person p1 = {1, "Zhang San"};
    Person p2 = {2, "Li Si"};

    enqueue(q, &p1);
    enqueue(q, &p2);

    printf("Queue size: %zu\n", getSize(q));

    Person tmp;
    while (!isEmpty(q)) {
        dequeue(q, &tmp);
        printf("Dequeued: ID=%d, Name=%s\n", tmp.id, tmp.name);
    }

    destroyQueue(&q);
    return 0;
}

Compile and run:

gcc -o demo main.c src/queue.c
./demo

Output:

Queue size: 2
Dequeued: ID=1, Name=Zhang San
Dequeued: ID=2, Name=Li Si

✅ Success! Our generic queue can correctly handle structures!

0x0004 | In-Depth Principles: The Design Philosophy You Can’t See

Do you think this is the end? No, the truly impressive part is just beginning.

1. Circular Queue vs Dynamic Array

We used a combination of circular queue + dynamic resizing.

  • Circular Queue: Utilizes modulo operations to allow <span>head</span> and <span>tail</span> to move in a circular manner within the array, reducing memory movement.
  • Dynamic Resizing: Automatically doubles the size when space is insufficient, amortizing the time complexity close to O(1).

This design achieves an excellent balance between performance and memory.

💡 Fun Fact: In STL, <span>std::queue</span> is backed by <span>deque</span> by default, and many places in the Linux kernel also use circular buffers, sharing the same underlying concept.

2. Deep Copy vs Shallow Copy: Which is the Safe Choice?

Have you ever thought about this question: What if I put pointers into the queue?

For example:

char* name = strdup("Wang Wu");
enqueue(q, &name);  // Storing char**

If the original pointer is freed, the one in the queue becomes a dangling pointer.

Therefore, <span>Generic-Queue</span><span> provides </span><strong><span>deep copy semantics</span></strong><span>: Regardless of whether you pass in a value or a pointer, I only care about the content of that memory and make a complete copy.</span>

This is also why its <span>enqueue</span> accepts <span>void* data</span> instead of <span>void* ptr</span>—it wants the data itself, not the address.

It’s like sending a package; the courier only cares about the contents of the package, not where you took it from.

3. Search Mechanism: Not Just Comparing Sizes, but Custom Rules

Its API supports two search methods:

void* find(queue* q, bool (*predicate)(void* data));      // Custom condition
void* findMem(queue* q, void* data);                     // Memory memcmp comparison

For example, if you want to find people older than 30 in a <span>Person</span> queue:

bool older_than_30(void* data) {
    Person* p = (Person*)data;
    return p->age > 30;
}

Person* found = (Person*)find(q, older_than_30);
if (found) {
    printf("Found: %s\n", found->name);
}

This is the charm of function pointers + callback mechanisms: It hands over the judgment logic to the user, while the library only handles traversal.

It’s like telling a real estate agent: “I want a house within 500 meters of the subway station, with a monthly rent not exceeding 5000”; the agent runs around, and you just wait for the results.

4. Reversing the Queue: How to Elegantly Turn Everything Upside Down?

<span>reverse(queue* q)</span><span> sounds a bit strange: shouldn't a queue be first-in, first-out? Why reverse it?</span>

Actually, this can be very useful in certain scenarios:

  • Want to view logs in reverse order while debugging;
  • Some algorithms require temporary order changes;
  • Or… you just want to prank your colleague 😈

The implementation method is also clever: it can use a stack as an auxiliary or directly swap with two pointers:

void reverse(queue* q) {
    if (q->size <= 1) return;

    for (size_t i = 0; i < q->size / 2; i++) {
        size_t left_idx  = (q->head + i) % q->capacity;
        size_t right_idx = (q->tail - 1 - i + q->capacity) % q->capacity;

        char* left  = (char*)q->data + left_idx * q->elem_size;
        char* right = (char*)q->data + right_idx * q->elem_size;

        // Swap two elements
        char temp[q->elem_size];
        memcpy(temp, left, q->elem_size);
        memcpy(left, right, q->elem_size);
        memcpy(right, temp, q->elem_size);
    }
}

Doesn’t it feel like “disarming a bomb with bare hands”?

0x0005 | Engineering Mindset: How to Make Others Want to Use Your Wheel?

Many people just throw their code on GitHub after writing it, and it’s no surprise that no one stars it—because they haven’t considered user experience at all.

This project has several very professional practices worth learning:

✅ Using CMake Build System

# CMakeLists.txt snippet
add_library(genericQueue STATIC src/queue.c)
target_include_directories(genericQueue PUBLIC include)

This means others can integrate it like this:

add_subdirectory(Generic-Queue)
target_link_libraries(your_app genericQueue)

A modern C project standard, saying goodbye to manual <span>gcc xxx.c -o xxx</span>.

✅ Support Installation to System Path

sudo make install

This will place the header files in <span>/usr/local/include</span> and the static library in <span>/usr/local/lib</span>, fully complying with Unix conventions.

In the future, others can write code directly:

#include <queue.h>

Instead of painfully writing relative paths.

✅ Providing Comprehensive Unit Tests (Written in C++!)

Interestingly, the library itself is written in pure C, but the tests use C++ + Google Test.

TEST(QueueTest, EnqueueDequeue) {
    queue* q = createQueue(sizeof(int));
    int val = 42;
    enqueue(q, &val);

    int result;
    ASSERT_TRUE(dequeue(q, &result));
    EXPECT_EQ(result, 42);
    destroyQueue(&q);
}

Why do this?

Because Google Test is one of the strongest C/C++ testing frameworks, with rich assertions, clear output, and support for parameterized tests.

Although C++ is mixed in, due to good C ABI compatibility, it can link successfully.

This is a typical pragmatic attitude of “using tools for my benefit”.

✅ Zero Dependency Release

The most critical point: Users do not need to install gtest or any other third-party libraries.

The test code only exists during the development phase, and the final released <span>.a</span> file and header files are clean.

This is true “zero dependency”.

0x0006 | Comparing Other Solutions: Why Don’t We Just Use STL or Glib?

Some may ask: Isn’t there <span>std::queue</span> in C++? Doesn’t Linux have Glib’s <span>GQueue</span>? Why reinvent the wheel?

Good question. Let’s compare horizontally:

Solution Advantages Disadvantages
<span>std::queue</span> (C++) STL standard, powerful functionality Must use C++, introduces template bloat
<span>GQueue</span> (Glib) Full functionality, cross-platform Introduces Glib dependency, larger size
Handwritten Linked List Queue Simple and intuitive Prone to errors, hard to maintain
Generic-Queue Pure C, no dependencies, lightweight, portable Small community, limited documentation

So its positioning is clear: Suitable for embedded systems, small projects, educational purposes, and scenarios where large frameworks are not desired.

It’s like going to the countryside to fix a water pipe; there’s no need to drive a Mercedes; a tricycle is more flexible.

0x0007 | Pitfall Guide: 5 Hard-Learned Lessons for Writing C Generic Containers

After years of pitfalls, I have summarized 5 iron rules to share with you who are trying to write generic data structures:

❌ Error 1: Returning Internal Pointers

void* front_bad(queue* q) {
    return (char*)q->data + q->head * q->elem_size;  // Dangerous!
}

If the user saves this pointer, subsequent resizing may lead to memory invalidation.

✅ Correct Approach: Always copy data through output parameters.

❌ Error 2: Ignoring Alignment Issues

Different CPU architectures have strict memory alignment requirements. For example, accessing an unaligned <span>double</span> on ARM may crash directly.

✅ Recommendation: Use <span>aligned_alloc</span> or ensure allocated memory is aligned.

❌ Error 3: Forgetting to Handle Edge Cases

  • Enqueueing into an empty queue?
  • Multiple destructions?
  • Passing NULL pointers?

All these need defensive checks at the beginning of the function.

❌ Error 4: Misusing Macros to Implement “Pseudo-Generics”

Some people like to use macros to generate different types:

#define DEFINE_QUEUE(type) \
    typedef struct { ... } type##_queue;

It feels great in the short term, but it’s a nightmare for long-term maintenance. Too many types can lead to slow compilation and symbol explosion.

✅ Recommended: Stick to the <span>void* + size</span> runtime generic pattern.

❌ Error 5: Ignoring Error Handling

Many C library functions fail silently without reporting errors, leading to crashes.

✅ Correct Approach: Every operation that can fail should return a boolean value or error code.

0x0008 | Expanding Ideas: How Can This Queue Be Upgraded?

Since we have mastered the core principles, let’s boldly imagine what features could be added in future versions:

🔹 Support for Iterators

Traverse like Python:

for (iterator it = begin(q); it != end(q); ++it) {
    Person* p = (Person*)get(it);
    printf("%s\n", p->name);
}

The implementation can use internal state to track the current position.

🔹 Support for Thread-Safe Mode

Add a mutex to make it a thread-safe queue:

queue* q = createThreadSafeQueue(sizeof(Task));

Suitable for producer-consumer models.

🔹 Support for Persistence (Serialization)

Save the queue to a file:

saveQueueToFile(q, "backup.dat");
queue* loaded = loadQueueFromFile("backup.dat", sizeof(Person));

Useful for log caching, power recovery, etc.

🔹 Support for Observer Pattern

Register callbacks to notify on enqueue/dequeue:

onEnqueue(q, log_to_file);   // Log every enqueue
onDequeue(q, update_ui);     // Update UI on every dequeue

Instantly transforms into an event-driven system.

0x0009 | Conclusion: Stop Saying C Language Can’t Write Elegant Code

Many people think C language is outdated, hard to write, and prone to crashes, so they turn to Python, Go, or Rust.

But I want to say: C language is not outdated; it is misunderstood.

When you see such a concise, robust, and well-designed generic queue, you will find:

  • It has no fancy syntax, but every line solves a problem;
  • It does not depend on any framework, yet possesses complete engineering capabilities;
  • It builds a reliable data structure with the most primitive tools.

This is the true spirit of craftsmanship.

Next time you face a seemingly simple task, consider asking:

“Can I make it more generic?”

“Can others use it right away?”

“Will I still dare to read my code in ten years?”

If your answers are all “Yes”, then what you wrote is not just code; it’s a work of art.

Finally, thanks to <span>kostakis</span><span> for open-sourcing this project. Although it only has 10 stars, it reminds me of the excitement I felt when I first understood pointers.</span>

Perhaps great code never needs to be widely praised. As long as one developer understands it and gains inspiration from it—it’s enough.

📌 Project Address: https://github.com/kostakis/Generic-Queue[2]

☕ If you find this article useful, feel free to share it with that friend who is still using <span>int queue[100]</span>, and tell him: “Bro, it’s time to upgrade your gear.”

References

[1]

<span>kostakis/Generic-Queue</span>: https://github.com/kostakis/Generic-Queue

[2]

https://github.com/kostakis/Generic-Queue: https://github.com/kostakis/Generic-Queue

Leave a Comment