views:

238

answers:

3

Hi,

Maybe there's no way to solve this the way I'd like it but I don't know everything so I better ask...

I've implemented a simple Queue with a dynamic array so the user can initialize with whatever number of items it wants. I'm also trying to use a void pointer as to allow any data type, but that's the problem.

Here's my code:

typedef void * QueueValue;

typedef struct sQueueItem {
    QueueValue value;
} QueueItem;

typedef struct sQueue {
    QueueItem *items;

    int first;
    int last;
    int size;
    int count;
} Queue;

void queueInitialize(Queue **queue, size_t size) {
    *queue = xmalloc(sizeof(Queue));

    QueueItem *items = xmalloc(sizeof(QueueItem) * size);

    (*queue)->items = items;
    (*queue)->first = 0;
    (*queue)->last = 0;
    (*queue)->size = size;
    (*queue)->count = 0;
}

Bool queuePush(Queue * const queue, QueueValue value, size_t val_sz) {
    if(isNull(queue) || isFull(queue)) return FALSE;

    queue->items[queue->last].value = xmalloc(val_sz);
    memcpy(queue->items[queue->last].value, value, val_sz);

    queue->last = (queue->last+1) % queue->size;
    queue->count += 1;

    return TRUE;
}

Bool queuePop(Queue * const queue, QueueValue *value) {
    if(isEmpty(queue)) return FALSE;

    *value = queue->items[queue->first].value;

    free(queue->items[queue->first].value);

    queue->first = (queue->first+1) % queue->size;
    queue->count -= 1;

    return TRUE;
}

The problem lies on the queuePop function. When I call it, I lose the value because I free it right away. I can't seem to solve this dilemma. I want my library to be generic and modular. The user should not care about allocating and freeing memory, that's the library's job.

How can the user still get the value from queuePop and let the library handle all memory allocs/frees?

+1  A: 

I think you want to change your idea about what to store. A user gives you a pointer to some memory that she allocated, so she should expect to deallocate it. You don't need to memcpy or free the value, you just need to keep track of the pointer. Pushing in the queue should transfer ownership to the queue, and popping from the queue should transfer ownership back to the user. So all you need to do is copy around the 'val' pointer.

Also, to clean up the queue storage when finished, you probably want a queueDestroy(Queue* q) function.

Edit:

  • Notice, you don't need QueueItem and you can store an array of QueueValues.
  • Also, I realize you explicitly said that memory management was the job of the library, but that's when you run into problems (like the one you're running into). The library should take care of its own memory management (the queue). But the alloc & dealloc of the items should be the user's job.
  • Another option is to provide a queueFront(Queue *q) which passes back the value, but then it gets deallocated by queuePop(Queue *q). I prefer your current approach though :)
  • Requiring the user to define a max size for the queue is kinda restricting. If you want this to be generic++,modular++ then you should use a predefined constant, then grow on queuePush(), if it's full. Alternatively you can use a linked list implementation (but contiguous memory is generally much much faster).
Stephen
And also ... would you really implement a queue like this? I'm trying to figure out why you wouldn't use a linked list. Am I missing something?
Brian Roach
@Brian: Yes, I forgot to comment on that too :) I think an array is reasonable, but it should definitely reallocate when it fills.
Stephen
If I had to use an array like this, I think I'd go with a circular affair, keep pointers into the array for the start and end of the queue.
Brian Roach
Stephen
I still don't like the idea of the user allocing/freeing memory. And this is already a circular buffer (notice how I increment/decrement the first/last variables). I already have a destroy function, I just didn't post it because it was not relevant to the question.
Nazgulled
@Nazgulled. You're right, sorry, it was late. Removed the circular buffer part.
Stephen
+1  A: 

Others have (correctly) pointed out the severe limitations of your design, but this will fix what you have. It assumes that the caller knows what size object is getting pushed and popped.

Theoretically, only two of these changes are absolutely essential, but the others serve to lower the likelihood of a crash (due to programmer error) from ~100% to ~80%.

typedef struct sQueueItem {
    QueueValue value;
    size_t     item_size;               // <-- you'll need this for the Pop
} QueueItem;

Bool queuePush(Queue * const queue, QueueValue value, size_t val_sz) {
    if(isNull(queue) || isFull(queue)) return FALSE;

    queue->items[queue->last].value = xmalloc(val_sz);
    memcpy(queue->items[queue->last].value, value, val_sz);
    queue->items[queue->last].item_size = val_sz;        // <-- save the size

    queue->last = (queue->last+1) % queue->size;
    queue->count += 1;

    return TRUE;
}

Bool queuePop(Queue * const queue, 
               QueueValue **value, // ESSENTIAL: now char **
               size_t item_size)   // so we can ensure enough room
{                                         
    if(isEmpty(queue)) return FALSE;

     // just for readability
    QueueItem *p = queue->items[queue->first];

    // watch for programmer error (maybe you should throw() something)
    assert(p->item_size == item_size);       

    // ESSENTIAL: copy the item to the caller's memory
    memcpy(*value, p->value, p->item_size); 

    free(queue->items[queue->first].value);

    queue->first = (queue->first+1) % queue->size;
    queue->count -= 1;

    return TRUE;
}

Edit:

It's been pointed out that I could have left queuePop as

Bool queuePop(Queue * const queue, 
               QueueValue *value,  // stet
               size_t item_size)   // so we can ensure enough room

and changed the `memcpy` to 

    // ESSENTIAL: copy the item to the caller's memory
    memcpy(value, p->value, p->item_size); 

I had at some point written it so if the caller passed a NULL in item_size, queuePop would do a malloc() and pass the pointer back to the caller via **value. I changed my mind and thought I reverted completely, but SO doesn't have Version Control :)

egrunin
There is absolutely no reason for the `value` parameter of `queuePop()` to be type `QueueValue **` - it only need be `QueueValue` (pointer to the location where the item will be copied).
caf
@caf - You're right, that was left over from an earlier approach I tried, where `queuePop()` did a fresh `malloc()`. I'll append a comment.
egrunin
+1  A: 

Your queuePop() function needs to work the same way as queuePush() - take the size of the location and memcpy() to it.

Bool queuePop(Queue * const queue, QueueValue value, size_t val_sz)
{
    if (isEmpty(queue)) return FALSE;

    memcpy(value, queue->items[queue->first].value, val_sz);

    free(queue->items[queue->first].value);

    queue->first = (queue->first+1) % queue->size;
    queue->count -= 1;

    return TRUE;
}
caf