Consider a test-driven approach to implementing this functionality. That is, write tests for each piece of functionality before implementing it.
We would want to begin with the minimum necessary to create and destroy an empty list:
#define TEST_LIST(list, count, ...) /* TODO */
int main(void)
{
{
List *list = list_alloc();
TEST_LIST(list, 0);
list_free(list);
}
}
We'll need the beginnings of the public interface (in a header file):
typedef struct List List;
List *list_alloc(void);
void list_free(List*);
Notice I'm using a common prefix to the function names. This is helpful because it means they all end up together when sorted alphabetically.
And now we can start the implementation. I'll use a circular list with dummy head node, because I anticipate that this will reduce special-case code for start and end.
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *prev;
struct Node *next;
} Node;
struct List {
Node content;
size_t size;
};
List *list_alloc(void) {
List *list = malloc(sizeof *list);
if (list) {
list->size = 0;
list->content.prev = NULL;
list->content.next = NULL;
}
return list;
}
void list_free(List *list)
{
if (!list) { return; }
Node *n = list->content.next;
while (n && n != &list->content) {
Node *t = n;
n = n->next;
free(t);
}
free(list);
}
Now we just need to implement the TEST_LIST
macro. At this stage, we just need to be able to test an empty list, but I'm going to jump ahead and implement the whole test:
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
/* verify list's invariants and contents */
bool test_list(char const *file, int line,
const List *list, size_t count, ...)
{
if (!list) {
fprintf(stderr, "%s:%d: List is null\n", file, line);
return false;
}
if (list->size != count) {
fprintf(stderr, "%s:%d: List length is %zu, but expected %zu\n",
file, line, list->size, count);
return false;
}
va_list args;
va_start(args, count);
Node const *node = list->content.next;
for (size_t i = 0; i < count; ++i) {
if (node->prev->next != node) {
fprintf(stderr, "%s:%d: Inconsistent 'prev' pointer at element %zu\n",
file, line, i);
return false;
}
if (node->next->prev != node) {
fprintf(stderr, "%s:%d: Inconsistent 'next' pointer at element %zu\n",
file, line, i);
return false;
}
int val = va_arg(args, int);
if (val != node->data) {
fprintf(stderr, "%s:%d: list[%zu] contains %d, but expected %d\n",
file, line, i, node->data, val);
return false;
}
node = node->next;
}
va_end(args);
return true;
}
#define TEST_LIST(...) if (!test_list(__FILE__, __LINE__, __VA_ARGS__)) \
return EXIT_FAILURE;
We now have something which will fail if we break any of the functionality, and which we can run under Valgrind to diagnose any of the usual memory problems.
Now we add a test for the next function:
{
List *list = list_alloc();
assert(list_prepend(list, 4));
TEST_LIST(list, 1, 4);
assert(list_prepend(list, 6));
TEST_LIST(list, 2, 6, 4);
list_free(list);
}
Then we can implement it:
bool list_prepend(List *list, int value)
{
if (!list) { return false; }
Node* node = malloc(sizeof *node);
if (!node) { return false; }
node->data = value;
node->next = list->content.next;
node->prev = &list->content;
list->content.next = node;
node->next->prev = node;
++list->size;
return true;
}
We run the test, and it passes, with no Valgrind errors. Great!
Next function:
{
List *list = list_alloc();
assert(list_append(list, 4));
TEST_LIST(list, 1, 4);
assert(list_append(list, 6));
TEST_LIST(list, 2, 4, 6);
list_free(list);
}
And implementation:
bool list_append(List *list, int value)
{
if (!list) { return false; }
Node* node = malloc(sizeof *node);
if (!node) { return false; }
node->data = value;
node->next = &list->content;
node->prev = list->content.prev;
list->content.prev = node;
node->prev->next = node;
++list->size;
return true;
}
This passes first time, too! But before we congratulate ourselves, now is the opportunity to refactor. We have two functions that are very similar, so let's reduce the duplication by introducing an internal helper function:
static bool insert_after(Node *node, int value)
{
Node *new_node = malloc(sizeof *new_node);
if (!new_node) { return false; };
new_node->data = value;
new_node->next = node->next;
new_node->prev = node;
new_node->prev->next = new_node;
new_node->next->prev = new_node;
return true;
}
bool list_prepend(List *list, int value)
{
if (!list) { return false; }
if (!insert_after(&list->content, value)) { return false; }
++list->size;
return true;
}
bool list_append(List *list, int value)
{
if (!list) { return false; }
if (!insert_after(list->content.prev, value)) { return false; }
++list->size;
return true;
}
The tests give us a safety-net to perform refactoring like this without fear that we break existing functionality.
The refactoring we have just done is about to pay off, as we add the function to insert at a given index. First, the tests. We start by testing the error case - inserting beyond the end:
{
List *list = list_alloc();
assert(!list_insert(list, 1, 5));
list_free(list);
}
We can make that test pass:
bool list_insert(List *list, size_t position, int value)
{
if (!list || position > list->size) { return false; }
return true; /* for now */
}
Now extend to handle a valid call:
{
List *list = list_alloc();
assert(!list_insert(list, 1, 5)); /* out of range */
TEST_LIST(list, 0);
assert(list_append(list, 4));
assert(list_append(list, 6));
assert(list_insert(list, 1, 5)); /* in the middle */
TEST_LIST(list, 3, 4, 5, 6);
assert(list_insert(list, 0, 3)); /* at the front */
TEST_LIST(list, 4, 3, 4, 5, 6);
assert(list_insert(list, 4, 7)); /* at the back */
TEST_LIST(list, 5, 3, 4, 5, 6, 7);
list_free(list);
}
bool list_insert(List *list, size_t position, int value)
{
if (!list || position > list->size) { return false; }
Node *n = &list->content;
while (position--) {
n = n->next;
}
if (!insert_after(n, value)) { return false; }
++list->size;
return true;
}
We can continue implementing the functionality a piece at a time. And as we go, add extra functions that help the tests; they are probably valuable to users as well. For example, I created a list_set_values()
function to quickly initialise a non-empty list.
When I did this, it resulted in this header:
#ifndef LIST_H_
#define LIST_H_
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
typedef struct List List;
//static const size_t Invalid_Index = (size_t)-1;
/* create and destroy */
List *list_alloc(void);
void list_free(List *list);
/* add values */
bool list_set_values(List *list, size_t count, ...);
bool list_prepend(List *list, int value);
bool list_append(List *list, int value);
bool list_insert(List *list, size_t position, int value);
/* remove values */
void list_clear(List *list);
void list_pop_front(List *list);
void list_pop_back(List *list);
bool list_remove_value(List *list, int value);
/* move values */
void list_rotate_left(List *list, size_t steps);
void list_rotate_right(List *list, size_t steps);
void list_reverse(List *list);
/* traversal */
void list_traverse(List *list, void (*fun)(int*,void*), void*);
void list_print(List *list, FILE *f);
/* for the unit tests */
bool test_list(char const *file, int line,
const List *list, size_t count, ...);
#endif
And these tests:
#include "list.h"
#undef NDEBUG
#include <assert.h>
#include <stdlib.h>
#define TEST_LIST(...) if (!test_list(__FILE__, __LINE__, __VA_ARGS__)) \
return EXIT_FAILURE;
static void times_two(int *p, void *unused)
{
(void)unused;
*p *= 2;
}
static void accumulate(int *p, void *s)
{
int *sum = s;
*sum += *p;
}
int main(void)
{
{
List *list = list_alloc();
TEST_LIST(list, 0);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 2, 10, 20));
TEST_LIST(list, 2, 10, 20);
list_clear(list);
TEST_LIST(list, 0);
list_free(list);
}
{
List *list = list_alloc();
assert(list_prepend(list, 4));
TEST_LIST(list, 1, 4);
assert(list_prepend(list, 6));
TEST_LIST(list, 2, 6, 4);
list_free(list);
}
{
List *list = list_alloc();
assert(list_append(list, 4));
TEST_LIST(list, 1, 4);
assert(list_append(list, 6));
TEST_LIST(list, 2, 4, 6);
list_free(list);
}
{
List *list = list_alloc();
assert(!list_insert(list, 1, 5)); /* out of range */
TEST_LIST(list, 0); /* unchanged */
assert(list_set_values(list, 2, 4, 6));
assert(list_insert(list, 1, 5)); /* in the middle */
TEST_LIST(list, 3, 4, 5, 6);
assert(list_insert(list, 0, 3)); /* at the front */
TEST_LIST(list, 4, 3, 4, 5, 6);
assert(list_insert(list, 4, 7)); /* at the back */
TEST_LIST(list, 5, 3, 4, 5, 6, 7);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 2, 5, 6));
list_pop_front(list);
TEST_LIST(list, 1, 6);
list_pop_front(list);
TEST_LIST(list, 0);
assert(list_set_values(list, 2, 5, 6));
list_pop_back(list);
TEST_LIST(list, 1, 5);
list_pop_back(list);
TEST_LIST(list, 0);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 5, 1, 2, 3, 4, 1));
assert(!list_remove_value(list, 0)); /* not present */
TEST_LIST(list, 5, 1, 2, 3, 4, 1);
assert(list_remove_value(list, 4));
TEST_LIST(list, 4, 1, 2, 3, 1);
assert(list_remove_value(list, 1));
TEST_LIST(list, 3, 2, 3, 1);
assert(list_remove_value(list, 1));
TEST_LIST(list, 2, 2, 3);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 5, 1, 2, 3, 4, 5));
list_rotate_left(list, 0);
TEST_LIST(list, 5, 1, 2, 3, 4, 5);
list_rotate_left(list, 2);
TEST_LIST(list, 5, 3, 4, 5, 1, 2);
list_rotate_right(list, 3);
TEST_LIST(list, 5, 5, 1, 2, 3, 4);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 5, 1, 2, 3, 4, 5));
list_reverse(list);
TEST_LIST(list, 5, 5, 4, 3, 2, 1);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 4, 1, 2, 3, 4));
list_traverse(list, times_two, NULL);
TEST_LIST(list, 4, 2, 4, 6, 8);
int sum = 0;
list_traverse(list, accumulate, &sum);
assert(sum == 20);
list_free(list);
}
{
List *list = list_alloc();
assert(list_set_values(list, 5, 1, 2, 3, 4, 5));
list_print(list, stdout);
puts("");
list_free(list);
}
}
And here's my implementation:
#include "list.h"
#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *prev;
struct Node *next;
} Node;
struct List {
Node content;
size_t size;
};
List *list_alloc(void) {
List *list = malloc(sizeof *list);
if (list) {
list->size = 0;
list->content.prev = &list->content;
list->content.next = &list->content;
}
return list;
}
static void delete_nodes(List *list)
{
Node *n = list->content.next;
while (n && n != &list->content) {
Node *t = n;
n = n->next;
free(t);
}
}
void list_free(List *list)
{
if (!list) { return; }
delete_nodes(list);
free(list);
}
static bool insert_after(Node *preceding, int value)
{
Node *new_node = malloc(sizeof *new_node);
if (!new_node) { return false; };
Node *following = preceding->next;
new_node->data = value;
new_node->next = following;
new_node->prev = preceding;
preceding->next = new_node;
following->prev = new_node;
return true;
}
bool list_prepend(List *list, int value)
{
if (!list) { return false; }
if (!insert_after(&list->content, value)) { return false; }
++list->size;
return true;
}
bool list_append(List *list, int value)
{
if (!list) { return false; }
if (!insert_after(list->content.prev, value)) { return false; }
++list->size;
return true;
}
bool list_insert(List *list, size_t position, int value)
{
if (!list || position > list->size) { return false; }
Node *n = &list->content;
while (position--) {
n = n->next;
}
if (!insert_after(n, value)) { return false; }
++list->size;
return true;
}
bool list_set_values(List *list, size_t count, ...)
{
list_clear(list);
va_list args;
va_start(args, count);
while (count--) {
if (!list_append(list, va_arg(args, int))) {
va_end(args);
return false;
}
}
va_end(args);
return true;
}
void list_clear(List *list)
{
if (!list) { return; }
delete_nodes(list);
list->size = 0;
list->content.prev = &list->content;
list->content.next = &list->content;
}
static void delete_node(List *list, Node *n)
{
n->prev->next = n->next;
n->next->prev = n->prev;
free(n);
--list->size;
}
void list_pop_front(List *list)
{
if (!list || !list->size) { return; }
delete_node(list, list->content.next);
}
void list_pop_back(List *list)
{
if (!list || !list->size) { return; }
delete_node(list, list->content.prev);
}
bool list_remove_value(List *list, int value)
{
Node *n = &list->content;
while ((n = n->next) != &list->content) {
if (n->data == value) {
delete_node(list, n);
return true;
}
}
return false; /* not found */
}
void list_rotate_left(List *list, size_t steps)
{
if (!list) { return; }
steps %= list->size;
if (!steps) { return; }
Node *n = list->content.next;
while (steps--) {
n = n->next;
}
/* detach the dummy head */
list->content.next->prev = list->content.prev;
list->content.prev->next = list->content.next;
/* and insert it before n */
list->content.next = n;
list->content.prev = n->prev;
n->prev = &list->content;
list->content.prev->next = &list->content;
}
void list_rotate_right(List *list, size_t steps)
{
if (!list) { return; }
steps %= list->size;
list_rotate_left(list, list->size - steps);
}
void list_reverse(List *list)
{
Node *n = &list->content;
do {
/* swap next and prev pointers */
Node *tmp = n->next;
n->next = n->prev;
n->prev = tmp;
} while ((n = n->next) != &list->content);
}
void list_traverse(List *list, void (*fun)(int*,void*), void *p)
{
Node *n = &list->content;
while ((n = n->next) != &list->content) {
fun(&n->data, p);
}
}
static void print_value(int *p, void *s)
{
FILE *f = s;
fprintf(f, "%d, ", *p);
}
void list_print(List *list, FILE *f)
{
fputs("{ ", f);
list_traverse(list, print_value, f);
fputs("}", f);
}
/* verify list's invariants and contents */
bool test_list(char const *file, int line,
const List *list, size_t count, ...)
{
if (!list) {
fprintf(stderr, "%s:%d: List is null\n", file, line);
return false;
}
if (list->size != count) {
fprintf(stderr, "%s:%d: List length is %zu, but expected %zu\n",
file, line, list->size, count);
return false;
}
va_list args;
va_start(args, count);
Node const *node = list->content.next;
for (size_t i = 0; i < count; ++i) {
if (node->prev->next != node) {
fprintf(stderr, "%s:%d: Inconsistent 'prev' pointer at element %zu\n",
file, line, i);
va_end(args);
return false;
}
if (node->next->prev != node) {
fprintf(stderr, "%s:%d: Inconsistent 'next' pointer at element %zu\n",
file, line, i);
va_end(args);
return false;
}
int val = va_arg(args, int);
if (val != node->data) {
fprintf(stderr, "%s:%d: list[%zu] contains %d, but expected %d\n",
file, line, i, node->data, val);
va_end(args);
return false;
}
node = node->next;
}
va_end(args);
return true;
}
This isn't totally finished. I recommend you consider adding (at least internally) one or more functions for detaching sub-lists and splicing them elsewhere. That will reduce duplication further.