I like your diagrams in the comments - a picture really can express much more than words sometimes! It's a shame the text lines are so long - I recommend keeping line lengths less than a standard terminal width of 80 columns (even in these days of large monitors, most readers prefer to have more files visible side-by-side than to have longer lines in each).
struct Queue { size_t capacity, size; void** data; size_t head, tail; };
Good choice of type for capacity
and size
. I'd probably have head
and tail
be pointers to void*
rather than indexes, but either is a valid design choice.
Keeping a record of head
, tail
and size
is somewhat redundant. Instead of maintaining size
as a member, we could compute it when needed from head
and tail
, provided we don't ever completely fill the queue (i.e. expand it just before head==tail
, rather than just after). Alternatively, we could dispense with either head
or tail
, and generate the value from the other one and size
.
Queue* queue = calloc(1, sizeof *queue); assert(queue);
That's plain wrong. We know that calloc()
can return zero, so claiming queue
is non-zero is mistaken.
It seems that you think assert()
is a tool for run-time checks, but that is not the case. assert()
exists to document things we know to be true (and, in debug builds, let us know when our claims are wrong).
The correct code is
Queue* queue = calloc(1, sizeof *queue);
if (!queue) { return queue; }
Not only does this perform the check in non-debug builds, it reports the failure to the caller, who can handle it appropriately.
Such misuse of assert()
exists throughout the program.
It's not clear why we're using calloc()
here rather than malloc()
- we write all the storage we allocate, so the zero-initialising done by calloc()
is just a waste of cycles.
void queue_free(Queue* queue) { assert(queue); free(queue->data);
Why not just handle a null queue
, to give an interface consistent with free()
? I'd write
void queue_free(Queue* queue)
{
if (!queue) { return; }
free(queue->data);
free(queue);
}
That makes life much easier for callers, who can now pass their Queue*
to queue_free()
without needing a separate path for null pointers.
if (queue->head > queue->tail) { for (size_t i = queue->head; i < queue->capacity; ++i) { tmp[i + queue->capacity] = tmp[i]; tmp[i] = NULL; } queue->head += queue->capacity; }
I think the loop there can be replaced by a simple memmove()
. There should be no need to assign NULL
to the positions between tail
and head
, as we'll not access those entries before they are next written.
There's a subtle connection between two parts of the code here:
size_t scale = 2; if (queue->head > queue->tail) { /* copy elements */ queue->head += queue->capacity; } queue->capacity *= scale;
That scale factor of 2 and the addition of queue->capacity
are tightly bound, yet if we were to change the scale factor to 3, or (also changing its type) to 1.5, we wouldn't necessarily spot that we need to update the addition to match. We could write
queue->head += queue->capacity * (scale - 1);
Alternatively, we could introduce a new variable instead of scale
:
new_capacity = queue->capacity * 2;
queue->head += new_capacity - queue->capacity;
This version makes it easier to use rational scale factors using integer arithmetic (queue->capacity * 3 / 2
, for example).
queue_iterate
has:
if (queue->size == 0) { return; }
That's unnecessary, since the rest of the function is a for
loop that will do nothing when size is zero. We can just omit this test.
One thing missing that I would have liked to have seen is a comprehensive set of unit tests. It's much better to demonstrate correctness by testing than by inspection, and self-contained library code such as this is highly suited to unit testing.
A good test suite also gives confidence to make improvements to the code with minimal risk of introducing regressions to previously-working functions.
Whenever a bug is found, it's good to reproduce it by adding a new test. Then fix the bug to make the test pass.
Occasionally, run the test suite using a profiler or checker such as Valgrind. Depending on the checker, that can reveal memory leaks or other allocation problems, cache performance, or many other run-time aspects of the code.