3
\$\begingroup\$

Some time ago I tried to make a generic linked list in pure C (no external libraries, only the C standard library) using void*s here. Building on top of that and with the same restrictions, I'm now making a hash table.

hashtable.h:

#ifndef HASHTABLE_H
#define HASHTABLE_H

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

#include "linkedlist.h"

typedef enum HashtableErrorCodes {
    HTERR_NONE,
    HTERR_NOTHIS,
    HTERR_BADALLOC,
    HTERR_NOSUCHVALUE,
    HTERR_DUPENT
} HashtableErrorCodes;

typedef struct Hashtable Hashtable;
typedef struct HashtableEntry HashtableEntry;

struct HashtableEntry {
    void *key;
    void *data;
};

struct Hashtable {
    Hashtable *this;

    LinkedList **data;
    size_t capacity;

    void (*destructor)(Hashtable *this);
    size_t (*hasher)(void *key, size_t maxIndex);
    bool (*keyComparer)(void *a, void *b);
    void (*keyDestroyer)(void *key);
    void (*dataDestroyer)(void *data);

    /**
     * Inserts data to the hashtable. Takes a pointer to a key and a pointer to the data to be inserted.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param key A pointer to the key to identify the data in the future.
     * @param data A pointer to the data to be stored.
     *
     * @returns HTERR_NONE if successful. HTERR_BADALLOC if space allocation to store the data failed.
    */
    HashtableErrorCodes (*add)(Hashtable *this, void *const key, void *const data);

    /**
     * Finds previously inserted data. Takes a pointer to the key used when inserting the data. Outputs the data via a
     * pointer to a pointer to the data.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param key A pointer to the key used when inserting the data.
     * @param data An output pointer that may point to a pointer to the previously inserted data.
     *
     * @returns HTERR_NONE if the data was found. HTERR_NOSUCHVALUE if no entry under the given key was found, in which
     * case the output pointer will be set to NULL.
    */
    HashtableErrorCodes (*find)(Hashtable *const this, void *const key, void **const data);

    /**
     * Removes previously inserted data. Takes a pointer to the key used when inserting the data. If successful, will
     * also deallocate the removed data and the key used to store it.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param key A pointer to the key used when inserting the data.
     *
     * @returns HTERR_NONE if the data was successfully deleted. HTERR_NOSUCHVALUE if no entry under the given key was
     * found.
    */
    HashtableErrorCodes (*remove)(Hashtable *const this, void *key);

    /**
     * Clears the hash table. All data will be deallocated by the key and data destroyer function provided during the
     * hash table's creation.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     *
     * @returns None
    */
    void (*clear)(Hashtable *const this);
};

/**
 * Creates a hashtable. Takes a size. Optionally takes a hasher function to customize how indexes are allocated, a key
 * comparer function to customize how key equality is evaluated, and a key and data destroyer function to enable complex
 * deallocation sequences beyond a simple call to free (e.g., deallocating struct members) during data removal.
 *
 * @param capacity Specifies the capacity of the hashtable. Overflow will be stored as additional elements in a linked
 * list.
 * @param hasher A function to hash keys during data entry into the hashtable. Must return an index less than the value
 * of capacity.
 * @param keyComparer A function to compare the equality of keys.
 * @param keyDestroyer A function to deallocate the key during data removal.
 * @param dataDestroyer A function to deallocate data during data removal.
 *
 * @returns Returns a pointer to a Hashtable if successful, otherwise returns a NULL pointer.
*/
Hashtable *newHashtable(const size_t capacity,
                        size_t (*const hasher)(void *const key, const size_t maxIndex),
                        bool (*keyComparer)(void *const a, void *const b),
                        void (*keyDestroyer)(void *key),
                        void (*dataDesroyer)(void *data));

/**
 * Deletes a hashtable. Takes a pointer to a hashtable. All data in the hash table will be deallocated by the data destroyer
 * function provided during the hash table's creation.
 *
 * @param table A pointer to a pointer to the hashtable to be deleted.
 * @returns None
*/
void deleteHashtable(Hashtable *const table);

HashtableEntry *newHashtableEntry(void *const key, void *const data);

#endif

hashtable.c:

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

#include "hashtable.h"
#include "linkedlist.h"

void ht_default_destructor(Hashtable *const this) {
    if (this == NULL) return;

    for (size_t i = 0; i < this->capacity; i++) {
        deleteLinkedList(this->data[i]);
        this->data[i] = NULL;
    }
}

size_t ht_default_hasher(void *const key, const size_t capacity) {
    size_t *const castedKey = (size_t *const) key;
    return *castedKey % capacity;
}

bool ht_default_keyComparer(void *const a, void *const b) {
    size_t *const castedA = (size_t *const) a;
    size_t *const castedB = (size_t *const) b;
    return castedA == castedB;
}

void ht_default_keyDestroyer(void *key) {
    free(key);
}

void ht_default_dataDestroyer(void *data) {
    HashtableEntry *castedData = (HashtableEntry *) data;
    free(castedData->data);
    free(castedData);
}

HashtableErrorCodes ht_default_add(Hashtable *const this, void *const key, void *const data) {
    if (this == NULL) return HTERR_NOTHIS;

    const size_t index = this->hasher(key, this->capacity);

    if (this->data[index] == NULL) {
        LinkedList *const list = newLinkedList(NULL, this->dataDestroyer);
        if (list == NULL) return HTERR_BADALLOC;

        this->data[index] = list;
    }

    HashtableEntry *const entry = newHashtableEntry(key, data);
    if (entry == NULL) {
        // Entry allocation might have failed, but the list is still good for next time so no need to clean up
        return HTERR_BADALLOC;
    }

    const LinkedListErrors res = this->data[index]->addUnique(this->data[index]->this, entry);
    if (res == LLERR_DUPENT) {
        // This entry under this key already exists
        return HTERR_DUPENT;
    } else if (res != LLERR_NONE) {
        // Something else went wrong, assume list failed to allocate node
        return HTERR_BADALLOC;
    }

    return HTERR_NONE;
}

HashtableErrorCodes ht_default_find(Hashtable *const this, void *const key, void **const data) {
    if (this == NULL) return HTERR_NOTHIS;

    const size_t index = this->hasher(key, this->capacity);
    LinkedList *const list = this->data[index];

    if (list == NULL) {
        *data = NULL;
        return HTERR_NOSUCHVALUE;
    }

    LinkedListNode *node = list->head;
    while (node != NULL) {
        HashtableEntry *entry = node->data;
        node = node->next;

        if (entry != NULL) {
            if (this->keyComparer(entry->key, key)) {
                *data = entry->data;
                return HTERR_NONE;
            }
        }
    }

    *data = NULL;
    return HTERR_NOSUCHVALUE;
}

HashtableErrorCodes ht_default_remove(Hashtable *const this, void *key) {
    if (this == NULL) return HTERR_NOTHIS;

    const size_t htIndex = this->hasher(key, this->capacity);
    LinkedList *const list = this->data[htIndex];

    if (list == NULL) return HTERR_NOSUCHVALUE;

    bool llFound = false;
    size_t llIndex = 0;
    LinkedListNode *node = list->head;
    while (node != NULL) {
        HashtableEntry *entry = node->data;
        if (this->keyComparer(entry->key, key)) {
            llFound = true;
            break;
        }

        llIndex++;
        node = node->next;
    }

    if (llFound) {
        const LinkedListErrors res = list->removeByIndex(list->this, llIndex);

        if (res != LLERR_NONE) {
            // it probably didn't exist
            return HTERR_NOSUCHVALUE;
        }

        return HTERR_NONE;
    }

    return HTERR_NOSUCHVALUE;
}

void ht_default_clear(Hashtable *const this) {
    if (this == NULL) return;

    for (size_t i = 0; i < this->capacity; i++) {
        if (this->data[i] == NULL) continue;

        deleteLinkedList(this->data[i]);
        this->data[i] = NULL;
    }
}

Hashtable *newHashtable(const size_t capacity,
                        size_t (*const hasher)(void *const key, const size_t maxIndex),
                        bool (*keyComparer)(void *const a, void *const b),
                        void (*keyDestroyer)(void *key),
                        void (*dataDestroyer)(void *data)) {
    Hashtable *const ht = malloc(sizeof(*ht));
    if (ht == NULL) return NULL;

    ht->this = ht;

    ht->destructor = ht_default_destructor;
    ht->hasher = hasher != NULL ? hasher : ht_default_hasher;
    ht->keyComparer = keyComparer != NULL ? keyComparer : ht_default_keyComparer;
    ht->keyDestroyer = keyDestroyer != NULL ? keyDestroyer : ht_default_keyDestroyer;
    ht->dataDestroyer = dataDestroyer != NULL ? dataDestroyer : ht_default_dataDestroyer;

    ht->add = ht_default_add;
    ht->find = ht_default_find;
    ht->remove = ht_default_remove;
    ht->clear = ht_default_clear;

    ht->data = calloc(capacity, sizeof(*(ht->data)));
    ht->capacity = capacity;

    return ht;
}

void deleteHashtable(Hashtable *const table) {
    if (table == NULL) return;
    table->destructor(table->this);
}

HashtableEntry *newHashtableEntry(void *const key, void *const data) {
    HashtableEntry *const entry = malloc(sizeof(*entry));
    if (entry == NULL) return NULL;

    entry->key = key;
    entry->data = data;

    return entry;
}

linkedlist.h (includes suggested improvements + more to accomodate hashtable's needs):

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

#ifndef LINKEDLIST_H
#define LINKEDLIST_H

typedef enum LinkedListErrors {
    LLERR_NONE,
    LLERR_NOTHIS,
    LLERR_BADALLOC,
    LLERR_NOSUCHVALUE,
    LLERR_BADINDEX,
    LLERR_DUPENT
} LinkedListErrors;

typedef struct LinkedList LinkedList;
typedef struct LinkedListNode LinkedListNode;

struct LinkedListNode {
    LinkedListNode *next;

    void *data;
};

struct LinkedList {
    LinkedList *this;

    LinkedListNode *head;
    size_t size;

    void (*destructor)(LinkedList *const this);
    int (*dataComparer)(void *const a, void *const b);
    void (*dataDestroyer)(void *const data);

    /**
     * Inserts data to the linked list at the tail end. Takes a pointer to the data to be inserted. Duplicate data may
     * be inserted.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param data A pointer to the data to be inserted.
     *
     * @returns Returns LLERR_NONE if successful. LLERR_BADALLOC if space allocation to store the data failed.
    */
    LinkedListErrors (*add)(LinkedList *const this, void *const data);

    /**
     * Similar to the regular add function. Duplicate data may not be inserted, however it is up to the caller to decide
     * how the data should be treated should insertion fail. Data will not be deallocated if insertion fails.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param data A pointer to the data to be inserted.
     *
     * @returns Returns LLERR_NONE if successful. LLERR_BADALLOC if space allocation to store the data failed. LLERR_DUPENT
     * if the data being inserted already exists.
    */
    LinkedListErrors (*addUnique)(LinkedList *const this, void *const data);

    /**
     * Finds previously inserted data. Takes a filtering function. Outputs data via a pointer to a pointer to the data.
     * Optionally writes the index at which the data is found to a pointer.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param comparer A filtering function.
     * @param data An output pointer that may point to a pointer to the previously inserted data.
     * @param index An output pointer that may point to a number indicating the index at which the data was found.
     *
     * @returns Returns LLERR_NONE if successful. LLERR_NOSUCHVALUE if the comparer function provided didn't find any
     * matches, in which case the output data pointer will be set to NULL and the value pointed to by the output index
     * pointer set to 0.
    */
    LinkedListErrors (*find)(LinkedList *const this, bool (*comparer)(void *const data), void **const data, size_t *const index);

    /**
     * Removes previously inserted data. Takes an index.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param index The index of the data to be deleted. Must be a valid index.
     *
     * @returns Returns LLERR_NONE if successful. LLERR_BADINDEX if the index provided is invalid. LLERR_NOSUCHVALUE if
     * no data was found under the given index.
    */
    LinkedListErrors (*removeByIndex)(LinkedList *const this, const size_t index);

    /**
     * Removes previously inserted data. Takes a sample object which will be evaluated for equality by the data comparer
     * function provided during the linked list's creation.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     * @param object A pointer to the sample object.
     *
     * @returns Returns LLERR_NONE if successful. LLERR_NOSUCHVALUE if the sample object provided is not found to be
     * equal to any object in the linked list by the data comparer function provided during the linked list's creation.
    */
    LinkedListErrors (*removeByObject)(LinkedList *const this, void *const object);

    /**
     * Clears the linked list. All data will be deallocated by the data destroyer function provided during the linked
     * list's creation.
     *
     * @param this Similar to C++'s implicit this parameter in class non-static functions.
     *
     * @returns None
    */
    void (*clear)(LinkedList *const this);
};

/**
 * Creates a linked list. Optionally takes a data comparer function to customize how data equality is evaluated and a
 * data destroyer function to enable complex data deallocation sequences beyond a simple call to free on the data (e.g.,
 * deallocating struct members) during data removal.
 *
 * @param dataComparer A function to compare the equality of data.
 * @param dataDestroyer A function to deallocate data during data removal.
 *
 * @returns Returns a pointer to a linked list if successful, otherwise returns a NULL pointer.
*/
LinkedList *newLinkedList(int (*const dataComparer)(void *const a, void *const b), void (*const dataDestroyer)(void *data));

/**
 * Deletes a linked list. Takes a pointer to a linked list. All data in the linked list will be deallocated by the data
 * destroyer function provided during the linked list's creation.
 *
 * @param list A pointer to the linked list to be deleted.
 *
 * @returns None
*/
void deleteLinkedList(LinkedList *const list);

#endif

linkedlist.c:

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

#include "linkedlist.h"

void ll_default_destructor(LinkedList *const this) {
    if (this == NULL) return;

    LinkedListNode *node = this->head;
    while (node != NULL) {
        LinkedListNode *prev = node;
        node = node->next;
        this->dataDestroyer(prev->data);
        free(prev);
    }

    this->head = NULL;
}

int ll_default_dataComparer(void *const a, void *const b) {
    int *const castedA = (int *const) a;
    int *const castedB = (int *const) b;

    return (*castedA > *castedB) - (*castedA < *castedB);
}

void ll_default_dataDestroyer(void *const data) {
    free(data);
}

LinkedListErrors ll_default_add(LinkedList *const this, void *const data, const bool unique) {
    if (this == NULL) return LLERR_NOTHIS;

    LinkedListNode *const newNode = malloc(sizeof(LinkedListNode));
    if (newNode == NULL) return LLERR_BADALLOC;

    newNode->next = NULL;
    newNode->data = data;

    LinkedListNode *prevNode = NULL;
    LinkedListNode *currNode = this->head;

    if (currNode == NULL) {
        this->head = newNode;
    } else {
        while (currNode != NULL) {
            if (unique) {
                if (this->dataComparer(currNode->data, data) == 0) {
                    free(newNode);

                    return LLERR_DUPENT;
                }
            }

            prevNode = currNode;
            currNode = currNode->next;
        }

        prevNode->next = newNode;
    }

    this->size++;

    return LLERR_NONE;
}

LinkedListErrors ll_default_addNonUnique(LinkedList *const this, void *const data) {
    return ll_default_add(this, data, false);
}

LinkedListErrors ll_default_addUnique(LinkedList *const this, void *const data) {
    return ll_default_add(this, data, true);
}

LinkedListErrors ll_default_find(LinkedList *const this, bool (*comparer)(void *const data), void **const data, size_t *const index) {
    if (this == NULL) return LLERR_NOTHIS;

    LinkedListNode *node = this->head;

    size_t currentIndex = 0;
    while (node != NULL) {
        if (comparer(node->data)) {
            if (data != NULL) *data = node->data;
            if (index != NULL) *index = currentIndex;

            return LLERR_NONE;
        }

        node = node->next;
        currentIndex++;
    }

    if (data != NULL) *data = NULL;
    if (index != NULL) *index = 0;
    return LLERR_NOSUCHVALUE;
}

LinkedListErrors ll_default_removeByIndex(LinkedList *const this, const size_t index) {
    if (this == NULL) return LLERR_NOTHIS;

    if (index >= this->size) return LLERR_BADINDEX;

    LinkedListNode *prevNode = NULL;
    LinkedListNode *currNode = this->head;

    for (size_t i = 0; i < index; i++) {
        if (currNode == NULL) return LLERR_NOSUCHVALUE;
        prevNode = currNode;
        currNode = currNode->next;
    }

    if (prevNode != NULL) {
        prevNode->next = currNode->next;

        this->dataDestroyer(prevNode->data);
    } else {
        this->head = currNode->next;
    }

    free(prevNode);
    this->size--;

    return LLERR_NONE;
}

LinkedListErrors ll_default_removeByObject(LinkedList *const this, void *const object) {
    if (this == NULL) return LLERR_NOTHIS;

    LinkedListNode *prevNode = NULL;
    LinkedListNode *currNode = this->head;

    while (currNode != NULL) {
        if (this->dataComparer(currNode->data, object) == 0) {
            if (prevNode != NULL) {
                prevNode->next = currNode->next;

                this->dataDestroyer(currNode->data);
            } else {
                this->head = currNode->next;
            }

            free(currNode);
            this->size--;

            return LLERR_NONE;
        }

        prevNode = currNode;
        currNode = currNode->next;
    }

    return LLERR_NOSUCHVALUE;
}

void ll_default_clear(LinkedList *const this) {
    if (this == NULL) return;

    LinkedListNode *prevNode = this->head;
    LinkedListNode *currNode = this->head;

    while (currNode != NULL) {
        prevNode = currNode;
        currNode = currNode->next;

        this->dataDestroyer(prevNode->data);
        free(prevNode);
    }

    this->head = NULL;
}

LinkedList *newLinkedList(int (*const dataComparer)(void *const a, void *const b), void (*const dataDestroyer)(void *data)) {
    LinkedList *const list = malloc(sizeof(LinkedList));
    if (list == NULL) return NULL;

    list->this = list;
    list->destructor = ll_default_destructor;
    list->dataComparer = dataComparer != NULL ? dataComparer : ll_default_dataComparer;
    list->dataDestroyer = dataDestroyer != NULL ? dataDestroyer : ll_default_dataDestroyer;

    list->add = ll_default_addNonUnique;
    list->addUnique = ll_default_addUnique;
    list->find = ll_default_find;
    list->removeByIndex = ll_default_removeByIndex;
    list->removeByObject = ll_default_removeByObject;
    list->clear = ll_default_clear;

    list->head = NULL;
    list->size = 0;

    return list;
}

void deleteLinkedList(LinkedList *const list) {
    if (list == NULL) return;
    list->destructor(list->this);
}

main.c (testing code is not as rigorously written as the main hashtable code):

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

#include "hashtable.h"

typedef struct Pizza {
    char *name;
    char *flavor;
    size_t size;
} Pizza;

Pizza *newPizza(char *name, char *flavor, size_t size) {
    Pizza *p = malloc(sizeof(*p));

    // p->name = malloc((strlen(name) + 1) * sizeof(*(p->name)));
    // p->name = malloc(256);
    // strcpy(p->name, name);
    p->name = strdup(name);

    // p->flavor = malloc(strlen(flavor) + 1 * sizeof(*(p->flavor)));
    // p->flavor = malloc(256);
    // strcpy(p->flavor, flavor);
    p->flavor = strdup(flavor);

    p->size = size;

    return p;
}

size_t hash(void *key, size_t maxIndex) {
    char *castedKey = (char *) key;
    size_t len = strlen(castedKey);

    size_t h = 0;
    for (size_t i = 0; i < len; i++) h += castedKey[i];
    h %= maxIndex;

    return h;
}

bool keycompare(void *a, void *b) {
    char *castedA = (char *) a;
    char *castedB = (char *) b;
    return strcmp(castedA, castedB) == 0;
}

void keynt(void *key) {
    // stub, since our key is part of our data,
    // which will be deallocated by the data
    // destroyer
}

void datant(void *data) {
    Pizza *castedData = (Pizza *) data;
    free(castedData->name);
    free(castedData->flavor);
    free(castedData);
}

void printpizza(Pizza *p) {
    if (p == NULL) printf("NULL\n"); else
    printf("{ name: %s, flavor: %s, size: %zu }\n\n", p->name, p->flavor, p->size);
}

void printtable(Hashtable *ht) {
    printf("[\n");
    for (size_t i = 0; i < ht->capacity; i++) {
        printf("\t%zu: [\n", i);

        LinkedList *ll = ht->data[i];

        if (ll == NULL) {
            printf("\t\tNULL,\n");
        } else {
            LinkedListNode *node = ll->head;
            while (node != NULL) {
                HashtableEntry *hte = (HashtableEntry *) node->data;
                Pizza *p = (Pizza *) hte->data;
                printf("\t\t{\n\t\t\tname: %s,\n\t\t\tflavor: %s,\n\t\t\tsize: %zu\n\t\t},\n", p->name, p->flavor, p->size);
                node = node->next;
            }
        }

        printf("\t],\n\n");
    }
    printf("]\n\n");
}

int main(void) {
    Hashtable *table = newHashtable(3, hash, keycompare, keynt, datant);

    Pizza *p1 = newPizza("Pizzus Tinyeus", "Bland", 69);
    table->add(table->this, p1->name, p1);

    Pizza *p2 = newPizza("Pizzus Smallus", "Sour", 420);
    table->add(table->this, p2->name, p2);

    Pizza *p3 = newPizza("Pizzus Biggus", "Spicy", 42069);
    table->add(table->this, p3->name, p3);

    Pizza *p4 = newPizza("Pizzus Enormous", "Magic", 69420);
    table->add(table->this, p4->name, p4);

    // do we have all the pizzas?

    printf("const pizzas = ");
    printtable(table);

    // can we insert a duplicate pizza?

    HashtableErrorCodes err = table->add(table->this, p4->name, p4);
    printf("err = %d\n\n", err);
    printf("const pizzasagain = ");
    printtable(table);

    // can we find a pizza?

    Pizza *p;
    table->find(table->this, "Pizzus Biggus", &p);
    printf("const pizza = ");
    printpizza(p);

    // can we delete a pizza?

    table->remove(table->this, "Pizzus Enormous");
    printf("const pizzasyetagain = ");
    printtable(table);

    // can we delete everything?

    table->clear(table->this);
    printf("const nopizzas = ");
    printtable(table);

    // not sure how to assert it's really gone with no leaks but at least it doesn't crash
    deleteHashtable(table->this);

    return 0;
}

Output of testing code as generated by ./main > res.txt:

const pizzas = [
    0: [
        {
            name: Pizzus Tinyeus,
            flavor: Bland,
            size: 69
        },
        {
            name: Pizzus Biggus,
            flavor: Spicy,
            size: 42069
        },
    ],

    1: [
        {
            name: Pizzus Enormous,
            flavor: Magic,
            size: 69420
        },
    ],

    2: [
        {
            name: Pizzus Smallus,
            flavor: Sour,
            size: 420
        },
    ],

]

err = 4

const pizzasagain = [
    0: [
        {
            name: Pizzus Tinyeus,
            flavor: Bland,
            size: 69
        },
        {
            name: Pizzus Biggus,
            flavor: Spicy,
            size: 42069
        },
    ],

    1: [
        {
            name: Pizzus Enormous,
            flavor: Magic,
            size: 69420
        },
    ],

    2: [
        {
            name: Pizzus Smallus,
            flavor: Sour,
            size: 420
        },
    ],

]

const pizza = { name: Pizzus Biggus, flavor: Spicy, size: 42069 }

const pizzasyetagain = [
    0: [
        {
            name: Pizzus Tinyeus,
            flavor: Bland,
            size: 69
        },
        {
            name: Pizzus Biggus,
            flavor: Spicy,
            size: 42069
        },
    ],

    1: [
    ],

    2: [
        {
            name: Pizzus Smallus,
            flavor: Sour,
            size: 420
        },
    ],

]

const nopizzas = [
    0: [
        NULL,
    ],

    1: [
        NULL,
    ],

    2: [
        NULL,
    ],

]

The linked list part is included as a dependency, the main part I'm looking for a review for is the hashtable itself (I'll post a separate request for the improved linked list).

This time around I've identified a few of my own concerns while writing this:

  1. The original linked list interface was rather unusable to implement the hashtable. After a few moments of scratching my head I concluded it was easier / faster to access the list myself rather than use its API, however, is there anything I can do to salvage it? The API is good enough for generic usage, however not very good to use as a base for other data structures, such as this hashtable.

  2. One of the suggestions given for my linked list was to use const where possible. Have I over/under done it here? Are there places where there could be const and yet I left it out or vice versa?

Thank you for your time.

PS: Functions with comments in the header files are what I consider to be the "public APIs", while those without I consider "internal functions".

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Use const for referenced data when possible

Example *keyComparer(): Certainly this compare does not change the keys. With bool (*keyComparer)(void *a, void *b), code is obliged to provide a compare function that points to non-const data, when ready-to-use robust compare functions use pointers to const data. See qsort() and bsearch() for C standard parameters examples: int (*compar)(const void *, const void *).

// bool (*keyComparer)(void *a, void *b);
bool (*keyComparer)(const void *a, const void *b);

const not needed in function declaration object parameters

Consider the 2 declarations. Both have the same function signature, yet what is easier for the user to read?

HashtableErrorCodes (*add)(Hashtable *this, void *const key, void *const data);
HashtableErrorCodes (*add)(Hashtable *this, void *key, void *data);

With function definitions, use const object parameters as desired. With short functions, I find it overly verbose.

const

To be clear there is a difference about const. Is it part of referenced data, or part of the object?

const type *ptr;  // Good if referenced data not changed.
type const object; // Useful for _definitions_ of large functions to emphasize constancy of `object`.

I think code is too often using type * const ptr when it should use const type *ptr. If still unclear of the difference, ask.

#include "linkedlist.h" should not be needed in hashtable.h

With good information hiding, the public does not need to see the internals of struct HashtableEntry

More stand-alone documentation

Instead of describing C code in terms of another language for the public as in "@param this Similar to C++'s implicit this parameter in class non-static functions.", describe it in a stand-alone fashion. Do not oblige users of your C code to know another language like C++. Capisce?

Good to see the .h file commented.

More consistent naming

hashtable.h introduces 4 prefixes. I recommend a single, possibly camel-case one and a 2nd, all upper case one if desired.

  • hashtable...
  • HASHTABLE...
  • Hashtable...
  • HTERR...

Still, better than most implementations with widely dispersed names.

hashtable.h first

In hashtable.h, useful to include hashtable.h first, before other .h files to test that it stands-alone and does not oblige other code to include other files.

In other .c, .h files, #include "hashtable.h" need not be first.

Same for linkedlist.h in linkedlist.c.

Style: if (data != NULL) or if (data)

I find negations tend to muddle understanding and so promote:

if (data) // Testing is data is non-NULL

if (data == NULL) // Testing is data is NULL

Style: Curious blank lines

Blank line (here and other places) look like mistakes.

        if (prevNode != NULL) {
            prevNode->next = currNode->next;

            this->dataDestroyer(currNode->data);

Style: Using an auto formatter or manual?

Style looks uniform, but enough there to imply manual formatting - which is best avoided.

enum_N

Often useful to have a enum to represent the number of possible errors. It simplifies range checking and creating an array of strings to match.

typedef enum LinkedListErrors {
    LLERR_NONE,
    LLERR_NOTHIS,
    LLERR_BADALLOC,
    LLERR_NOSUCHVALUE,
    LLERR_BADINDEX,
    LLERR_DUPENT,
    LLERR_N            // add
} LinkedListErrors;
\$\endgroup\$
8
  • \$\begingroup\$ Re: "const not needed in function declaration object parameters": Would it not be confusing / surprising if the declaration and definition had different const-ness of parameters? Re: "I think code is too often using type * const ptr when it should use const type *ptr.": Would const type *const ptr be over-verbose? The idea being that neither the object nor the pointer should / needs to change. \$\endgroup\$ Commented Dec 31, 2022 at 4:06
  • \$\begingroup\$ Re: "#include "linkedlist.h" should not be needed in hashtable.h": Where do you propose I put it then? The hashtable depends on the linked list in the struct definition. Re: "... the public does not need to see the internals of struct HashtableEntry": Where do you propose I put it then? In hashtable.c? \$\endgroup\$ Commented Dec 31, 2022 at 4:09
  • \$\begingroup\$ Re: "hashtable.h introduces 4 prefixes": I might be misunderstanding but I only see 2 prefixes, Hashtable* and HTERR*. hashtable only shows up in the comments and file name when I do a case sensitive search in the file, while HASHTABLE only exists in the include guard. Maybe you're counting more things than I am? \$\endgroup\$ Commented Dec 31, 2022 at 4:14
  • \$\begingroup\$ Re: "I find negations tend to muddle understanding ...": Maybe it's just me, but I find that explicitly testing for values such as if (x == NULL) or if (x != NULL) faster to read than waiting for myself to realize the statement testing for truthy or falsey such as if (x) or if (!x) corresponds to which explicit value test. \$\endgroup\$ Commented Dec 31, 2022 at 4:19
  • \$\begingroup\$ @404NameNotFound "Would it not be confusing / surprising if the declaration and definition had different const-ness of parameters?" --> No, different const of parameters is not an issue. Different const of what a pointer points to is a problem. \$\endgroup\$
    – chux
    Commented Dec 31, 2022 at 4:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.