vector final

This commit is contained in:
2026-08-19 00:38:36 +02:00
parent a0d88e7cbf
commit 7f0c576b10
+224 -38
View File
@@ -16,12 +16,42 @@ Guarantees:
which will transfer ownership of the data to the new vector.
- Deep copies may be made with vector_deep_copy(), which will copy the data as
well as the vector itself. This is slower than a shallow copy, but safer.
Deep copies are only available for vectors without an element destructor, as
a byte-wise copy of owning elements would result in a double free.
- No gaps in the raw data array.
- When a vector is resized, the data is reallocated to a new memory location.
This means that any pointers to the old data will be invalidated. If you want
to keep using the old data, you should make a deep copy of the vector before
resizing it.
- Order of elements is preserved.
- It is safe to pass a pointer into a vector's own data as the source element
of vector_push_back(), vector_insert() and vector_set(). The library detects
this and copies the value before it can be invalidated or overwritten.
On a vector with an element destructor this is rejected with -1 instead,
as a byte-wise copy would leave two slots owning the same memory. To
duplicate an owning element, copy what it owns yourself and push that.
Element destructors:
A vector may be given an element destructor with vector_set_destructor(). It is
called for every element that leaves the vector, i.e. by vector_pop_back(),
vector_pop_at(), vector_set() (on the element being overwritten), vector_clear()
and vector_destroy().
The destructor receives a pointer to the element's slot inside the vector's data
array - NOT a pointer that was returned by malloc(). It must free whatever the
element owns, and must never free the pointer it was handed, as that memory
belongs to the vector. For a vector of char* this means:
void free_str(void* element) { free(*(char**)element); }
vector_t* vec = vector_create(sizeof(char*));
vector_set_destructor(vec, free_str);
A warning regarding misuse:
The vector manages its own buffer correctly: no leaks, no double frees of the data
array, no use of stale pointers internally. It cannot reason about what your
elements own — that is what the destructor is for, and it is your responsibility
to set one and to avoid duplicating owned pointers between slots.
License:
MIT License
@@ -64,6 +94,55 @@ typedef struct {
vector_destructor_t destructor; // Function pointer to the element destructor
} vector_t;
/* Forward declarations */
static inline vector_t* vector_create(size_t element_size);
static inline int vector_set_destructor(vector_t* vec, vector_destructor_t destructor);
static inline vector_destructor_t vector_get_destructor(const vector_t* vec);
static inline int vector_reserve(vector_t* vec, size_t new_capacity);
static inline int vector_prune(vector_t* vec);
static inline int vector_push_back(vector_t* vec, const void* element);
static inline int vector_pop_back(vector_t* vec);
static inline int vector_pop_at(vector_t* vec, size_t index);
static inline int vector_insert(vector_t* vec, size_t index, const void* element);
static inline int vector_clear(vector_t* vec);
static inline int vector_set(vector_t* vec, size_t index, const void* element);
static inline int vector_is_empty(const vector_t* vec);
static inline void* vector_front(vector_t* vec);
static inline const void* vector_front_const(const vector_t* vec);
static inline void* vector_back(vector_t* vec);
static inline const void* vector_back_const(const vector_t* vec);
static inline void* vector_get(vector_t* vec, size_t index);
static inline const void* vector_get_const(const vector_t* vec, size_t index);
static inline size_t vector_size(const vector_t* vec);
static inline size_t vector_capacity(const vector_t* vec);
static inline size_t vector_element_size(const vector_t* vec);
static inline const void* vector_as_c_array(const vector_t* vec);
static inline void* vector_as_c_array_mutable(vector_t* vec);
static inline int vector_move(vector_t* dest, vector_t** src);
static inline vector_t* vector_deep_copy(const vector_t* vec);
static inline int vector_destroy(vector_t* vec);
/*
@brief Checks whether a pointer points inside the vector's own data array. Used internally to make the write functions safe against self-referential input.
@param vec A pointer to the vector to check against.
@param ptr The pointer to check.
@return 1 if the pointer lies within the vector's allocated buffer, 0 otherwise.
@attention This is an internal helper. You are not expected to call it directly, but it is harmless if you do.
*/
static inline int vector_is_aliased(const vector_t* vec, const void* ptr) {
if (!vec || !ptr || !vec->data) {
return 0; // Nothing to alias
}
// Compared as integers rather than pointers, as comparing pointers into
// different objects is not well defined.
uintptr_t base = (uintptr_t)vec->data;
uintptr_t end = base + (vec->capacity * vec->element_size);
uintptr_t target = (uintptr_t)ptr;
return target >= base && target < end;
}
/*
@brief Creates a new vector with the specified element size.
@param element_size The size of each element in the vector. Call with sizeof(type)
@@ -71,15 +150,10 @@ typedef struct {
@attention The vector must be destroyed with vector_destroy() to free its memory. Failing to do so will result in a memory leak.
@attention The vector's initial capacity is set to 10. If you want to change the initial capacity globally, set the DLIBC_VECTOR_INITIAL_CAPACITY macro before including this header file. The initial capacity must be greater than 0.
@attention The vector's element size must be greater than 0. If you pass 0, the function will return NULL.
@attention The vector is created without an element destructor. If your elements own memory of their own, set one with vector_set_destructor().
*/
static inline vector_t* vector_create(size_t element_size) {
vector_t* vec = (vector_t*)malloc(sizeof(vector_t));
if (!vec) {
return NULL; // Allocation failed
}
if (element_size == 0) {
free(vec);
return NULL; // Invalid element size
}
@@ -89,19 +163,23 @@ static inline vector_t* vector_create(size_t element_size) {
#if DLIBC_VECTOR_INITIAL_CAPACITY <= 0
#error "DLIBC_VECTOR_INITIAL_CAPACITY must be greater than 0"
#endif
size_t initial_capacity = DLIBC_VECTOR_INITIAL_CAPACITY > 0 ? DLIBC_VECTOR_INITIAL_CAPACITY : 10; // Use the macro if defined and greater than 0, otherwise default to 10
size_t initial_capacity = DLIBC_VECTOR_INITIAL_CAPACITY; // Use the macro if defined
#endif
if (SIZE_MAX / element_size < initial_capacity) {
free(vec);
return NULL; // Prevent overflow
}
vector_t* vec = (vector_t*)malloc(sizeof(vector_t));
if (!vec) {
return NULL; // Allocation failed
}
vec->size = 0;
vec->capacity = initial_capacity;
vec->element_size = element_size;
vec->data = malloc(vec->capacity * vec->element_size);
vec->destructor = NULL; // Initialize destructor to NULL
vec->data = malloc(vec->capacity * vec->element_size);
if (!vec->data) {
free(vec);
return NULL; // Allocation failed
@@ -111,22 +189,35 @@ static inline vector_t* vector_create(size_t element_size) {
}
/*
@brief Sets the destructor function for the vector's elements. This function will be called on each element when the vector is destroyed, allowing for custom cleanup of dynamically allocated memory within the elements.
@brief Sets the destructor function for the vector's elements. This function will be called on each element as it leaves the vector, allowing for custom cleanup of dynamically allocated memory within the elements.
@param vec A pointer to the vector for which to set the destructor.
@param destructor A function pointer to the destructor function. The function should take a single void* parameter, which will be a pointer to the element to be destroyed.
@param destructor A function pointer to the destructor function, or NULL to remove the current one. The function should take a single void* parameter, which will be a pointer to the element to be destroyed.
@return 0 on success, -1 if the vector is NULL.
@attention If you do not set a destructor function, the vector will not automatically free any dynamically allocated memory within its elements when it is destroyed. You must ensure that you free any such memory manually before destroying the vector to avoid memory leaks.
@attention Honestly, you can just directly assign this with vec->destructor = destructor; but this is a more "user-friendly" way to do it.
@attention The destructor is passed a pointer to the element's slot inside the vector's data array, not a pointer returned by malloc(). It must free what the element owns and must never free the pointer it is given. For a vector of char*, the destructor body is free(*(char**)element);
@attention This assumes that your destructor function is valid and safe to call. If it isn't, bad things will happen and it will not be nice to watch.
@attention Set the destructor before adding any elements. Setting it on a vector that already holds elements is allowed, but elements that were removed beforehand will not have been destroyed.
*/
static inline int vector_set_destructor(vector_t* vec, vector_destructor_t destructor) {
if (vec) {
vec->destructor = destructor;
} else {
if (!vec) {
return -1; // Invalid vector
}
return 0;
vec->destructor = destructor;
return 0; // Success
}
/*
@brief Gets the destructor function currently set on the vector.
@param vec A pointer to the vector whose destructor is to be retrieved.
@return The vector's destructor function pointer, or NULL if the vector is NULL or has no destructor set.
*/
static inline vector_destructor_t vector_get_destructor(const vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
return vec->destructor;
}
/*
@@ -195,19 +286,36 @@ static inline int vector_prune(vector_t* vec) {
@brief Adds an element to the end of the vector.
@param vec A pointer to the vector to which the element will be added.
@param element A pointer to the element to be added. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, or if reservation fails.
@return 0 on success, -1 if the vector is NULL, the element is NULL, if the element is an owning element that is aliased in the vector and a destructor is set, or if reservation fails.
@attention The element must be a pointer to a valid memory location containing data of the same type as the vector's element type. The vector will make a copy of the data, so the original element can be modified or freed after this function returns.
@attention The element may point into the vector's own data array. If growing the vector moves the data, the pointer is followed to its new location automatically.
*/
static inline int vector_push_back(vector_t* vec, const void* element) {
if (!vec || !element) {
return -1; // Invalid vector or element
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to push an owning element that lives inside the vector
}
if (vec->size >= vec->capacity) {
// If the element lives inside our own data array, remember where it sits
// so that we can find it again after the data has been reallocated
size_t offset = 0;
int aliased = vector_is_aliased(vec, element);
if (aliased) {
offset = (size_t)((const char*)element - (const char*)vec->data);
}
// Reserve space for the vector to double its current capacity
if (vector_reserve(vec, vec->capacity * 2) != 0) {
return -1; // Reserve failed
}
if (aliased) {
element = (const char*)vec->data + offset;
}
}
// Copy the new element into the vector's data array
@@ -221,7 +329,7 @@ static inline int vector_push_back(vector_t* vec, const void* element) {
@brief Removes the last element from the vector.
@param vec A pointer to the vector from which the element will be removed.
@return 0 on success, -1 if the vector is NULL or empty.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically unless a destructor is set.
*/
static inline int vector_pop_back(vector_t* vec) {
if (!vec || vec->size == 0) {
@@ -242,7 +350,7 @@ static inline int vector_pop_back(vector_t* vec) {
@param vec A pointer to the vector from which the element will be removed.
@param index The index of the element to be removed.
@return 0 on success, -1 if the vector is NULL or the index is out of bounds.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically unless a destructor is set.
*/
static inline int vector_pop_at(vector_t* vec, size_t index) {
if (!vec || index >= vec->size) {
@@ -267,18 +375,38 @@ static inline int vector_pop_at(vector_t* vec, size_t index) {
@brief Inserts an element at the specified index in the vector.
@param vec A pointer to the vector into which the element will be inserted.
@param index The index at which to insert the element.
@param element A pointer to the element to be inserted.
@return 0 on success, -1 if the vector is NULL, the element is NULL, or if reservation fails.
@attention After calling this function, the vector's size will be increased by one. The memory occupied by the inserted element will not be freed automatically.
@param element A pointer to the element to be inserted. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, the index is out of bounds, if the element is an owning element that is aliased in the vector and a destructor is set, or if reservation fails.
@attention After calling this function, the vector's size will be increased by one and every element from the index onwards will have moved one position to the right. Any pointers into the vector are invalidated.
@attention The element may point into the vector's own data array. Its value is copied aside first, so that neither the reallocation nor the shifting of elements can corrupt it.
*/
static inline int vector_insert(vector_t* vec, size_t index, const void* element) {
if (!vec || !element || index > vec->size) {
return -1; // Invalid vector, element, or index out of bounds
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to insert an owning element that lives inside the vector
}
// If the element lives inside our own data array, take a copy of it now. Both
// the reservation below and the shifting of elements would otherwise destroy
// the value before it can be read
void* temp = NULL;
if (vector_is_aliased(vec, element)) {
temp = malloc(vec->element_size);
if (!temp) {
return -1; // Allocation failed
}
memcpy(temp, element, vec->element_size);
element = temp;
}
if (vec->size >= vec->capacity) {
// Reserve space for the vector to double its current capacity
if (vector_reserve(vec, vec->capacity * 2) != 0) {
free(temp);
return -1; // Reserve failed
}
}
@@ -292,6 +420,7 @@ static inline int vector_insert(vector_t* vec, size_t index, const void* element
memcpy((char*)vec->data + (index * vec->element_size), element, vec->element_size);
vec->size++;
free(temp); // Harmless if no copy was needed, as temp is NULL in that case
return 0; // Success
}
@@ -299,7 +428,8 @@ static inline int vector_insert(vector_t* vec, size_t index, const void* element
@brief Clears all elements from the vector.
@param vec A pointer to the vector to be cleared.
@return 0 on success, -1 if the vector is NULL.
@attention After calling this function, the vector's size will be zero. The memory occupied by the elements will not be freed automatically.
@attention After calling this function, the vector's size will be zero. The memory occupied by the elements will not be freed automatically unless a destructor is set.
@attention The vector's capacity is left untouched, so the vector may be refilled without reallocating. Call vector_prune() afterwards if you want the memory back.
*/
static inline int vector_clear(vector_t* vec) {
if (!vec) {
@@ -318,19 +448,39 @@ static inline int vector_clear(vector_t* vec) {
}
/*
@brief Sets the element at the specified index in the vector.
@brief Sets the element at the specified index in the vector, overwriting whatever was there before.
@param vec A pointer to the vector in which to set the element.
@param index The index of the element to set.
@param element A pointer to the element to set.
@return 0 on success, -1 if the vector is NULL, the element is NULL, or if the index is out of bounds.
@attention The memory occupied by the element will not be freed automatically.
@param element A pointer to the element to set. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, if the element is an owning element that is aliased in the vector and a destructor is set, or if the index is out of bounds.
@attention This never grows the vector. The index must be below the current size.
@attention If a destructor is set, it is called on the element being overwritten before the new value is copied in.
@attention Setting an element to itself is a no-op and reports success.
@attention On a vector without a destructor, the element may point into the
vector's own data array; the copy handles any overlap. On a vector with a
destructor this is refused with -1, as it would leave two slots owning the
same memory.
*/
static inline int vector_set(vector_t* vec, size_t index, const void* element) {
if (!vec || !element || index >= vec->size) {
return -1; // Invalid vector, element, or index out of bounds
}
memcpy((char*)vec->data + (index * vec->element_size), element, vec->element_size);
void* slot = (char*)vec->data + (index * vec->element_size);
if (slot == element) {
return 0; // Setting the element to itself, no action needed
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to set an owning element that lives inside the vector
}
if (vec->destructor) {
vec->destructor(slot); // Call the destructor for the element being overwritten
}
memmove(slot, element, vec->element_size); // memmove, as the element may overlap the slot
return 0; // Success
}
@@ -348,6 +498,34 @@ static inline int vector_is_empty(const vector_t* vec) {
return vec->size == 0;
}
/*
@brief Gets a pointer to the first element in the vector.
@param vec A pointer to the vector from which to get the element.
@return A pointer to the first element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline void* vector_front(vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return vec->data;
}
/*
@brief Gets a const pointer to the first element in the vector. Cannot be used to modify the element.
@param vec A pointer to the vector from which to get the element.
@return A constant pointer to the first element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline const void* vector_front_const(const vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return vec->data;
}
/*
@brief Gets a pointer to the last element in the vector.
@param vec A pointer to the vector from which to get the element.
@@ -475,12 +653,13 @@ static inline void* vector_as_c_array_mutable(vector_t* vec) {
}
/*
@brief Moves a vector to another vector, transferring ownership of the data. The source vector pointer can be safely discarded after this operation, as it will be set to NULL. The destination vector will take ownership of the source vector's data.
@brief Moves a vector to another vector, transferring ownership of the data. The destination vector will take ownership of the source vector's data, its element size and its destructor.
@param dest A pointer to the destination vector.
@param src A pointer to the source vector.
@return 0 on success, -1 if either vector is NULL.
@attention After calling this function, the source vector will be freed (excluding data). The source pointer will be set to NULL. The destination vector will take ownership of the source vector's data.
@attention The destination vector's existing data will be freed if it already has allocated memory. Ensure that you do not need the existing data before calling this function.
@param src A pointer to the pointer holding the source vector. It will be set to NULL.
@return 0 on success, -1 if the destination vector is NULL, the source pointer is NULL, or the source vector is NULL.
@attention After calling this function, the source vector will be freed (excluding data) and the source pointer will be set to NULL, so it cannot be used again.
@attention The destination vector's existing elements will be destroyed (using the destination's own destructor, if it has one) and its data freed. Ensure that you do not need the existing data before calling this function.
@attention Moving a vector onto itself is a no-op and reports success, leaving the vector untouched.
*/
static inline int vector_move(vector_t* dest, vector_t** src) {
if (!dest || !src || !*src) {
@@ -491,22 +670,24 @@ static inline int vector_move(vector_t* dest, vector_t** src) {
return 0; // Moving to itself, no action needed
}
// Free the destination vector's data if it already has allocated memory
if (dest->data) {
// Destroy the destination vector's existing elements with its own destructor,
// then free its data, as it is about to be replaced
vector_clear(dest);
free(dest->data);
}
// Transfer ownership of the source vector's data to the destination vector
dest->size = (*src)->size;
dest->capacity = (*src)->capacity;
dest->element_size = (*src)->element_size;
dest->data = (*src)->data;
dest->destructor = (*src)->destructor; // The elements keep the cleanup they came with
// Reset the source vector to an empty state
(*src)->size = 0;
(*src)->capacity = 0;
(*src)->element_size = 0;
(*src)->data = NULL;
(*src)->destructor = NULL;
free(*src); // Free the source vector structure, but not its data (ownership transferred)
*src = NULL;
@@ -517,14 +698,19 @@ static inline int vector_move(vector_t* dest, vector_t** src) {
/*
@brief Creates a deep copy of the vector, including its data.
@param vec A pointer to the vector to be copied.
@return A pointer to the newly created deep copy of the vector, or NULL if allocation fails or if the input vector is NULL.
@return A pointer to the newly created deep copy of the vector, or NULL if allocation fails, if the input vector is NULL, or if the input vector has a destructor set.
@attention The returned vector must be destroyed with vector_destroy() to free its memory. Failing to do so will result in a memory leak.
@attention Vectors with a destructor cannot be deep copied and this function will return NULL for them. The elements are copied byte for byte, so any memory they own would end up owned by both vectors and freed twice. If you need to copy such a vector, do it by hand: create a new vector and push copies of the elements into it yourself.
*/
static inline vector_t* vector_deep_copy(const vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
if (vec->destructor) {
return NULL; // Refuse to byte-copy elements that own memory
}
vector_t* new_vec = vector_create(vec->element_size);
if (!new_vec) {
return NULL; // Allocation failed
@@ -549,7 +735,7 @@ static inline vector_t* vector_deep_copy(const vector_t* vec) {
@param vec A pointer to the vector to be destroyed.
@return 0 on success, -1 if the vector is NULL.
@attention After calling this function, the vector pointer should not be used again. Accessing it after destruction will lead to undefined behavior.
@attention If stored elements are structs, consider setting the destructor function pointer to the vector_t struct to allow for custom cleanup of elements. Otherwise, the vector will only free the memory allocated for the data array and the vector structure itself, but not any dynamically allocated memory within the elements.
@attention If stored elements own memory of their own, set a destructor with vector_set_destructor() to have it cleaned up here. Otherwise, the vector will only free the memory allocated for the data array and the vector structure itself, but not any dynamically allocated memory within the elements.
*/
static inline int vector_destroy(vector_t* vec) {
if (!vec) {