major refactoring of attributes

Thu, 07 Nov 2024 22:46:58 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 07 Nov 2024 22:46:58 +0100
changeset 985
68754c7de906
parent 984
e8f354a25ac8
child 986
38fa7e41194c

major refactoring of attributes

resolves #460
resolves #471
resolves #472

src/allocator.c file | annotate | diff | comparison | revisions
src/array_list.c file | annotate | diff | comparison | revisions
src/buffer.c file | annotate | diff | comparison | revisions
src/cx/allocator.h file | annotate | diff | comparison | revisions
src/cx/array_list.h file | annotate | diff | comparison | revisions
src/cx/buffer.h file | annotate | diff | comparison | revisions
src/cx/common.h file | annotate | diff | comparison | revisions
src/cx/compare.h file | annotate | diff | comparison | revisions
src/cx/hash_key.h file | annotate | diff | comparison | revisions
src/cx/hash_map.h file | annotate | diff | comparison | revisions
src/cx/iterator.h file | annotate | diff | comparison | revisions
src/cx/json.h file | annotate | diff | comparison | revisions
src/cx/linked_list.h file | annotate | diff | comparison | revisions
src/cx/list.h file | annotate | diff | comparison | revisions
src/cx/map.h file | annotate | diff | comparison | revisions
src/cx/mempool.h file | annotate | diff | comparison | revisions
src/cx/printf.h file | annotate | diff | comparison | revisions
src/cx/properties.h file | annotate | diff | comparison | revisions
src/cx/streams.h file | annotate | diff | comparison | revisions
src/cx/string.h file | annotate | diff | comparison | revisions
src/cx/test.h file | annotate | diff | comparison | revisions
src/cx/tree.h file | annotate | diff | comparison | revisions
src/list.c file | annotate | diff | comparison | revisions
src/map.c file | annotate | diff | comparison | revisions
src/mempool.c file | annotate | diff | comparison | revisions
src/properties.c file | annotate | diff | comparison | revisions
src/string.c file | annotate | diff | comparison | revisions
tests/test_allocator.c file | annotate | diff | comparison | revisions
tests/test_buffer.c file | annotate | diff | comparison | revisions
tests/test_hash_map.c file | annotate | diff | comparison | revisions
tests/test_list.c file | annotate | diff | comparison | revisions
tests/test_mempool.c file | annotate | diff | comparison | revisions
tests/test_tree.c file | annotate | diff | comparison | revisions
tests/util_allocator.c file | annotate | diff | comparison | revisions
--- a/src/allocator.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/allocator.c	Thu Nov 07 22:46:58 2024 +0100
@@ -32,7 +32,7 @@
 
 __attribute__((__malloc__, __alloc_size__(2)))
 static void *cx_malloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         size_t n
 ) {
     return malloc(n);
@@ -40,7 +40,7 @@
 
 __attribute__((__warn_unused_result__, __alloc_size__(3)))
 static void *cx_realloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         void *mem,
         size_t n
 ) {
@@ -49,7 +49,7 @@
 
 __attribute__((__malloc__, __alloc_size__(2, 3)))
 static void *cx_calloc_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         size_t nelem,
         size_t n
 ) {
@@ -58,7 +58,7 @@
 
 __attribute__((__nonnull__))
 static void cx_free_stdlib(
-        __attribute__((__unused__)) void *d,
+        cx_attr_unused void *d,
         void *mem
 ) {
     free(mem);
--- a/src/array_list.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/array_list.c	Thu Nov 07 22:46:58 2024 +0100
@@ -37,7 +37,7 @@
         void *array,
         size_t capacity,
         size_t elem_size,
-        __attribute__((__unused__)) CxArrayReallocator *alloc
+        cx_attr_unused CxArrayReallocator *alloc
 ) {
     return realloc(array, capacity * elem_size);
 }
@@ -54,7 +54,7 @@
         void *array,
         size_t capacity,
         size_t elem_size,
-        __attribute__((__unused__)) CxArrayReallocator *alloc
+        cx_attr_unused CxArrayReallocator *alloc
 ) {
     // retrieve the pointer to the actual allocator
     const CxAllocator *al = alloc->ptr1;
@@ -326,6 +326,44 @@
     return result < 0 ? (pivot_index - 1) : pivot_index;
 }
 
+size_t cx_array_binary_search(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t index = cx_array_binary_search_inf(
+            arr, size, elem_size, elem, cmp_func
+    );
+    if (index < size &&
+            cmp_func(((const char *) arr) + index * elem_size, elem) == 0) {
+        return index;
+    } else {
+        return size;
+    }
+}
+
+size_t cx_array_binary_search_sup(
+        const void *arr,
+        size_t size,
+        size_t elem_size,
+        const void *elem,
+        cx_compare_func cmp_func
+) {
+    size_t inf = cx_array_binary_search_inf(
+            arr, size, elem_size, elem, cmp_func
+    );
+    if (inf == size) {
+        // no infimum means, first element is supremum
+        return 0;
+    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
+        return inf;
+    } else {
+        return inf + 1;
+    }
+}
+
 #ifndef CX_ARRAY_SWAP_SBO_SIZE
 #define CX_ARRAY_SWAP_SBO_SIZE 128
 #endif
--- a/src/buffer.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/buffer.c	Thu Nov 07 22:46:58 2024 +0100
@@ -86,6 +86,7 @@
 }
 
 void cxBufferFree(CxBuffer *buffer) {
+    if (buffer == NULL) return;
     if ((buffer->flags & CX_BUFFER_FREE_CONTENTS) == CX_BUFFER_FREE_CONTENTS) {
         cxFree(buffer->allocator, buffer->bytes);
     }
--- a/src/cx/allocator.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/allocator.h	Thu Nov 07 22:46:58 2024 +0100
@@ -46,6 +46,8 @@
     /**
      * The allocator's malloc() implementation.
      */
+    cx_attr_nonnull
+    cx_attr_nodiscard cx_attr_allocsize(2)
     void *(*malloc)(
             void *data,
             size_t n
@@ -54,7 +56,8 @@
     /**
      * The allocator's realloc() implementation.
      */
-    __attribute__((__warn_unused_result__))
+    cx_attr_nodiscard
+    cx_attr_allocsize(3)
     void *(*realloc)(
             void *data,
             void *mem,
@@ -64,6 +67,8 @@
     /**
      * The allocator's calloc() implementation.
      */
+    cx_attr_nodiscard
+    cx_attr_allocsize(2, 3)
     void *(*calloc)(
             void *data,
             size_t nelem,
@@ -73,7 +78,7 @@
     /**
      * The allocator's free() implementation.
      */
-    __attribute__((__nonnull__))
+    cx_attr_nonnull_arg(1)
     void (*free)(
             void *data,
             void *mem
@@ -109,12 +114,11 @@
  *
  * A destructor function deallocates possible contents and MAY free the memory
  * pointed to by \p memory. Read the documentation of the respective function
- * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in that
- * particular implementation.
+ * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
+ * that particular implementation.
  *
  * @param memory a pointer to the object to destruct
   */
-__attribute__((__nonnull__))
 typedef void (*cx_destructor_func)(void *memory);
 
 /**
@@ -122,20 +126,20 @@
  *
  * A destructor function deallocates possible contents and MAY free the memory
  * pointed to by \p memory. Read the documentation of the respective function
- * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in that
- * particular implementation.
+ * pointer to learn if a destructor SHALL, MAY, or MUST NOT free the memory in
+ * that particular implementation.
  *
  * @param data an optional pointer to custom data
  * @param memory a pointer to the object to destruct
   */
-__attribute__((__nonnull__(2)))
 typedef void (*cx_destructor_func2)(
         void *data,
         void *memory
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  *
  * \par Error handling
  * \c errno will be set by realloc() on failure.
@@ -144,14 +148,16 @@
  * @param n the new size in bytes
  * @return zero on success, non-zero on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_reallocate(
         void **mem,
         size_t n
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  *
  * The size is calculated by multiplying \p nemb and \p size.
  *
@@ -164,7 +170,8 @@
  * @param size the size of each element
  * @return zero on success, non-zero on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_reallocatearray(
         void **mem,
         size_t nmemb,
@@ -172,7 +179,8 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  *
  * \par Error handling
  * \c errno will be set by realloc() on failure.
@@ -184,7 +192,8 @@
 #define cx_reallocate(mem, n) cx_reallocate((void**)(mem), n)
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  *
  * The size is calculated by multiplying \p nemb and \p size.
  *
@@ -197,7 +206,22 @@
  * @param size the size of each element
  * @return zero on success, non-zero on failure
  */
-#define cx_reallocatearray(mem, nmemb, size) cx_reallocatearray((void**)(mem), nmemb, size)
+#define cx_reallocatearray(mem, nmemb, size) \
+    cx_reallocatearray((void**)(mem), nmemb, size)
+
+/**
+ * Free a block allocated by this allocator.
+ *
+ * \note Freeing a block of a different allocator is undefined.
+ *
+ * @param allocator the allocator
+ * @param mem a pointer to the block to free
+ */
+cx_attr_nonnull_arg(1)
+void cxFree(
+        const CxAllocator *allocator,
+        void *mem
+);
 
 /**
  * Allocate \p n bytes of memory.
@@ -206,18 +230,21 @@
  * @param n the number of bytes
  * @return a pointer to the allocated memory
  */
-__attribute__((__malloc__))
-__attribute__((__nonnull__))
-__attribute__((__alloc_size__(2)))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_malloc
+cx_attr_dealloc_ucx
+cx_attr_allocsize(2)
 void *cxMalloc(
         const CxAllocator *allocator,
         size_t n
 );
 
 /**
- * Re-allocate the previously allocated block in \p mem, making the new block \p n bytes long.
- * This function may return the same pointer that was passed to it, if moving the memory
- * was not necessary.
+ * Re-allocate the previously allocated block in \p mem, making the new block
+ * \p n bytes long.
+ * This function may return the same pointer that was passed to it, if moving
+ * the memory was not necessary.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
  *
@@ -226,9 +253,10 @@
  * @param n the new size in bytes
  * @return a pointer to the re-allocated memory
  */
-__attribute__((__warn_unused_result__))
-__attribute__((__nonnull__(1)))
-__attribute__((__alloc_size__(3)))
+cx_attr_nodiscard
+cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx
+cx_attr_allocsize(3)
 void *cxRealloc(
         const CxAllocator *allocator,
         void *mem,
@@ -236,12 +264,14 @@
 );
 
 /**
- * Re-allocate the previously allocated block in \p mem, making the new block \p n bytes long.
- * This function may return the same pointer that was passed to it, if moving the memory
- * was not necessary.
+ * Re-allocate the previously allocated block in \p mem, making the new block
+ * \p n bytes long.
+ * This function may return the same pointer that was passed to it, if moving
+ * the memory was not necessary.
  *
  * The size is calculated by multiplying \p nemb and \p size.
- * If that multiplication overflows, this function returns \c NULL and \c errno will be set.
+ * If that multiplication overflows, this function returns \c NULL and \c errno
+ * will be set.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
  *
@@ -251,9 +281,10 @@
  * @param size the size of each element
  * @return a pointer to the re-allocated memory
  */
-__attribute__((__warn_unused_result__))
-__attribute__((__nonnull__(1)))
-__attribute__((__alloc_size__(3, 4)))
+cx_attr_nodiscard
+cx_attr_nonnull_arg(1)
+cx_attr_dealloc_ucx
+cx_attr_allocsize(3, 4)
 void *cxReallocArray(
         const CxAllocator *allocator,
         void *mem,
@@ -262,7 +293,8 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  * This function acts like cxRealloc() using the pointer pointed to by \p mem.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
@@ -275,7 +307,8 @@
  * @param n the new size in bytes
  * @return zero on success, non-zero on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cxReallocate(
         const CxAllocator *allocator,
         void **mem,
@@ -283,7 +316,8 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
  * This function acts like cxRealloc() using the pointer pointed to by \p mem.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
@@ -296,11 +330,14 @@
  * @param n the new size in bytes
  * @return zero on success, non-zero on failure
  */
-#define cxReallocate(allocator, mem, n) cxReallocate(allocator, (void**)(mem), n)
+#define cxReallocate(allocator, mem, n) \
+    cxReallocate(allocator, (void**)(mem), n)
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
- * This function acts like cxReallocArray() using the pointer pointed to by \p mem.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxReallocArray() using the pointer pointed to
+ * by \p mem.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
  *
@@ -314,7 +351,8 @@
  * @param size the size of each element
  * @return zero on success, non-zero on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cxReallocateArray(
         const CxAllocator *allocator,
         void **mem,
@@ -323,8 +361,10 @@
 );
 
 /**
- * Re-allocate a previously allocated block and changes the pointer in-place, if necessary.
- * This function acts like cxReallocArray() using the pointer pointed to by \p mem.
+ * Re-allocate a previously allocated block and changes the pointer in-place,
+ * if necessary.
+ * This function acts like cxReallocArray() using the pointer pointed to
+ * by \p mem.
  *
  * \note Re-allocating a block allocated by a different allocator is undefined.
  *
@@ -349,29 +389,17 @@
  * @param n the size of each element in bytes
  * @return a pointer to the allocated memory
  */
-__attribute__((__malloc__))
-__attribute__((__nonnull__))
-__attribute__((__alloc_size__(2, 3)))
+cx_attr_nonnull_arg(1)
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc_ucx
+cx_attr_allocsize(2, 3)
 void *cxCalloc(
         const CxAllocator *allocator,
         size_t nelem,
         size_t n
 );
 
-/**
- * Free a block allocated by this allocator.
- *
- * \note Freeing a block of a different allocator is undefined.
- *
- * @param allocator the allocator
- * @param mem a pointer to the block to free
- */
-__attribute__((__nonnull__(1)))
-void cxFree(
-        const CxAllocator *allocator,
-        void *mem
-);
-
 #ifdef __cplusplus
 } // extern "C"
 #endif
--- a/src/cx/array_list.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/array_list.h	Thu Nov 07 22:46:58 2024 +0100
@@ -28,7 +28,6 @@
 /**
  * \file array_list.h
  * \brief Array list implementation.
- * \details Also provides several low-level functions for custom array list implementations.
  * \author Mike Becker
  * \author Olaf Wintermann
  * \copyright 2-Clause BSD License
@@ -45,7 +44,8 @@
 #endif
 
 /**
- * The maximum item size in an array list that fits into stack buffer when swapped.
+ * The maximum item size in an array list that fits into stack buffer
+ * when swapped.
  */
 extern const unsigned cx_array_swap_sbo_size;
 
@@ -94,6 +94,9 @@
      * @param alloc a reference to this allocator
      * @return a pointer to the reallocated memory or \c NULL on failure
      */
+    cx_attr_nodiscard
+    cx_attr_nonnull_arg(4)
+    cx_attr_allocsize(2, 3)
     void *(*realloc)(
             void *array,
             size_t capacity,
@@ -184,7 +187,7 @@
  * if reallocation shall not happen
  * @return zero on success, non-zero error code on failure
  */
-__attribute__((__nonnull__(1, 2, 5)))
+cx_attr_nonnull_arg(1, 2, 5)
 enum cx_array_result cx_array_copy(
         void **target,
         size_t *size,
@@ -262,7 +265,7 @@
  * @param reallocator the array reallocator to use
  * @return zero on success, non-zero error code on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 enum cx_array_result cx_array_insert_sorted(
         void **target,
         size_t *size,
@@ -342,7 +345,7 @@
  * @param cmp_func the compare function
  * @return the index of the largest lower bound, or \p size
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_array_binary_search_inf(
         const void *arr,
         size_t size,
@@ -365,23 +368,14 @@
  * @return the index of the element in the array, or \p size if the element
  * cannot be found
  */
-__attribute__((__nonnull__))
-static inline size_t cx_array_binary_search(
+cx_attr_nonnull
+size_t cx_array_binary_search(
         const void *arr,
         size_t size,
         size_t elem_size,
         const void *elem,
         cx_compare_func cmp_func
-) {
-    size_t index = cx_array_binary_search_inf(
-            arr, size, elem_size, elem, cmp_func
-    );
-    if (index < size && cmp_func(((const char *) arr) + index * elem_size, elem) == 0) {
-        return index;
-    } else {
-        return size;
-    }
-}
+);
 
 /**
  * Searches the smallest upper bound in a sorted array.
@@ -403,24 +397,14 @@
  * @param cmp_func the compare function
  * @return the index of the smallest upper bound, or \p size
  */
-__attribute__((__nonnull__))
-static inline size_t cx_array_binary_search_sup(
+cx_attr_nonnull
+size_t cx_array_binary_search_sup(
         const void *arr,
         size_t size,
         size_t elem_size,
         const void *elem,
         cx_compare_func cmp_func
-) {
-    size_t inf = cx_array_binary_search_inf(arr, size, elem_size, elem, cmp_func);
-    if (inf == size) {
-        // no infimum means, first element is supremum
-        return 0;
-    } else if (cmp_func(((const char *) arr) + inf * elem_size, elem) == 0) {
-        return inf;
-    } else {
-        return inf + 1;
-    }
-}
+);
 
 /**
  * Swaps two array elements.
@@ -430,7 +414,7 @@
  * @param idx1 index of first element
  * @param idx2 index of second element
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_array_swap(
         void *arr,
         size_t elem_size,
@@ -454,6 +438,9 @@
  * @param initial_capacity the initial number of elements the array can store
  * @return the created list
  */
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxListDestroy, 1)
 CxList *cxArrayListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
--- a/src/cx/buffer.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/buffer.h	Thu Nov 07 22:46:58 2024 +0100
@@ -51,7 +51,7 @@
 
 #include <sys/types.h>
 
-#ifdef    __cplusplus
+#ifdef __cplusplus
 extern "C" {
 #endif
 
@@ -155,7 +155,7 @@
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return zero on success, non-zero if a required allocation failed
  */
-__attribute__((__nonnull__(1)))
+cx_attr_nonnull_arg(1)
 int cxBufferInit(
         CxBuffer *buffer,
         void *space,
@@ -165,6 +165,32 @@
 );
 
 /**
+ * Destroys the buffer contents.
+ *
+ * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
+ * If you want to free the memory of the entire buffer, use cxBufferFree().
+ *
+ * @param buffer the buffer which contents shall be destroyed
+ * @see cxBufferInit()
+ */
+cx_attr_nonnull
+void cxBufferDestroy(CxBuffer *buffer);
+
+/**
+ * Deallocates the buffer.
+ *
+ * If the #CX_BUFFER_FREE_CONTENTS feature is enabled, this function also destroys
+ * the contents. If you \em only want to destroy the contents, use cxBufferDestroy().
+ *
+ * \remark As with all free() functions, this accepts \c NULL arguments in which
+ * case it does nothing.
+ *
+ * @param buffer the buffer to deallocate
+ * @see cxBufferCreate()
+ */
+void cxBufferFree(CxBuffer *buffer);
+
+/**
  * Allocates and initializes a fresh buffer.
  *
  * \note You may provide \c NULL as argument for \p space.
@@ -179,6 +205,9 @@
  * @param flags buffer features (see cx_buffer_s.flags)
  * @return a pointer to the buffer on success, \c NULL if a required allocation failed
  */
+cx_attr_malloc
+cx_attr_dealloc(cxBufferFree, 1)
+cx_attr_nodiscard
 CxBuffer *cxBufferCreate(
         void *space,
         size_t capacity,
@@ -187,30 +216,6 @@
 );
 
 /**
- * Destroys the buffer contents.
- *
- * Has no effect if the #CX_BUFFER_FREE_CONTENTS feature is not enabled.
- * If you want to free the memory of the entire buffer, use cxBufferFree().
- *
- * @param buffer the buffer which contents shall be destroyed
- * @see cxBufferInit()
- */
-__attribute__((__nonnull__))
-void cxBufferDestroy(CxBuffer *buffer);
-
-/**
- * Deallocates the buffer.
- *
- * If the #CX_BUFFER_FREE_CONTENTS feature is enabled, this function also destroys
- * the contents. If you \em only want to destroy the contents, use cxBufferDestroy().
- *
- * @param buffer the buffer to deallocate
- * @see cxBufferCreate()
- */
-__attribute__((__nonnull__))
-void cxBufferFree(CxBuffer *buffer);
-
-/**
  * Shifts the contents of the buffer by the given offset.
  *
  * If the offset is positive, the contents are shifted to the right.
@@ -243,7 +248,7 @@
  * @param shift the shift offset (negative means left shift)
  * @return 0 on success, non-zero if a required auto-extension fails
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShift(
         CxBuffer *buffer,
         off_t shift
@@ -258,7 +263,7 @@
  * @return 0 on success, non-zero if a required auto-extension fails
  * @see cxBufferShift()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShiftRight(
         CxBuffer *buffer,
         size_t shift
@@ -276,7 +281,7 @@
  * @return always zero
  * @see cxBufferShift()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferShiftLeft(
         CxBuffer *buffer,
         size_t shift
@@ -302,7 +307,7 @@
  * @return 0 on success, non-zero if the position is invalid
  *
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferSeek(
         CxBuffer *buffer,
         off_t offset,
@@ -318,7 +323,7 @@
  * @param buffer the buffer to be cleared
  * @see cxBufferReset()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxBufferClear(CxBuffer *buffer);
 
 /**
@@ -330,7 +335,7 @@
  * @param buffer the buffer to be cleared
  * @see cxBufferClear()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxBufferReset(CxBuffer *buffer);
 
 /**
@@ -340,7 +345,8 @@
  * @return non-zero, if the current buffer position has exceeded the last
  * byte of the buffer's contents.
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cxBufferEof(const CxBuffer *buffer);
 
 
@@ -353,7 +359,7 @@
  * @param capacity the minimum required capacity for this buffer
  * @return 0 on success or a non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferMinimumCapacity(
         CxBuffer *buffer,
         size_t capacity
@@ -385,7 +391,7 @@
  * @param buffer the CxBuffer to write to
  * @return the total count of elements written
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cxBufferWrite(
         const void *ptr,
         size_t size,
@@ -406,7 +412,7 @@
  * @param buffer the CxBuffer to read from
  * @return the total number of elements read
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cxBufferRead(
         void *ptr,
         size_t size,
@@ -429,7 +435,7 @@
  * @return the byte that has bean written or \c EOF when the end of the stream is
  * reached and automatic extension is not enabled or not possible
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferPut(
         CxBuffer *buffer,
         int c
@@ -442,7 +448,8 @@
  * @param str the zero-terminated string
  * @return the number of bytes written
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 size_t cxBufferPutString(
         CxBuffer *buffer,
         const char *str
@@ -456,7 +463,7 @@
  * @param buffer the buffer to read from
  * @return the character or \c EOF, if the end of the buffer is reached
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxBufferGet(CxBuffer *buffer);
 
 #ifdef __cplusplus
--- a/src/cx/common.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/common.h	Thu Nov 07 22:46:58 2024 +0100
@@ -88,13 +88,166 @@
 /** Version constant which ensures to increase monotonically. */
 #define UCX_VERSION (((UCX_VERSION_MAJOR)<<16)|UCX_VERSION_MINOR)
 
-// Common Includes
+// ---------------------------------------------------------------------------
+//       Common includes
+// ---------------------------------------------------------------------------
 
 #include <stdlib.h>
 #include <stddef.h>
 #include <stdbool.h>
 #include <stdint.h>
 
+// ---------------------------------------------------------------------------
+//       Attribute definitions
+// ---------------------------------------------------------------------------
+
+#ifndef __GNUC__
+/**
+ * Removes GNU C attributes where they are not supported.
+ */
+#define __attribute__(x)
+#endif
+
+/**
+ * All pointer arguments must be non-NULL.
+ */
+#define cx_attr_nonnull __attribute__((__nonnull__))
+
+/**
+ * The specified pointer arguments must be non-NULL.
+ */
+#define cx_attr_nonnull_arg(...) __attribute__((__nonnull__(__VA_ARGS__)))
+
+/**
+ * The returned value is guaranteed to be non-NULL.
+ */
+#define cx_attr_returns_nonnull __attribute__((__returns_nonnull__))
+
+/**
+ * The attributed function always returns freshly allocated memory.
+ */
+#define cx_attr_malloc __attribute__((__malloc__))
+
+#ifndef __clang__
+/**
+ * The pointer returned by the attributed function is supposed to be freed
+ * by \p freefunc.
+ *
+ * @param freefunc the function that shall be used to free the memory
+ * @param freefunc_arg the index of the pointer argument in \p freefunc
+ */
+#define cx_attr_dealloc(freefunc, freefunc_arg) \
+    __attribute__((__malloc__(freefunc, freefunc_arg)))
+#else
+/**
+ * Not supported in clang.
+ */
+#define cx_attr_dealloc(...)
+#endif // __clang__
+
+/**
+ * Shortcut to specify #cxFree() as deallocator.
+ */
+#define cx_attr_dealloc_ucx cx_attr_dealloc(cxFree, 2)
+
+/**
+ * Specifies the parameters from which the allocation size is calculated.
+ */
+#define cx_attr_allocsize(...) __attribute__((__alloc_size__(__VA_ARGS__)))
+
+
+#ifdef __clang__
+/**
+ * No support for \c null_terminated_string_arg in clang or GCC below 14.
+ */
+#define cx_attr_cstr_arg(idx)
+/**
+ * No support for access attribute in clang.
+ */
+#define cx_attr_access(mode, ...)
+#else
+#if __GNUC__ < 10
+/**
+ * No support for access attribute in GCC < 10.
+ */
+#define cx_attr_access(mode, ...)
+#else
+/**
+ * Helper macro to define access macros.
+ */
+#define cx_attr_access(mode, ...) __attribute__((__access__(mode, __VA_ARGS__)))
+#endif // __GNUC__ < 10
+#if __GNUC__ < 14
+/**
+ * No support for \c null_terminated_string_arg in clang or GCC below 14.
+ */
+#define cx_attr_cstr_arg(idx)
+#else
+/**
+ * The specified argument is expected to be a zero-terminated string.
+ *
+ * @param idx the index of the argument
+ */
+#define cx_attr_cstr_arg(idx) \
+    __attribute__((__null_terminated_string_arg__(idx)))
+#endif // __GNUC__ < 14
+#endif // __clang__
+
+
+/**
+ * Specifies that the function will only read through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_r(...) cx_attr_access(__read_only__, __VA_ARGS__)
+
+/**
+ * Specifies that the function will read and write through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_rw(...) cx_attr_access(__read_write__, __VA_ARGS__)
+
+/**
+ * Specifies that the function will only write through the given pointer.
+ *
+ * Takes one or two arguments: the index of the pointer and (optionally) the
+ * index of another argument specifying the maximum number of accessed bytes.
+ */
+#define cx_attr_access_w(...) cx_attr_access(__write_only__, __VA_ARGS__)
+
+#if __STDC_VERSION__ >= 202300L
+
+/**
+ * Do not warn about unused variable.
+ */
+#define cx_attr_unused [[maybe_unused]]
+
+/**
+ * Warn about discarded return value.
+ */
+#define cx_attr_nodiscard [[nodiscard]]
+
+#else // no C23
+
+/**
+ * Do not warn about unused variable.
+ */
+#define cx_attr_unused __attribute__((__unused__))
+
+/**
+ * Warn about discarded return value.
+ */
+#define cx_attr_nodiscard __attribute__((__warn_unused_result__))
+
+#endif // __STDC_VERSION__
+
+// ---------------------------------------------------------------------------
+//       Useful function pointers
+// ---------------------------------------------------------------------------
+
 /**
  * Function pointer compatible with fwrite-like functions.
  */
@@ -103,7 +256,7 @@
         size_t,
         size_t,
         void *
-);
+) cx_attr_nonnull;
 
 /**
  * Function pointer compatible with fread-like functions.
@@ -113,7 +266,11 @@
         size_t,
         size_t,
         void *
-);
+) cx_attr_nonnull;
+
+// ---------------------------------------------------------------------------
+//       Utility macros
+// ---------------------------------------------------------------------------
 
 /**
  * Determines the number of members in a static C array.
@@ -126,7 +283,10 @@
  */
 #define cx_nmemb(arr) (sizeof(arr)/sizeof((arr)[0]))
 
-// cx_szmul() definition
+// ---------------------------------------------------------------------------
+//       szmul implementation
+// ---------------------------------------------------------------------------
+
 #if (__GNUC__ >= 5 || defined(__clang__)) && !defined(CX_NO_SZMUL_BUILTIN)
 #define CX_SZMUL_BUILTIN
 
@@ -174,24 +334,18 @@
 
 #endif // cx_szmul
 
-// Compiler specific stuff
 
-#ifndef __GNUC__
-/**
- * Removes GNU C attributes where they are not supported.
- */
-#define __attribute__(x)
-#endif
+// ---------------------------------------------------------------------------
+//       Fixes for MSVC incompatibilities
+// ---------------------------------------------------------------------------
 
 #ifdef _MSC_VER
-
 // fix missing ssize_t definition
 #include <BaseTsd.h>
 typedef SSIZE_T ssize_t;
 
 // fix missing _Thread_local support
 #define _Thread_local __declspec(thread)
-
-#endif
+#endif // _MSC_VER
 
 #endif // UCX_COMMON_H
--- a/src/cx/compare.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/compare.h	Thu Nov 07 22:46:58 2024 +0100
@@ -42,16 +42,15 @@
 extern "C" {
 #endif
 
-#ifndef CX_COMPARE_FUNC_DEFINED
-#define CX_COMPARE_FUNC_DEFINED
 /**
  * A comparator function comparing two collection elements.
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 typedef int(*cx_compare_func)(
         const void *left,
         const void *right
 );
-#endif // CX_COMPARE_FUNC_DEFINED
 
 /**
  * Compares two integers of type int.
@@ -61,6 +60,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int(const void *i1, const void *i2);
 
 /**
@@ -71,6 +72,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_longint(const void *i1, const void *i2);
 
 /**
@@ -81,6 +84,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_longlong(const void *i1, const void *i2);
 
 /**
@@ -91,6 +96,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int16(const void *i1, const void *i2);
 
 /**
@@ -101,6 +108,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int32(const void *i1, const void *i2);
 
 /**
@@ -111,6 +120,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_int64(const void *i1, const void *i2);
 
 /**
@@ -121,6 +132,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint(const void *i1, const void *i2);
 
 /**
@@ -131,6 +144,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_ulongint(const void *i1, const void *i2);
 
 /**
@@ -141,6 +156,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_ulonglong(const void *i1, const void *i2);
 
 /**
@@ -151,6 +168,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint16(const void *i1, const void *i2);
 
 /**
@@ -161,6 +180,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint32(const void *i1, const void *i2);
 
 /**
@@ -171,6 +192,8 @@
  * @return -1, if *i1 is less than *i2, 0 if both are equal,
  * 1 if *i1 is greater than *i2
  */
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_uint64(const void *i1, const void *i2);
 
 /**
@@ -181,7 +204,8 @@
  * @return -1, if *f1 is less than *f2, 0 if both are equal,
  * 1 if *f1 is greater than *f2
  */
-
+cx_attr_nonnull
+cx_attr_nodiscard
 int cx_cmp_float(const void *f1, const void *f2);
 
 /**
@@ -192,10 +216,9 @@
  * @return -1, if *d1 is less than *d2, 0 if both are equal,
  * 1 if *d1 is greater than *d2
  */
-int cx_cmp_double(
-        const void *d1,
-        const void *d2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_double(const void *d1, const void *d2);
 
 /**
  * Compares the integer representation of two pointers.
@@ -205,10 +228,9 @@
  * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
  * 1 if *ptr1 is greater than *ptr2
  */
-int cx_cmp_intptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_intptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the unsigned integer representation of two pointers.
@@ -218,10 +240,9 @@
  * @return -1 if *ptr1 is less than *ptr2, 0 if both are equal,
  * 1 if *ptr1 is greater than *ptr2
  */
-int cx_cmp_uintptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_uintptr(const void *ptr1, const void *ptr2);
 
 /**
  * Compares the pointers specified in the arguments without de-referencing.
@@ -231,10 +252,9 @@
  * @return -1 if ptr1 is less than ptr2, 0 if both are equal,
  * 1 if ptr1 is greater than ptr2
  */
-int cx_cmp_ptr(
-        const void *ptr1,
-        const void *ptr2
-);
+cx_attr_nonnull
+cx_attr_nodiscard
+int cx_cmp_ptr(const void *ptr1, const void *ptr2);
 
 #ifdef __cplusplus
 } // extern "C"
--- a/src/cx/hash_key.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/hash_key.h	Thu Nov 07 22:46:58 2024 +0100
@@ -70,6 +70,7 @@
  *
  * @param key the key, the hash shall be computed for
  */
+cx_attr_nonnull
 void cx_hash_murmur(CxHashKey *key);
 
 /**
@@ -80,7 +81,8 @@
  * @param str the string
  * @return the hash key
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 CxHashKey cx_hash_key_str(const char *str);
 
 /**
@@ -90,7 +92,8 @@
  * @param len the length
  * @return the hash key
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 CxHashKey cx_hash_key_bytes(
         const unsigned char *bytes,
         size_t len
@@ -107,6 +110,8 @@
  * @param len the length of object in memory
  * @return the hash key
  */
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 __attribute__((__warn_unused_result__))
 CxHashKey cx_hash_key(
         const void *obj,
--- a/src/cx/hash_map.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/hash_map.h	Thu Nov 07 22:46:58 2024 +0100
@@ -81,7 +81,10 @@
  * @param buckets the initial number of buckets in this hash map
  * @return a pointer to the new hash map
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxMapDestroy, 1)
 CxMap *cxHashMapCreate(
         const CxAllocator *allocator,
         size_t itemsize,
@@ -122,7 +125,7 @@
  * @param map the map to rehash
  * @return zero on success, non-zero if a memory allocation error occurred
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxMapRehash(CxMap *map);
 
 
--- a/src/cx/iterator.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/iterator.h	Thu Nov 07 22:46:58 2024 +0100
@@ -46,7 +46,7 @@
     /**
      * True iff the iterator points to valid data.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
     bool (*valid)(const void *);
 
     /**
@@ -54,13 +54,15 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*current)(const void *);
 
     /**
      * Original implementation in case the function needs to be wrapped.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*current_impl)(const void *);
 
     /**
@@ -68,7 +70,7 @@
      *
      * When valid returns false, the behavior of this function is undefined.
      */
-    __attribute__ ((__nonnull__))
+    cx_attr_nonnull
     void (*next)(void *);
     /**
      * Indicates whether this iterator may remove elements.
@@ -228,7 +230,7 @@
  * @param elem_count the number of elements in the array
  * @return an iterator for the specified array
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxIterator cxIterator(
         const void *array,
         size_t elem_size,
@@ -258,7 +260,7 @@
  * when removing an element
  * @return an iterator for the specified array
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxIterator cxMutIterator(
         void *array,
         size_t elem_size,
--- a/src/cx/json.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/json.h	Thu Nov 07 22:46:58 2024 +0100
@@ -173,13 +173,14 @@
 
 // TODO: add support for CxAllocator
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxJsonInit(CxJson *json);
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxJsonDestroy(CxJson *json);
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
 int cxJsonFilln(CxJson *json, const char *buf, size_t len);
 
 #define cxJsonFill(json, str) _Generic((str), \
@@ -189,7 +190,7 @@
     const char*: cx_json_fill_str)            \
     (json, str)
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_json_fill_cxstr(
         CxJson *json,
         cxstring str
@@ -197,7 +198,7 @@
     return cxJsonFilln(json, str.ptr, str.length);
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_json_fill_mutstr(
         CxJson *json,
         cxmutstr str
@@ -205,7 +206,8 @@
     return cxJsonFilln(json, str.ptr, str.length);
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cx_json_fill_str(
         CxJson *json,
         const char *str
@@ -213,78 +215,79 @@
     return cxJsonFilln(json, str, strlen(str));
 }
 
+void cxJsonValueFree(CxJsonValue *value);
 
-__attribute__((__nonnull__))
+// TODO: if the CxJsonValue was a returned value, we could reference cxJsonValueFree() as deallocator
+cx_attr_nonnull
 int cxJsonNext(CxJson *json, CxJsonValue **value);
 
-void cxJsonValueFree(CxJsonValue *value);
-
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsObject(CxJsonValue *value) {
     return value->type == CX_JSON_OBJECT;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsArray(CxJsonValue *value) {
     return value->type == CX_JSON_ARRAY;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsString(CxJsonValue *value) {
     return value->type == CX_JSON_STRING;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsNumber(CxJsonValue *value) {
     return value->type == CX_JSON_NUMBER || value->type == CX_JSON_INTEGER;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsInteger(CxJsonValue *value) {
     return value->type == CX_JSON_INTEGER;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsLiteral(CxJsonValue *value) {
     return value->type == CX_JSON_LITERAL;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsBool(CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal != CX_JSON_NULL;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsTrue(CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_TRUE;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsFalse(CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_FALSE;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonIsNull(CxJsonValue *value) {
     return cxJsonIsLiteral(value) && value->value.literal == CX_JSON_NULL;
 }
 
-__attribute__((__nonnull__, __returns_nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 static inline char *cxJsonAsString(CxJsonValue *value) {
     return value->value.string.ptr;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline cxstring cxJsonAsCxString(CxJsonValue *value) {
     return cx_strcast(value->value.string);
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline cxmutstr cxJsonAsCxMutStr(CxJsonValue *value) {
     return value->value.string;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline double cxJsonAsDouble(CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
         return (double) value->value.integer;
@@ -293,7 +296,7 @@
     }
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int64_t cxJsonAsInteger(CxJsonValue *value) {
     if (value->type == CX_JSON_INTEGER) {
         return value->value.integer;
@@ -302,23 +305,26 @@
     }
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxJsonAsBool(CxJsonValue *value) {
     return value->value.literal == CX_JSON_TRUE;
 }
 
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxJsonArrSize(CxJsonValue *value) {
     return value->value.array.array_size;
 }
 
-__attribute__((__nonnull__, __returns_nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 CxJsonValue *cxJsonArrGet(CxJsonValue *value, size_t index);
 
 // TODO: add cxJsonArrIter()
 
 // TODO: implement cxJsonObjGet as a _Generic with support for cxstring
-__attribute__((__nonnull__, __returns_nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
+cx_attr_cstr_arg(2)
 CxJsonValue *cxJsonObjGet(CxJsonValue *value, const char* name);
 
 #ifdef __cplusplus
--- a/src/cx/linked_list.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/linked_list.h	Thu Nov 07 22:46:58 2024 +0100
@@ -64,6 +64,9 @@
  * @param elem_size the size of each element in bytes
  * @return the created list
  */
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxListDestroy, 1)
 CxList *cxLinkedListCreate(
         const CxAllocator *allocator,
         cx_compare_func comparator,
@@ -104,7 +107,8 @@
  * @param index the search index
  * @return the node found at the specified index
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 void *cx_linked_list_at(
         const void *start,
         size_t start_index,
@@ -122,7 +126,7 @@
  * @param elem a pointer to the element to find
  * @return the index of the element or a negative value if it could not be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 ssize_t cx_linked_list_find(
         const void *start,
         ptrdiff_t loc_advance,
@@ -143,7 +147,7 @@
  * @param elem a pointer to the element to find
  * @return the index of the element or a negative value if it could not be found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 ssize_t cx_linked_list_find_node(
         void **result,
         const void *start,
@@ -164,7 +168,8 @@
  * @param loc_prev the location of the \c prev pointer
  * @return a pointer to the first node
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 void *cx_linked_list_first(
         const void *node,
         ptrdiff_t loc_prev
@@ -181,7 +186,8 @@
  * @param loc_next the location of the \c next pointer
  * @return a pointer to the last node
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_returns_nonnull
 void *cx_linked_list_last(
         const void *node,
         ptrdiff_t loc_next
@@ -197,7 +203,7 @@
  * @param node the successor of the node to find
  * @return the node or \c NULL if \p node has no predecessor
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void *cx_linked_list_prev(
         const void *begin,
         ptrdiff_t loc_next,
@@ -216,7 +222,7 @@
  * @param loc_next the location of a \c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be appended
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 void cx_linked_list_add(
         void **begin,
         void **end,
@@ -237,7 +243,7 @@
  * @param loc_next the location of a \c next pointer within your node struct (required)
  * @param new_node a pointer to the node that shall be prepended
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 void cx_linked_list_prepend(
         void **begin,
         void **end,
@@ -254,7 +260,7 @@
  * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
  * @param loc_next the location of a \c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_linked_list_link(
         void *left,
         void *right,
@@ -272,7 +278,7 @@
  * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
  * @param loc_next the location of a \c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_linked_list_unlink(
         void *left,
         void *right,
@@ -294,7 +300,7 @@
  * @param node the node after which to insert (\c NULL if you want to prepend the node to the list)
  * @param new_node a pointer to the node that shall be inserted
  */
-__attribute__((__nonnull__(6)))
+cx_attr_nonnull_arg(6)
 void cx_linked_list_insert(
         void **begin,
         void **end,
@@ -324,7 +330,7 @@
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param insert_end a pointer to the last node of the chain (or NULL if the last node shall be determined)
  */
-__attribute__((__nonnull__(6)))
+cx_attr_nonnull_arg(6)
 void cx_linked_list_insert_chain(
         void **begin,
         void **end,
@@ -349,7 +355,7 @@
  * @param new_node a pointer to the node that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
-__attribute__((__nonnull__(1, 5, 6)))
+cx_attr_nonnull_arg(1, 5, 6)
 void cx_linked_list_insert_sorted(
         void **begin,
         void **end,
@@ -378,7 +384,7 @@
  * @param insert_begin a pointer to the first node of the chain that shall be inserted
  * @param cmp_func a compare function that will receive the node pointers
  */
-__attribute__((__nonnull__(1, 5, 6)))
+cx_attr_nonnull_arg(1, 5, 6)
 void cx_linked_list_insert_sorted_chain(
         void **begin,
         void **end,
@@ -409,7 +415,7 @@
  * @param num the number of nodes to remove
  * @return the actual number of nodes that were removed (may be less when the list did not have enough nodes)
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 size_t cx_linked_list_remove_chain(
         void **begin,
         void **end,
@@ -438,7 +444,7 @@
  * @param loc_next the location of a \c next pointer within your node struct (required)
  * @param node the node to remove
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 static inline void cx_linked_list_remove(
         void **begin,
         void **end,
@@ -482,7 +488,7 @@
  * @param loc_data the location of the \c data pointer within your node struct
  * @param cmp_func the compare function defining the sort order
  */
-__attribute__((__nonnull__(1, 6)))
+cx_attr_nonnull_arg(1, 6)
 void cx_linked_list_sort(
         void **begin,
         void **end,
@@ -506,7 +512,7 @@
  * @return the first non-zero result of invoking \p cmp_func or: negative if the left list is smaller than the
  * right list, positive if the left list is larger than the right list, zero if both lists are equal.
  */
-__attribute__((__nonnull__(5)))
+cx_attr_nonnull_arg(5)
 int cx_linked_list_compare(
         const void *begin_left,
         const void *begin_right,
@@ -523,7 +529,7 @@
  * @param loc_prev the location of a \c prev pointer within your node struct (negative if your struct does not have one)
  * @param loc_next the location of a \c next pointer within your node struct (required)
  */
-__attribute__((__nonnull__(1)))
+cx_attr_nonnull_arg(1)
 void cx_linked_list_reverse(
         void **begin,
         void **end,
--- a/src/cx/list.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/list.h	Thu Nov 07 22:46:58 2024 +0100
@@ -52,6 +52,9 @@
  * Structure for holding the base data of a list.
  */
 struct cx_list_s {
+    /**
+     * Common members for collections.
+     */
     CX_COLLECTION_BASE;
     /**
      * The list class definition.
@@ -73,12 +76,14 @@
      * Implementations SHALL invoke the content destructor functions if provided
      * and SHALL deallocate the list memory.
      */
+    cx_attr_nonnull
     void (*destructor)(struct cx_list_s *list);
 
     /**
      * Member function for inserting a single element.
      * Implementors SHOULD see to performant implementations for corner cases.
      */
+    cx_attr_nonnull
     int (*insert_element)(
             struct cx_list_s *list,
             size_t index,
@@ -90,6 +95,7 @@
      * Implementors SHOULD see to performant implementations for corner cases.
      * @see cx_list_default_insert_array()
      */
+    cx_attr_nonnull
     size_t (*insert_array)(
             struct cx_list_s *list,
             size_t index,
@@ -102,6 +108,7 @@
      *
      * @see cx_list_default_insert_sorted()
      */
+    cx_attr_nonnull
     size_t (*insert_sorted)(
             struct cx_list_s *list,
             const void *sorted_data,
@@ -111,6 +118,7 @@
     /**
      * Member function for inserting an element relative to an iterator position.
      */
+    cx_attr_nonnull
     int (*insert_iter)(
             struct cx_iterator_s *iter,
             const void *elem,
@@ -127,6 +135,8 @@
      * The function SHALL return the actual number of elements removed, which
      * might be lower than \p num when going out of bounds.
      */
+    cx_attr_nonnull_arg(1)
+    cx_attr_access_w(4)
     size_t (*remove)(
             struct cx_list_s *list,
             size_t index,
@@ -137,12 +147,14 @@
     /**
      * Member function for removing all elements.
      */
+    cx_attr_nonnull
     void (*clear)(struct cx_list_s *list);
 
     /**
      * Member function for swapping two elements.
      * @see cx_list_default_swap()
      */
+    cx_attr_nonnull
     int (*swap)(
             struct cx_list_s *list,
             size_t i,
@@ -152,6 +164,8 @@
     /**
      * Member function for element lookup.
      */
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*at)(
             const struct cx_list_s *list,
             size_t index
@@ -160,6 +174,8 @@
     /**
      * Member function for finding and optionally removing an element.
      */
+    cx_attr_nonnull
+    cx_attr_nodiscard
     ssize_t (*find_remove)(
             struct cx_list_s *list,
             const void *elem,
@@ -170,6 +186,7 @@
      * Member function for sorting the list in-place.
      * @see cx_list_default_sort()
      */
+    cx_attr_nonnull
     void (*sort)(struct cx_list_s *list);
 
     /**
@@ -177,6 +194,7 @@
      * to another list of the same type.
      * If set to \c NULL, comparison won't be optimized.
      */
+    cx_attr_nonnull
     int (*compare)(
             const struct cx_list_s *list,
             const struct cx_list_s *other
@@ -185,11 +203,13 @@
     /**
      * Member function for reversing the order of the items.
      */
+    cx_attr_nonnull
     void (*reverse)(struct cx_list_s *list);
 
     /**
      * Member function for returning an iterator pointing to the specified index.
      */
+    cx_attr_nonnull
     struct cx_iterator_s (*iterator)(
             const struct cx_list_s *list,
             size_t index,
@@ -211,7 +231,7 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_list_default_insert_array(
         struct cx_list_s *list,
         size_t index,
@@ -235,7 +255,7 @@
  * @param n the number of elements to insert
  * @return the number of elements actually inserted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 size_t cx_list_default_insert_sorted(
         struct cx_list_s *list,
         const void *sorted_data,
@@ -253,7 +273,7 @@
  *
  * @param list the list that shall be sorted
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_list_default_sort(struct cx_list_s *list);
 
 /**
@@ -268,7 +288,7 @@
  * @return zero on success, non-zero when indices are out of bounds or memory
  * allocation for the temporary buffer fails
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cx_list_default_swap(struct cx_list_s *list, size_t i, size_t j);
 
 /**
@@ -285,7 +305,7 @@
  * @param list the list
  * @see cxListStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxListStoreObjects(CxList *list);
 
 /**
@@ -300,7 +320,7 @@
  * @param list the list
  * @see cxListStoreObjects()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxListStorePointers(CxList *list);
 
 /**
@@ -310,7 +330,7 @@
  * @return true, if this list is storing pointers
  * @see cxListStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxListIsStoringPointers(const CxList *list) {
     return list->collection.store_pointer;
 }
@@ -321,7 +341,7 @@
  * @param list the list
  * @return the number of currently stored elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListSize(const CxList *list) {
     return list->collection.size;
 }
@@ -334,7 +354,7 @@
  * @return zero on success, non-zero on memory allocation failure
  * @see cxListAddArray()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListAdd(
         CxList *list,
         const void *elem
@@ -358,7 +378,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListAddArray(
         CxList *list,
         const void *array,
@@ -380,7 +400,7 @@
  * @see cxListInsertAfter()
  * @see cxListInsertBefore()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsert(
         CxList *list,
         size_t index,
@@ -396,7 +416,7 @@
  * @param elem a pointer to the element to add
  * @return zero on success, non-zero on memory allocation failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertSorted(
         CxList *list,
         const void *elem
@@ -424,7 +444,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListInsertArray(
         CxList *list,
         size_t index,
@@ -451,7 +471,7 @@
  * @param n the number of elements to add
  * @return the number of added elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListInsertSortedArray(
         CxList *list,
         const void *array,
@@ -475,7 +495,7 @@
  * @see cxListInsert()
  * @see cxListInsertBefore()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertAfter(
         CxIterator *iter,
         const void *elem
@@ -498,7 +518,7 @@
  * @see cxListInsert()
  * @see cxListInsertAfter()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListInsertBefore(
         CxIterator *iter,
         const void *elem
@@ -516,7 +536,7 @@
  * @param index the index of the element
  * @return zero on success, non-zero if the index is out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListRemove(
         CxList *list,
         size_t index
@@ -535,7 +555,8 @@
  * @param targetbuf a buffer where to copy the element
  * @return zero on success, non-zero if the index is out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(3)
 static inline int cxListRemoveAndGet(
         CxList *list,
         size_t index,
@@ -559,7 +580,7 @@
  * @param num the number of elements to remove
  * @return the actual number of removed elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxListRemoveArray(
         CxList *list,
         size_t index,
@@ -580,7 +601,8 @@
  * @param targetbuf a buffer where to copy the elements
  * @return the actual number of removed elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(4)
 static inline size_t cxListRemoveArrayAndGet(
         CxList *list,
         size_t index,
@@ -598,7 +620,7 @@
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListClear(CxList *list) {
     list->cl->clear(list);
 }
@@ -614,7 +636,7 @@
  * @param j the index of the second element
  * @return zero on success, non-zero if one of the indices is out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxListSwap(
         CxList *list,
         size_t i,
@@ -630,9 +652,9 @@
  * @param index the index of the element
  * @return a pointer to the element or \c NULL if the index is out of bounds
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void *cxListAt(
-        CxList *list,
+        const CxList *list,
         size_t index
 ) {
     return list->cl->at(list, index);
@@ -649,7 +671,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListIteratorAt(
         const CxList *list,
         size_t index
@@ -668,7 +691,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListBackwardsIteratorAt(
         const CxList *list,
         size_t index
@@ -687,7 +711,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxListMutIteratorAt(
         CxList *list,
         size_t index
@@ -705,7 +730,8 @@
  * @param index the index where the iterator shall point at
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxListMutBackwardsIteratorAt(
         CxList *list,
         size_t index
@@ -721,7 +747,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListIterator(const CxList *list) {
     return list->cl->iterator(list, 0, false);
 }
@@ -736,7 +763,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListMutIterator(CxList *list) {
     return cxListMutIteratorAt(list, 0);
 }
@@ -752,7 +780,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListBackwardsIterator(const CxList *list) {
     return list->cl->iterator(list, list->collection.size - 1, true);
 }
@@ -767,7 +796,8 @@
  * @param list the list
  * @return a new iterator
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxListMutBackwardsIterator(CxList *list) {
     return cxListMutBackwardsIteratorAt(list, list->collection.size - 1);
 }
@@ -782,7 +812,8 @@
  * @return the index of the element or a negative
  * value when the element is not found
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline ssize_t cxListFind(
         const CxList *list,
         const void *elem
@@ -800,7 +831,7 @@
  * @return the index of the now removed element or a negative
  * value when the element is not found or could not be removed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline ssize_t cxListFindRemove(
         CxList *list,
         const void *elem
@@ -815,7 +846,7 @@
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListSort(CxList *list) {
     list->cl->sort(list);
 }
@@ -825,7 +856,7 @@
  *
  * @param list the list
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxListReverse(CxList *list) {
     list->cl->reverse(list);
 }
@@ -841,7 +872,8 @@
  * @return zero, if both lists are equal element wise,
  * negative if the first list is smaller, positive if the first list is larger
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
 int cxListCompare(
         const CxList *list,
         const CxList *other
@@ -857,15 +889,17 @@
  *
  * @param list the list which shall be destroyed
  */
-__attribute__((__nonnull__))
-void cxListDestroy(CxList *list);
+static inline void cxListDestroy(CxList *list) {
+    if (list == NULL) return;
+    list->cl->destructor(list);
+}
 
 /**
  * A shared instance of an empty list.
  *
- * Writing to that list is undefined.
+ * Writing to that list is not allowed.
  */
-extern CxList * const cxEmptyList;
+extern CxList *const cxEmptyList;
 
 
 #ifdef __cplusplus
--- a/src/cx/map.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/map.h	Thu Nov 07 22:46:58 2024 +0100
@@ -89,19 +89,19 @@
     /**
      * Deallocates the entire memory.
      */
-    __attribute__((__nonnull__))
+    cx_attr_nonnull
     void (*destructor)(struct cx_map_s *map);
 
     /**
      * Removes all elements.
      */
-    __attribute__((__nonnull__))
+    cx_attr_nonnull
     void (*clear)(struct cx_map_s *map);
 
     /**
      * Add or overwrite an element.
      */
-    __attribute__((__nonnull__))
+    cx_attr_nonnull
     int (*put)(
             CxMap *map,
             CxHashKey key,
@@ -111,7 +111,8 @@
     /**
      * Returns an element.
      */
-    __attribute__((__nonnull__, __warn_unused_result__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     void *(*get)(
             const CxMap *map,
             CxHashKey key
@@ -120,7 +121,7 @@
     /**
      * Removes an element.
      */
-    __attribute__((__nonnull__))
+    cx_attr_nonnull
     void *(*remove)(
             CxMap *map,
             CxHashKey key,
@@ -130,7 +131,8 @@
     /**
      * Creates an iterator for this map.
      */
-    __attribute__((__nonnull__, __warn_unused_result__))
+    cx_attr_nonnull
+    cx_attr_nodiscard
     CxIterator (*iterator)(const CxMap *map, enum cx_map_iterator_type type);
 };
 
@@ -151,7 +153,7 @@
 /**
  * A shared instance of an empty map.
  *
- * Writing to that map is undefined.
+ * Writing to that map is not allowed.
  */
 extern CxMap *const cxEmptyMap;
 
@@ -164,7 +166,7 @@
  * @param map the map
  * @see cxMapStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapStoreObjects(CxMap *map) {
     map->collection.store_pointer = false;
 }
@@ -181,7 +183,7 @@
  * @param map the map
  * @see cxMapStoreObjects()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapStorePointers(CxMap *map) {
     map->collection.store_pointer = true;
     map->collection.elem_size = sizeof(void *);
@@ -194,7 +196,7 @@
  * @return true, if this map is storing pointers
  * @see cxMapStorePointers()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline bool cxMapIsStoringPointers(const CxMap *map) {
     return map->collection.store_pointer;
 }
@@ -215,7 +217,7 @@
  *
  * @param map the map to be cleared
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapClear(CxMap *map) {
     map->cl->clear(map);
 }
@@ -226,7 +228,7 @@
  * @param map the map
  * @return the number of stored elements
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline size_t cxMapSize(const CxMap *map) {
     return map->collection.size;
 }
@@ -243,7 +245,8 @@
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored values
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIteratorValues(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_VALUES);
 }
@@ -259,7 +262,8 @@
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored keys
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIteratorKeys(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_KEYS);
 }
@@ -277,7 +281,8 @@
  * @see cxMapIteratorKeys()
  * @see cxMapIteratorValues()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline CxIterator cxMapIterator(const CxMap *map) {
     return map->cl->iterator(map, CX_MAP_ITERATOR_PAIRS);
 }
@@ -292,7 +297,8 @@
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored values
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIteratorValues(CxMap *map);
 
 /**
@@ -306,7 +312,8 @@
  * @param map the map to create the iterator for
  * @return an iterator for the currently stored keys
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIteratorKeys(CxMap *map);
 
 /**
@@ -322,7 +329,8 @@
  * @see cxMapMutIteratorKeys()
  * @see cxMapMutIteratorValues()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxIterator cxMapMutIterator(CxMap *map);
 
 #ifdef __cplusplus
@@ -336,7 +344,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         CxHashKey const &key,
@@ -354,7 +362,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         cxstring const &key,
@@ -371,7 +379,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cxMapPut(
         CxMap *map,
         cxmutstr const &key,
@@ -388,7 +396,8 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cxMapPut(
         CxMap *map,
         const char *key,
@@ -404,7 +413,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         CxHashKey const &key
@@ -419,7 +429,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         cxstring const &key
@@ -434,7 +445,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapGet(
         const CxMap *map,
         cxmutstr const &key
@@ -449,7 +461,9 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cxMapGet(
         const CxMap *map,
         const char *key
@@ -471,7 +485,7 @@
  * @see cxMapRemoveAndGet()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapRemove(
         CxMap *map,
         CxHashKey const &key
@@ -493,7 +507,7 @@
  * @see cxMapRemoveAndGet()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapRemove(
         CxMap *map,
         cxstring const &key
@@ -515,7 +529,7 @@
  * @see cxMapRemoveAndGet()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapRemove(
         CxMap *map,
         cxmutstr const &key
@@ -537,7 +551,8 @@
  * @see cxMapRemoveAndGet()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline void cxMapRemove(
         CxMap *map,
         const char *key
@@ -559,7 +574,7 @@
  * @see cxMapRemove()
  * @see cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapDetach(
         CxMap *map,
         CxHashKey const &key
@@ -581,7 +596,7 @@
  * @see cxMapRemove()
  * @see cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapDetach(
         CxMap *map,
         cxstring const &key
@@ -603,7 +618,7 @@
  * @see cxMapRemove()
  * @see cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxMapDetach(
         CxMap *map,
         cxmutstr const &key
@@ -625,7 +640,8 @@
  * @see cxMapRemove()
  * @see cxMapRemoveAndGet()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline void cxMapDetach(
         CxMap *map,
         const char *key
@@ -652,7 +668,8 @@
  * @see cxMapStorePointers()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapRemoveAndGet(
         CxMap *map,
         CxHashKey key
@@ -679,7 +696,8 @@
  * @see cxMapStorePointers()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapRemoveAndGet(
         CxMap *map,
         cxstring key
@@ -706,7 +724,8 @@
  * @see cxMapStorePointers()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cxMapRemoveAndGet(
         CxMap *map,
         cxmutstr key
@@ -733,7 +752,9 @@
  * @see cxMapStorePointers()
  * @see cxMapDetach()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cxMapRemoveAndGet(
         CxMap *map,
         const char *key
@@ -751,7 +772,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put(
         CxMap *map,
         CxHashKey key,
@@ -768,7 +789,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put_cxstr(
         CxMap *map,
         cxstring key,
@@ -785,7 +806,7 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_map_put_mustr(
         CxMap *map,
         cxmutstr key,
@@ -802,7 +823,8 @@
  * @param value the value
  * @return 0 on success, non-zero value on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cx_map_put_str(
         CxMap *map,
         const char *key,
@@ -834,7 +856,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get(
         const CxMap *map,
         CxHashKey key
@@ -849,7 +872,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get_cxstr(
         const CxMap *map,
         cxstring key
@@ -864,7 +888,8 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_get_mustr(
         const CxMap *map,
         cxmutstr key
@@ -879,7 +904,9 @@
  * @param key the key
  * @return the value
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cx_map_get_str(
         const CxMap *map,
         const char *key
@@ -908,7 +935,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_remove(
         CxMap *map,
         CxHashKey key
@@ -922,7 +949,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_remove_cxstr(
         CxMap *map,
         cxstring key
@@ -936,7 +963,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_remove_mustr(
         CxMap *map,
         cxmutstr key
@@ -950,7 +977,8 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline void cx_map_remove_str(
         CxMap *map,
         const char *key
@@ -987,7 +1015,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_detach(
         CxMap *map,
         CxHashKey key
@@ -1002,7 +1030,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_detach_cxstr(
         CxMap *map,
         cxstring key
@@ -1017,7 +1045,7 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cx_map_detach_mustr(
         CxMap *map,
         cxmutstr key
@@ -1032,7 +1060,8 @@
  * @param map the map
  * @param key the key
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline void cx_map_detach_str(
         CxMap *map,
         const char *key
@@ -1070,7 +1099,8 @@
  * @return the stored pointer or \c NULL if either the key is not present
  * in the map or the map is not storing pointers
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_remove_and_get(
         CxMap *map,
         CxHashKey key
@@ -1086,7 +1116,8 @@
  * @return the stored pointer or \c NULL if either the key is not present
  * in the map or the map is not storing pointers
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_remove_and_get_cxstr(
         CxMap *map,
         cxstring key
@@ -1102,7 +1133,8 @@
  * @return the stored pointer or \c NULL if either the key is not present
  * in the map or the map is not storing pointers
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 static inline void *cx_map_remove_and_get_mustr(
         CxMap *map,
         cxmutstr key
@@ -1118,7 +1150,9 @@
  * @return the stored pointer or \c NULL if either the key is not present
  * in the map or the map is not storing pointers
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(2)
 static inline void *cx_map_remove_and_get_str(
         CxMap *map,
         const char *key
--- a/src/cx/mempool.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/mempool.h	Thu Nov 07 22:46:58 2024 +0100
@@ -76,15 +76,24 @@
 typedef struct cx_mempool_s CxMempool;
 
 /**
+ * Destroys a memory pool and frees the managed memory.
+ *
+ * @param pool the memory pool to destroy
+ */
+void cxMempoolDestroy(CxMempool *pool);
+
+/**
  * Creates an array-based memory pool with a shared destructor function.
  *
  * This destructor MUST NOT free the memory.
  *
  * @param capacity the initial capacity of the pool
- * @param destr the destructor function to use for allocated memory
+ * @param destr optional destructor function to use for allocated memory
  * @return the created memory pool or \c NULL if allocation failed
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxMempoolDestroy, 1)
 CxMempool *cxMempoolCreate(size_t capacity, cx_destructor_func destr);
 
 /**
@@ -93,18 +102,7 @@
  * @param capacity the initial capacity of the pool
  * @return the created memory pool or \c NULL if allocation failed
  */
-__attribute__((__warn_unused_result__))
-static inline CxMempool *cxBasicMempoolCreate(size_t capacity) {
-    return cxMempoolCreate(capacity, NULL);
-}
-
-/**
- * Destroys a memory pool and frees the managed memory.
- *
- * @param pool the memory pool to destroy
- */
-__attribute__((__nonnull__))
-void cxMempoolDestroy(CxMempool *pool);
+#define cxBasicMempoolCreate(capacity) cxMempoolCreate(capacity, NULL)
 
 /**
  * Sets the destructor function for a specific allocated memory object.
@@ -115,13 +113,24 @@
  * @param memory the object allocated in the pool
  * @param fnc the destructor function
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxMempoolSetDestructor(
         void *memory,
         cx_destructor_func fnc
 );
 
 /**
+ * Removes the destructor function for a specific allocated memory object.
+ *
+ * If the memory is not managed by a UCX memory pool, the behavior is undefined.
+ * The destructor MUST NOT free the memory.
+ *
+ * @param memory the object allocated in the pool
+ */
+cx_attr_nonnull
+void cxMempoolRemoveDestructor(void *memory);
+
+/**
  * Registers foreign memory with this pool.
  *
  * The destructor, in contrast to memory allocated by the pool, MUST free the memory.
@@ -130,11 +139,11 @@
  * If that allocation fails, this function will return non-zero.
  *
  * @param pool the pool
- * @param memory the object allocated in the pool
+ * @param memory the object to register (MUST NOT be already allocated in the pool)
  * @param destr the destructor function
  * @return zero on success, non-zero on failure
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 int cxMempoolRegister(
         CxMempool *pool,
         void *memory,
--- a/src/cx/printf.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/printf.h	Thu Nov 07 22:46:58 2024 +0100
@@ -40,6 +40,14 @@
 #include "string.h"
 #include <stdarg.h>
 
+/**
+ * Attribute for printf-like functions.
+ * @param fmt_idx index of the format string parameter
+ * @param arg_idx index of the first formatting argument
+ */
+#define cx_attr_printf(fmt_idx, arg_idx) \
+    __attribute__((__format__(printf, fmt_idx, arg_idx)))
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -60,7 +68,9 @@
  * @param ... additional arguments
  * @return the total number of bytes written
  */
-__attribute__((__nonnull__(1, 2, 3), __format__(printf, 3, 4)))
+cx_attr_nonnull_arg(1, 2, 3)
+cx_attr_printf(3, 4)
+cx_attr_cstr_arg(3)
 int cx_fprintf(
         void *stream,
         cx_write_func wfc,
@@ -79,7 +89,8 @@
  * @return the total number of bytes written
  * @see cx_fprintf()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(3)
 int cx_vfprintf(
         void *stream,
         cx_write_func wfc,
@@ -99,7 +110,9 @@
  * @return the formatted string
  * @see cx_strfree_a()
  */
-__attribute__((__nonnull__(1, 2), __format__(printf, 2, 3)))
+cx_attr_nonnull_arg(1, 2)
+cx_attr_printf(2, 3)
+cx_attr_cstr_arg(2)
 cxmutstr cx_asprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -132,7 +145,8 @@
  * @return the formatted string
  * @see cx_asprintf_a()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 cxmutstr cx_vasprintf_a(
         const CxAllocator *allocator,
         const char *fmt,
@@ -196,8 +210,16 @@
  * @param ... additional arguments
  * @return the length of produced string
  */
-__attribute__((__nonnull__(1, 2, 3, 4), __format__(printf, 4, 5)))
-int cx_sprintf_a(CxAllocator *alloc, char **str, size_t *len, const char *fmt, ... );
+cx_attr_nonnull_arg(1, 2, 3, 4)
+cx_attr_printf(4, 5)
+cx_attr_cstr_arg(4)
+int cx_sprintf_a(
+        CxAllocator *alloc,
+        char **str,
+        size_t *len,
+        const char *fmt,
+        ...
+);
 
 
 /**
@@ -231,8 +253,15 @@
  * @param ap argument list
  * @return the length of produced string
  */
-__attribute__((__nonnull__))
-int cx_vsprintf_a(CxAllocator *alloc, char **str, size_t *len, const char *fmt, va_list ap);
+cx_attr_nonnull
+cx_attr_cstr_arg(4)
+int cx_vsprintf_a(
+        CxAllocator *alloc,
+        char **str,
+        size_t *len,
+        const char *fmt,
+        va_list ap
+);
 
 
 /**
@@ -279,7 +308,17 @@
  * @return the length of produced string
  */
 __attribute__((__nonnull__(1, 2, 4, 5), __format__(printf, 5, 6)))
-int cx_sprintf_sa(CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, ... );
+cx_attr_nonnull_arg(1, 2, 4, 5)
+cx_attr_printf(5, 6)
+cx_attr_cstr_arg(5)
+int cx_sprintf_sa(
+        CxAllocator *alloc,
+        char *buf,
+        size_t *len,
+        char **str,
+        const char *fmt,
+        ...
+);
 
 /**
  * An \c sprintf like function which allocates a new string when the buffer is not large enough.
@@ -324,8 +363,16 @@
  * @param ap argument list
  * @return the length of produced string
  */
-__attribute__((__nonnull__))
-int cx_vsprintf_sa(CxAllocator *alloc, char *buf, size_t *len, char **str, const char *fmt, va_list ap);
+cx_attr_nonnull
+cx_attr_cstr_arg(5)
+int cx_vsprintf_sa(
+        CxAllocator *alloc,
+        char *buf,
+        size_t *len,
+        char **str,
+        const char *fmt,
+        va_list ap
+);
 
 
 #ifdef __cplusplus
--- a/src/cx/properties.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/properties.h	Thu Nov 07 22:46:58 2024 +0100
@@ -227,7 +227,7 @@
  * @param value the value
  * @return zero on success, non-zero when sinking the k/v-pair failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 typedef int(*cx_properties_sink_func)(
         CxProperties *prop,
         CxPropertiesSink *sink,
@@ -273,7 +273,7 @@
  * @param target a string buffer where the read data shall be stored
  * @return zero on success, non-zero when reading data failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 typedef int(*cx_properties_read_func)(
         CxProperties *prop,
         CxPropertiesSource *src,
@@ -287,7 +287,7 @@
  * @param src the source
  * @return zero when initialization was successful, non-zero otherwise
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 typedef int(*cx_properties_read_init_func)(
         CxProperties *prop,
         CxPropertiesSource *src
@@ -299,7 +299,7 @@
  * @param prop the properties interface that wants to read from the source
  * @param src the source
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 typedef void(*cx_properties_read_clean_func)(
         CxProperties *prop,
         CxPropertiesSource *src
@@ -345,7 +345,7 @@
  * @param config the properties configuration
  * @see cxPropertiesInitDefault()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxPropertiesInit(CxProperties *prop, CxPropertiesConfig config);
 
 /**
@@ -359,7 +359,7 @@
  *
  * @param prop the properties interface
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxPropertiesDestroy(CxProperties *prop);
 
 /**
@@ -388,7 +388,8 @@
  * @param len the length of the data
  * @return non-zero when a memory allocation was necessary but failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
 int cxPropertiesFilln(
         CxProperties *prop,
         const char *buf,
@@ -422,7 +423,7 @@
  * @param str the string
  * @return non-zero when a memory allocation was necessary but failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_properties_fill_cxstr(
         CxProperties *prop,
         cxstring str
@@ -437,7 +438,7 @@
  * @param str the string
  * @return non-zero when a memory allocation was necessary but failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline int cx_properties_fill_mutstr(
         CxProperties *prop,
         cxmutstr str
@@ -452,7 +453,8 @@
  * @param str the string
  * @return non-zero when a memory allocation was necessary but failed
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_cstr_arg(2)
 static inline int cx_properties_fill_str(
         CxProperties *prop,
         const char *str
@@ -467,7 +469,7 @@
  * @param buf a pointer to stack memory
  * @param capacity the capacity of the stack memory
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxPropertiesUseStack(
         CxProperties *prop,
         char *buf,
@@ -494,7 +496,8 @@
  * @return the status code as defined above
  * @see cxPropertiesFill()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxPropertiesStatus cxPropertiesNext(
         CxProperties *prop,
         cxstring *key,
@@ -513,7 +516,8 @@
  * @return the sink
  * @see cxPropertiesLoad()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
 CxPropertiesSink cxPropertiesMapSink(CxMap *map);
 
 /**
@@ -523,7 +527,7 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxPropertiesSource cxPropertiesStringSource(cxstring str);
 
 /**
@@ -534,7 +538,9 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 CxPropertiesSource cxPropertiesCstrnSource(const char *str, size_t len);
 
 /**
@@ -547,7 +553,9 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 CxPropertiesSource cxPropertiesCstrSource(const char *str);
 
 /**
@@ -559,7 +567,9 @@
  * @return the properties source
  * @see cxPropertiesLoad()
  */
-__attribute__((__nonnull__, __warn_unused_result__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_r(1)
 CxPropertiesSource cxPropertiesFileSource(FILE *file, size_t chunk_size);
 
 
@@ -578,7 +588,7 @@
  * @param source the source
  * @return the status of the last operation
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 CxPropertiesStatus cxPropertiesLoad(
         CxProperties *prop,
         CxPropertiesSink sink,
--- a/src/cx/streams.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/streams.h	Thu Nov 07 22:46:58 2024 +0100
@@ -61,7 +61,10 @@
  * iterations.
  * @return the total number of bytes copied
  */
-__attribute__((__nonnull__(1, 2, 3, 4)))
+cx_attr_nonnull_arg(1, 2, 3, 4)
+cx_attr_access_r(1)
+cx_attr_access_w(2)
+cx_attr_access_w(5)
 size_t cx_stream_bncopy(
         void *src,
         void *dest,
@@ -100,7 +103,9 @@
  * @param n the maximum number of bytes that shall be copied.
  * @return total number of bytes copied
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_r(1)
+cx_attr_access_w(2)
 size_t cx_stream_ncopy(
         void *src,
         void *dest,
--- a/src/cx/string.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/string.h	Thu Nov 07 22:46:58 2024 +0100
@@ -172,7 +172,9 @@
  *
  * @see cx_mutstrn()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 cxmutstr cx_mutstr(char *cstring);
 
 /**
@@ -191,7 +193,8 @@
  *
  * @see cx_mutstr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_rw(1, 2)
 cxmutstr cx_mutstrn(
         char *cstring,
         size_t length
@@ -212,7 +215,9 @@
  *
  * @see cx_strn()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
 cxstring cx_str(const char *cstring);
 
 
@@ -232,7 +237,8 @@
  *
  * @see cx_str()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
+cx_attr_access_r(1, 2)
 cxstring cx_strn(
         const char *cstring,
         size_t length
@@ -248,7 +254,7 @@
 * @param str the mutable string to cast
 * @return an immutable copy of the string pointer
 */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strcast(cxmutstr str);
 
 /**
@@ -262,7 +268,6 @@
  *
  * @param str the string to free
  */
-__attribute__((__nonnull__))
 void cx_strfree(cxmutstr *str);
 
 /**
@@ -277,7 +282,7 @@
  * @param alloc the allocator
  * @param str the string to free
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull_arg(1)
 void cx_strfree_a(
         const CxAllocator *alloc,
         cxmutstr *str
@@ -293,7 +298,7 @@
  * @param ...      all strings
  * @return the accumulated length of all strings
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 size_t cx_strlen(
         size_t count,
         ...
@@ -317,7 +322,8 @@
  * @param ...   all other strings
  * @return the concatenated string
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strcat_ma(
         const CxAllocator *alloc,
         cxmutstr str,
@@ -393,7 +399,7 @@
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strsubs(
         cxstring string,
         size_t start
@@ -418,7 +424,7 @@
  * @see cx_strsubs_m()
  * @see cx_strsubsl_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strsubsl(
         cxstring string,
         size_t start,
@@ -440,7 +446,7 @@
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strsubs_m(
         cxmutstr string,
         size_t start
@@ -465,7 +471,7 @@
  * @see cx_strsubs()
  * @see cx_strsubsl()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strsubsl_m(
         cxmutstr string,
         size_t start,
@@ -484,7 +490,7 @@
  *
  * @see cx_strchr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strchr(
         cxstring string,
         int chr
@@ -502,7 +508,7 @@
  *
  * @see cx_strchr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strchr_m(
         cxmutstr string,
         int chr
@@ -520,7 +526,7 @@
  *
  * @see cx_strrchr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strrchr(
         cxstring string,
         int chr
@@ -538,7 +544,7 @@
  *
  * @see cx_strrchr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strrchr_m(
         cxmutstr string,
         int chr
@@ -560,7 +566,7 @@
  *               contained
  * @see cx_strstr_m()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strstr(
         cxstring haystack,
         cxstring needle
@@ -582,7 +588,7 @@
  *               contained
  * @see cx_strstr()
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strstr_m(
         cxmutstr haystack,
         cxstring needle
@@ -600,7 +606,9 @@
  * @param output a pre-allocated array of at least \p limit length
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(4, 3)
 size_t cx_strsplit(
         cxstring string,
         cxstring delim,
@@ -627,7 +635,9 @@
  * written to
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(5)
 size_t cx_strsplit_a(
         const CxAllocator *allocator,
         cxstring string,
@@ -649,7 +659,9 @@
  * @param output a pre-allocated array of at least \p limit length
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(4, 3)
 size_t cx_strsplit_m(
         cxmutstr string,
         cxstring delim,
@@ -676,7 +688,9 @@
  * written to
  * @return the actual number of split items
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
+cx_attr_access_w(5)
 size_t cx_strsplit_ma(
         const CxAllocator *allocator,
         cxmutstr string,
@@ -693,7 +707,7 @@
  * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
  * than \p s2, zero if both strings equal
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 int cx_strcmp(
         cxstring s1,
         cxstring s2
@@ -707,7 +721,7 @@
  * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
  * than \p s2, zero if both strings equal ignoring case
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 int cx_strcasecmp(
         cxstring s1,
         cxstring s2
@@ -723,7 +737,8 @@
  * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
  * than \p s2, zero if both strings equal
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cx_strcmp_p(
         const void *s1,
         const void *s2
@@ -739,7 +754,8 @@
  * @return negative if \p s1 is smaller than \p s2, positive if \p s1 is larger
  * than \p s2, zero if both strings equal ignoring case
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 int cx_strcasecmp_p(
         const void *s1,
         const void *s2
@@ -758,7 +774,8 @@
  * @return a duplicate of the string
  * @see cx_strdup()
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strdup_a(
         const CxAllocator *allocator,
         cxstring string
@@ -816,7 +833,7 @@
  * @param string the string that shall be trimmed
  * @return the trimmed string
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxstring cx_strtrim(cxstring string);
 
 /**
@@ -828,7 +845,7 @@
  * @param string the string that shall be trimmed
  * @return the trimmed string
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 cxmutstr cx_strtrim_m(cxmutstr string);
 
 /**
@@ -839,7 +856,7 @@
  * @return \c true, if and only if the string has the specified prefix,
  * \c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strprefix(
         cxstring string,
         cxstring prefix
@@ -853,7 +870,7 @@
  * @return \c true, if and only if the string has the specified suffix,
  * \c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strsuffix(
         cxstring string,
         cxstring suffix
@@ -867,7 +884,7 @@
  * @return \c true, if and only if the string has the specified prefix,
  * \c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strcaseprefix(
         cxstring string,
         cxstring prefix
@@ -881,7 +898,7 @@
  * @return \c true, if and only if the string has the specified suffix,
  * \c false otherwise
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 bool cx_strcasesuffix(
         cxstring string,
         cxstring suffix
@@ -926,7 +943,8 @@
  * @param replmax maximum number of replacements
  * @return the resulting string after applying the replacements
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nodiscard
+cx_attr_nonnull
 cxmutstr cx_strreplacen_a(
         const CxAllocator *allocator,
         cxstring str,
@@ -1004,7 +1022,7 @@
  * @param limit the maximum number of tokens that shall be returned
  * @return a new string tokenization context
  */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxStrtokCtx cx_strtok(
         cxstring str,
         cxstring delim,
@@ -1019,7 +1037,7 @@
 * @param limit the maximum number of tokens that shall be returned
 * @return a new string tokenization context
 */
-__attribute__((__warn_unused_result__))
+cx_attr_nodiscard
 CxStrtokCtx cx_strtok_m(
         cxmutstr str,
         cxstring delim,
@@ -1036,7 +1054,9 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_w(2)
 bool cx_strtok_next(
         CxStrtokCtx *ctx,
         cxstring *token
@@ -1054,7 +1074,9 @@
  * @return true if successful, false if the limit or the end of the string
  * has been reached
  */
-__attribute__((__warn_unused_result__, __nonnull__))
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_access_w(2)
 bool cx_strtok_next_m(
         CxStrtokCtx *ctx,
         cxmutstr *token
@@ -1067,7 +1089,8 @@
  * @param delim array of more delimiters
  * @param count number of elements in the array
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_r(2, 3)
 void cx_strtok_delim(
         CxStrtokCtx *ctx,
         const cxstring *delim,
--- a/src/cx/test.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/test.h	Thu Nov 07 22:46:58 2024 +0100
@@ -96,6 +96,7 @@
 typedef struct CxTestSuite CxTestSuite;
 
 /** Pointer to a test function. */
+cx_attr_nonnull
 typedef void(*CxTest)(CxTestSuite *, void *, cx_write_func);
 
 /** Type for the internal list of test cases. */
@@ -137,6 +138,10 @@
  * @param name optional name of the suite
  * @return a new test suite
  */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_cstr_arg(1)
+cx_attr_malloc
 static inline CxTestSuite* cx_test_suite_new(const char *name) {
     CxTestSuite* suite = (CxTestSuite*) malloc(sizeof(CxTestSuite));
     if (suite != NULL) {
@@ -154,6 +159,7 @@
  * @param suite the test suite to destroy
  */
 static inline void cx_test_suite_free(CxTestSuite* suite) {
+    if (suite == NULL) return;
     CxTestSet *l = suite->tests;
     while (l != NULL) {
         CxTestSet *e = l;
@@ -170,6 +176,7 @@
  * @param test the test function to register
  * @return zero on success or non-zero on failure
  */
+cx_attr_nonnull
 static inline int cx_test_register(CxTestSuite* suite, CxTest test) {
     CxTestSet *t = (CxTestSet*) malloc(sizeof(CxTestSet));
     if (t) {
@@ -196,6 +203,7 @@
  * @param out_target the target buffer or file to write the output to
  * @param out_writer the write function writing to \p out_target
  */
+cx_attr_nonnull
 static inline void cx_test_run(CxTestSuite *suite,
                                void *out_target, cx_write_func out_writer) {
     if (suite->name == NULL) {
--- a/src/cx/tree.h	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/cx/tree.h	Thu Nov 07 22:46:58 2024 +0100
@@ -209,7 +209,7 @@
  * Releases internal memory of the given tree iterator.
  * @param iter the iterator
  */
- __attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxTreeIteratorDispose(CxTreeIterator *iter) {
     free(iter->stack);
     iter->stack = NULL;
@@ -219,7 +219,7 @@
  * Releases internal memory of the given tree visitor.
  * @param visitor the visitor
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 static inline void cxTreeVisitorDispose(CxTreeVisitor *visitor) {
     struct cx_tree_visitor_queue_s *q = visitor->queue_next;
     while (q != NULL) {
@@ -262,7 +262,7 @@
  * @param loc_next offset in the node struct for the next pointer
  * @see cx_tree_unlink()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_tree_link(
         void *restrict parent,
         void *restrict node,
@@ -287,7 +287,7 @@
  * @param loc_next offset in the node struct for the next pointer
  * @see cx_tree_link()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cx_tree_unlink(
         void *node,
         ptrdiff_t loc_parent,
@@ -328,6 +328,7 @@
  * positive if one of the children might contain the data,
  * negative if neither the node, nor the children contains the data
  */
+cx_attr_nonnull
 typedef int (*cx_tree_search_data_func)(const void *node, const void *data);
 
 
@@ -357,6 +358,7 @@
  * positive if one of the children might contain the data,
  * negative if neither the node, nor the children contains the data
  */
+cx_attr_nonnull
 typedef int (*cx_tree_search_func)(const void *node, const void *new_node);
 
 /**
@@ -383,7 +385,8 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(5)
 int cx_tree_search_data(
         const void *root,
         size_t depth,
@@ -418,7 +421,8 @@
  * could contain the node (but doesn't right now), negative if the tree does not
  * contain any node that might be related to the searched data
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
+cx_attr_access_w(5)
 int cx_tree_search(
         const void *root,
         size_t depth,
@@ -449,6 +453,7 @@
  * @return the new tree iterator
  * @see cxTreeIteratorDispose()
  */
+cx_attr_nodiscard
 CxTreeIterator cx_tree_iterator(
         void *root,
         bool visit_on_exit,
@@ -474,6 +479,7 @@
  * @return the new tree visitor
  * @see cxTreeVisitorDispose()
  */
+cx_attr_nodiscard
 CxTreeVisitor cx_tree_visitor(
         void *root,
         ptrdiff_t loc_children,
@@ -490,6 +496,7 @@
  * \note the function may leave the node pointers in the struct uninitialized.
  * The caller is responsible to set them according to the intended use case.
  */
+cx_attr_nonnull_arg(1)
 typedef void *(*cx_tree_node_create_func)(const void *, void *);
 
 /**
@@ -538,7 +545,8 @@
  * @return the number of nodes created and added
  * @see cx_tree_add()
  */
-__attribute__((__nonnull__(1, 3, 4, 6, 7)))
+cx_attr_nonnull_arg(1, 3, 4, 6, 7)
+cx_attr_access_w(6)
 size_t cx_tree_add_iter(
         struct cx_iterator_base_s *iter,
         size_t num,
@@ -591,7 +599,8 @@
  * @return the number of array elements successfully processed
  * @see cx_tree_add()
  */
-__attribute__((__nonnull__(1, 4, 5, 7, 8)))
+cx_attr_nonnull_arg(1, 4, 5, 7, 8)
+cx_attr_access_w(7)
 size_t cx_tree_add_array(
         const void *src,
         size_t num,
@@ -653,7 +662,8 @@
  * @return zero when a new node was created and added to the tree,
  * non-zero otherwise
  */
-__attribute__((__nonnull__(1, 2, 3, 5, 6)))
+cx_attr_nonnull_arg(1, 2, 3, 5, 6)
+cx_attr_access_w(5)
 int cx_tree_add(
         const void *src,
         cx_tree_search_func sfunc,
@@ -829,6 +839,7 @@
      * Implementations SHALL NOT simply invoke \p insert_many as this comes
      * with too much overhead.
      */
+    cx_attr_nonnull
     int (*insert_element)(
             struct cx_tree_s *tree,
             const void *data
@@ -840,6 +851,7 @@
      * Implementations SHALL avoid to perform a full search in the tree for
      * every element even though the source data MAY be unsorted.
      */
+    cx_attr_nonnull
     size_t (*insert_many)(
             struct cx_tree_s *tree,
             struct cx_iterator_base_s *iter,
@@ -849,6 +861,7 @@
     /**
      * Member function for finding a node.
      */
+    cx_attr_nonnull
     void *(*find)(
             struct cx_tree_s *tree,
             const void *subtree,
@@ -862,461 +875,6 @@
  */
 typedef struct cx_tree_s CxTree;
 
-/**
- * Creates a new tree structure based on the specified layout.
- *
- * The specified \p allocator will be used for creating the tree struct
- * and SHALL be used by \p create_func to allocate memory for the nodes.
- *
- * \note This function will also register an advanced destructor which
- * will free the nodes with the allocator's free() method.
- *
- * @param allocator the allocator that shall be used
- * @param create_func a function that creates new nodes
- * @param search_func a function that compares two nodes
- * @param search_data_func a function that compares a node with data
- * @param loc_parent offset in the node struct for the parent pointer
- * @param loc_children offset in the node struct for the children linked list
- * @param loc_last_child optional offset in the node struct for the pointer to
- * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev optional offset in the node struct for the prev pointer
- * @param loc_next offset in the node struct for the next pointer
- * @return the new tree
- * @see cxTreeCreateSimple()
- * @see cxTreeCreateWrapped()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-CxTree *cxTreeCreate(
-        const CxAllocator *allocator,
-        cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func,
-        cx_tree_search_data_func search_data_func,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
-
-/**
- * Creates a new tree structure based on a default layout.
- *
- * Nodes created by \p create_func MUST contain #cx_tree_node_base_s as first
- * member (or at least respect the default offsets specified in the tree
- * struct) and they MUST be allocated with the specified allocator.
- *
- * \note This function will also register an advanced destructor which
- * will free the nodes with the allocator's free() method.
- *
- * @param allocator the allocator that shall be used
- * @param create_func a function that creates new nodes
- * @param search_func a function that compares two nodes
- * @param search_data_func a function that compares a node with data
- * @return the new tree
- * @see cxTreeCreate()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTree *cxTreeCreateSimple(
-        const CxAllocator *allocator,
-        cx_tree_node_create_func create_func,
-        cx_tree_search_func search_func,
-        cx_tree_search_data_func search_data_func
-) {
-    return cxTreeCreate(
-            allocator,
-            create_func,
-            search_func,
-            search_data_func,
-            cx_tree_node_base_layout
-    );
-}
-
-/**
- * Creates a new tree structure based on an existing tree.
- *
- * The specified \p allocator will be used for creating the tree struct.
- *
- * \attention This function will create an incompletely defined tree structure
- * where neither the create function, the search function, nor a destructor
- * will be set. If you wish to use any of this functionality for the wrapped
- * tree, you need to specify those functions afterwards.
- *
- * @param allocator the allocator that was used for nodes of the wrapped tree
- * @param root the root node of the tree that shall be wrapped
- * @param loc_parent offset in the node struct for the parent pointer
- * @param loc_children offset in the node struct for the children linked list
- * @param loc_last_child optional offset in the node struct for the pointer to
- * the last child in the linked list (negative if there is no such pointer)
- * @param loc_prev optional offset in the node struct for the prev pointer
- * @param loc_next offset in the node struct for the next pointer
- * @return the new tree
- * @see cxTreeCreate()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-CxTree *cxTreeCreateWrapped(
-        const CxAllocator *allocator,
-        void *root,
-        ptrdiff_t loc_parent,
-        ptrdiff_t loc_children,
-        ptrdiff_t loc_last_child,
-        ptrdiff_t loc_prev,
-        ptrdiff_t loc_next
-);
-
-/**
- * Inserts data into the tree.
- *
- * \remark For this function to work, the tree needs specified search and
- * create functions, which might not be available for wrapped trees
- * (see #cxTreeCreateWrapped()).
- *
- * @param tree the tree
- * @param data the data to insert
- * @return zero on success, non-zero on failure
- */
-__attribute__((__nonnull__))
-static inline int cxTreeInsert(
-        CxTree *tree,
-        const void *data
-) {
-    return tree->cl->insert_element(tree, data);
-}
-
-/**
- * Inserts elements provided by an iterator efficiently into the tree.
- *
- * \remark For this function to work, the tree needs specified search and
- * create functions, which might not be available for wrapped trees
- * (see #cxTreeCreateWrapped()).
- *
- * @param tree the tree
- * @param iter the iterator providing the elements
- * @param n the maximum number of elements to insert
- * @return the number of elements that could be successfully inserted
- */
-__attribute__((__nonnull__))
-static inline size_t cxTreeInsertIter(
-        CxTree *tree,
-        struct cx_iterator_base_s *iter,
-        size_t n
-) {
-    return tree->cl->insert_many(tree, iter, n);
-}
-
-/**
- * Inserts an array of data efficiently into the tree.
- *
- * \remark For this function to work, the tree needs specified search and
- * create functions, which might not be available for wrapped trees
- * (see #cxTreeCreateWrapped()).
- *
- * @param tree the tree
- * @param data the array of data to insert
- * @param elem_size the size of each element in the array
- * @param n the number of elements in the array
- * @return the number of elements that could be successfully inserted
- */
-__attribute__((__nonnull__))
-static inline size_t cxTreeInsertArray(
-        CxTree *tree,
-        const void *data,
-        size_t elem_size,
-        size_t n
-) {
-    if (n == 0) return 0;
-    if (n == 1) return 0 == cxTreeInsert(tree, data) ? 1 : 0;
-    CxIterator iter = cxIterator(data, elem_size, n);
-    return cxTreeInsertIter(tree, cxIteratorRef(iter), n);
-}
-
-/**
- * Searches the data in the specified tree.
- *
- * \remark For this function to work, the tree needs a specified \c search_data
- * function, which might not be available wrapped trees
- * (see #cxTreeCreateWrapped()).
- *
- * @param tree the tree
- * @param data the data to search for
- * @return the first matching node, or \c NULL when the data cannot be found
- */
-__attribute__((__nonnull__))
-static inline void *cxTreeFind(
-        CxTree *tree,
-        const void *data
-) {
-    return tree->cl->find(tree, tree->root, data, 0);
-}
-
-/**
- * Searches the data in the specified subtree.
- *
- * When \p max_depth is zero, the depth is not limited.
- * The \p subtree_root itself is on depth 1 and its children have depth 2.
- *
- * \note When \p subtree_root is not part of the \p tree, the behavior is
- * undefined.
- *
- * \remark For this function to work, the tree needs a specified \c search_data
- * function, which might not be the case for wrapped trees
- * (see #cxTreeCreateWrapped()).
- *
- * @param tree the tree
- * @param data the data to search for
- * @param subtree_root the node where to start
- * @param max_depth the maximum search depth
- * @return the first matching node, or \c NULL when the data cannot be found
- */
-__attribute__((__nonnull__))
-static inline void *cxTreeFindInSubtree(
-        CxTree *tree,
-        const void *data,
-        void *subtree_root,
-        size_t max_depth
-) {
-    return tree->cl->find(tree, subtree_root, data, max_depth);
-}
-
-/**
- * Determines the size of the specified subtree.
- *
- * @param tree the tree
- * @param subtree_root the root node of the subtree
- * @return the number of nodes in the specified subtree
- */
-__attribute__((__nonnull__))
-size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
-
-/**
- * Determines the depth of the specified subtree.
- *
- * @param tree the tree
- * @param subtree_root the root node of the subtree
- * @return the tree depth including the \p subtree_root
- */
-__attribute__((__nonnull__))
-size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
-
-/**
- * Determines the depth of the entire tree.
- *
- * @param tree the tree
- * @return the tree depth, counting the root as one
- */
-__attribute__((__nonnull__))
-size_t cxTreeDepth(CxTree *tree);
-
-/**
- * Creates a depth-first iterator for the specified tree starting in \p node.
- *
- * If the node is not part of the tree, the behavior is undefined.
- *
- * @param tree the tree to iterate
- * @param node the node where to start
- * @param visit_on_exit true, if the iterator shall visit a node again when
- * leaving the subtree
- * @return a tree iterator (depth-first)
- * @see cxTreeVisit()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeIterator cxTreeIterateSubtree(
-        CxTree *tree,
-        void *node,
-        bool visit_on_exit
-) {
-    return cx_tree_iterator(
-            node, visit_on_exit,
-            tree->loc_children, tree->loc_next
-    );
-}
-
-/**
- * Creates a breadth-first iterator for the specified tree starting in \p node.
- *
- * If the node is not part of the tree, the behavior is undefined.
- *
- * @param tree the tree to iterate
- * @param node the node where to start
- * @return a tree visitor (a.k.a. breadth-first iterator)
- * @see cxTreeIterate()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
-    return cx_tree_visitor(
-            node, tree->loc_children, tree->loc_next
-    );
-}
-
-/**
- * Creates a depth-first iterator for the specified tree.
- *
- * @param tree the tree to iterate
- * @param visit_on_exit true, if the iterator shall visit a node again when
- * leaving the subtree
- * @return a tree iterator (depth-first)
- * @see cxTreeVisit()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeIterator cxTreeIterate(
-        CxTree *tree,
-        bool visit_on_exit
-) {
-    return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
-}
-
-/**
- * Creates a breadth-first iterator for the specified tree.
- *
- * @param tree the tree to iterate
- * @return a tree visitor (a.k.a. breadth-first iterator)
- * @see cxTreeIterate()
- */
-__attribute__((__nonnull__, __warn_unused_result__))
-static inline CxTreeVisitor cxTreeVisit(CxTree *tree) {
-    return cxTreeVisitSubtree(tree, tree->root);
-}
-
-/**
- * Sets the (new) parent of the specified child.
- *
- * If the \p child is not already member of the tree, this function behaves
- * as #cxTreeAddChildNode().
- *
- * @param tree the tree
- * @param parent the (new) parent of the child
- * @param child the node to add
- * @see cxTreeAddChildNode()
- */
-__attribute__((__nonnull__))
-void cxTreeSetParent(
-        CxTree *tree,
-        void *parent,
-        void *child
-);
-
-/**
- * Adds a new node to the tree.
- *
- * If the \p child is already member of the tree, the behavior is undefined.
- * Use #cxTreeSetParent() if you want to move a subtree to another location.
- *
- * \attention The node may be externally created, but MUST obey the same rules
- * as if it was created by the tree itself with #cxTreeAddChild() (e.g. use
- * the same allocator).
- *
- * @param tree the tree
- * @param parent the parent of the node to add
- * @param child the node to add
- * @see cxTreeSetParent()
- */
-__attribute__((__nonnull__))
-void cxTreeAddChildNode(
-        CxTree *tree,
-        void *parent,
-        void *child
-);
-
-/**
- * Creates a new node and adds it to the tree.
- *
- * With this function you can decide where exactly the new node shall be added.
- * If you specified an appropriate search function, you may want to consider
- * leaving this task to the tree by using #cxTreeInsert().
- *
- * Be aware that adding nodes at arbitrary locations in the tree might cause
- * wrong or undesired results when subsequently invoking #cxTreeInsert() and
- * the invariant imposed by the search function does not hold any longer.
- *
- * @param tree the tree
- * @param parent the parent node of the new node
- * @param data the data that will be submitted to the create function
- * @return zero when the new node was created, non-zero on allocation failure
- * @see cxTreeInsert()
- */
-__attribute__((__nonnull__))
-int cxTreeAddChild(
-        CxTree *tree,
-        void *parent,
-        const void *data
-);
-
-/**
- * A function that is invoked when a node needs to be re-linked to a new parent.
- *
- * When a node is re-linked, sometimes the contents need to be updated.
- * This callback is invoked by #cxTreeRemoveNode() and #cxTreeDestroyNode()
- * so that those updates can be applied when re-linking the children of the
- * removed node.
- *
- * @param node the affected node
- * @param old_parent the old parent of the node
- * @param new_parent the new parent of the node
- */
-typedef void (*cx_tree_relink_func)(
-        void *node,
-        const void *old_parent,
-        const void *new_parent
-);
-
-/**
- * Removes a node and re-links its children to its former parent.
- *
- * If the node is not part of the tree, the behavior is undefined.
- *
- * \note The destructor function, if any, will \em not be invoked. That means
- * you will need to free the removed node by yourself, eventually.
- *
- * @param tree the tree
- * @param node the node to remove (must not be the root node)
- * @param relink_func optional callback to update the content of each re-linked
- * node
- * @return zero on success, non-zero if \p node is the root node of the tree
- */
-__attribute__((__nonnull__(1,2)))
-int cxTreeRemoveNode(
-        CxTree *tree,
-        void *node,
-        cx_tree_relink_func relink_func
-);
-
-/**
- * Removes a node and it's subtree from the tree.
- *
- * If the node is not part of the tree, the behavior is undefined.
- *
- * \note The destructor function, if any, will \em not be invoked. That means
- * you will need to free the removed subtree by yourself, eventually.
- *
- * @param tree the tree
- * @param node the node to remove
- */
-__attribute__((__nonnull__))
-void cxTreeRemoveSubtree(CxTree *tree, void *node);
-
-/**
- * Destroys a node and re-links its children to its former parent.
- *
- * If the node is not part of the tree, the behavior is undefined.
- *
- * It is guaranteed that the simple destructor is invoked before
- * the advanced destructor.
- *
- * \attention This function will not free the memory of the node with the
- * tree's allocator, because that is usually done by the advanced destructor
- * and would therefore result in a double-free.
- *
- * @param tree the tree
- * @param node the node to destroy (must not be the root node)
- * @param relink_func optional callback to update the content of each re-linked
- * node
- * @return zero on success, non-zero if \p node is the root node of the tree
- */
-__attribute__((__nonnull__(1,2)))
-int cxTreeDestroyNode(
-        CxTree *tree,
-        void *node,
-        cx_tree_relink_func relink_func
-);
 
 /**
  * Destroys a node and it's subtree.
@@ -1339,7 +897,7 @@
  * @param node the node to remove
  * @see cxTreeDestroy()
  */
-__attribute__((__nonnull__))
+cx_attr_nonnull
 void cxTreeDestroySubtree(CxTree *tree, void *node);
 
 
@@ -1378,14 +936,475 @@
  *
  * @param tree the tree to destroy
  */
-__attribute__((__nonnull__))
 static inline void cxTreeDestroy(CxTree *tree) {
+    if (tree == NULL) return;
     if (tree->root != NULL) {
-        cxTreeDestroySubtree(tree, tree->root);
+        cxTreeClear(tree);
     }
     cxFree(tree->allocator, tree);
 }
 
+/**
+ * Creates a new tree structure based on the specified layout.
+ *
+ * The specified \p allocator will be used for creating the tree struct
+ * and SHALL be used by \p create_func to allocate memory for the nodes.
+ *
+ * \note This function will also register an advanced destructor which
+ * will free the nodes with the allocator's free() method.
+ *
+ * @param allocator the allocator that shall be used
+ * @param create_func a function that creates new nodes
+ * @param search_func a function that compares two nodes
+ * @param search_data_func a function that compares a node with data
+ * @param loc_parent offset in the node struct for the parent pointer
+ * @param loc_children offset in the node struct for the children linked list
+ * @param loc_last_child optional offset in the node struct for the pointer to
+ * the last child in the linked list (negative if there is no such pointer)
+ * @param loc_prev optional offset in the node struct for the prev pointer
+ * @param loc_next offset in the node struct for the next pointer
+ * @return the new tree
+ * @see cxTreeCreateSimple()
+ * @see cxTreeCreateWrapped()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxTreeDestroy, 1)
+CxTree *cxTreeCreate(
+        const CxAllocator *allocator,
+        cx_tree_node_create_func create_func,
+        cx_tree_search_func search_func,
+        cx_tree_search_data_func search_data_func,
+        ptrdiff_t loc_parent,
+        ptrdiff_t loc_children,
+        ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next
+);
+
+/**
+ * Creates a new tree structure based on a default layout.
+ *
+ * Nodes created by \p create_func MUST contain #cx_tree_node_base_s as first
+ * member (or at least respect the default offsets specified in the tree
+ * struct) and they MUST be allocated with the specified allocator.
+ *
+ * \note This function will also register an advanced destructor which
+ * will free the nodes with the allocator's free() method.
+ *
+ * @param allocator the allocator that shall be used
+ * @param create_func a function that creates new nodes
+ * @param search_func a function that compares two nodes
+ * @param search_data_func a function that compares a node with data
+ * @return the new tree
+ * @see cxTreeCreate()
+ */
+#define cxTreeCreateSimple(\
+    allocator, create_func, search_func, search_data_func \
+) cxTreeCreate(allocator, create_func, search_func, search_data_func, \
+cx_tree_node_base_layout)
+
+/**
+ * Creates a new tree structure based on an existing tree.
+ *
+ * The specified \p allocator will be used for creating the tree struct.
+ *
+ * \attention This function will create an incompletely defined tree structure
+ * where neither the create function, the search function, nor a destructor
+ * will be set. If you wish to use any of this functionality for the wrapped
+ * tree, you need to specify those functions afterwards.
+ *
+ * @param allocator the allocator that was used for nodes of the wrapped tree
+ * @param root the root node of the tree that shall be wrapped
+ * @param loc_parent offset in the node struct for the parent pointer
+ * @param loc_children offset in the node struct for the children linked list
+ * @param loc_last_child optional offset in the node struct for the pointer to
+ * the last child in the linked list (negative if there is no such pointer)
+ * @param loc_prev optional offset in the node struct for the prev pointer
+ * @param loc_next offset in the node struct for the next pointer
+ * @return the new tree
+ * @see cxTreeCreate()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+cx_attr_malloc
+cx_attr_dealloc(cxTreeDestroy, 1)
+CxTree *cxTreeCreateWrapped(
+        const CxAllocator *allocator,
+        void *root,
+        ptrdiff_t loc_parent,
+        ptrdiff_t loc_children,
+        ptrdiff_t loc_last_child,
+        ptrdiff_t loc_prev,
+        ptrdiff_t loc_next
+);
+
+/**
+ * Inserts data into the tree.
+ *
+ * \remark For this function to work, the tree needs specified search and
+ * create functions, which might not be available for wrapped trees
+ * (see #cxTreeCreateWrapped()).
+ *
+ * @param tree the tree
+ * @param data the data to insert
+ * @return zero on success, non-zero on failure
+ */
+cx_attr_nonnull
+static inline int cxTreeInsert(
+        CxTree *tree,
+        const void *data
+) {
+    return tree->cl->insert_element(tree, data);
+}
+
+/**
+ * Inserts elements provided by an iterator efficiently into the tree.
+ *
+ * \remark For this function to work, the tree needs specified search and
+ * create functions, which might not be available for wrapped trees
+ * (see #cxTreeCreateWrapped()).
+ *
+ * @param tree the tree
+ * @param iter the iterator providing the elements
+ * @param n the maximum number of elements to insert
+ * @return the number of elements that could be successfully inserted
+ */
+cx_attr_nonnull
+static inline size_t cxTreeInsertIter(
+        CxTree *tree,
+        struct cx_iterator_base_s *iter,
+        size_t n
+) {
+    return tree->cl->insert_many(tree, iter, n);
+}
+
+/**
+ * Inserts an array of data efficiently into the tree.
+ *
+ * \remark For this function to work, the tree needs specified search and
+ * create functions, which might not be available for wrapped trees
+ * (see #cxTreeCreateWrapped()).
+ *
+ * @param tree the tree
+ * @param data the array of data to insert
+ * @param elem_size the size of each element in the array
+ * @param n the number of elements in the array
+ * @return the number of elements that could be successfully inserted
+ */
+cx_attr_nonnull
+static inline size_t cxTreeInsertArray(
+        CxTree *tree,
+        const void *data,
+        size_t elem_size,
+        size_t n
+) {
+    if (n == 0) return 0;
+    if (n == 1) return 0 == cxTreeInsert(tree, data) ? 1 : 0;
+    CxIterator iter = cxIterator(data, elem_size, n);
+    return cxTreeInsertIter(tree, cxIteratorRef(iter), n);
+}
+
+/**
+ * Searches the data in the specified tree.
+ *
+ * \remark For this function to work, the tree needs a specified \c search_data
+ * function, which might not be available wrapped trees
+ * (see #cxTreeCreateWrapped()).
+ *
+ * @param tree the tree
+ * @param data the data to search for
+ * @return the first matching node, or \c NULL when the data cannot be found
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline void *cxTreeFind(
+        CxTree *tree,
+        const void *data
+) {
+    return tree->cl->find(tree, tree->root, data, 0);
+}
+
+/**
+ * Searches the data in the specified subtree.
+ *
+ * When \p max_depth is zero, the depth is not limited.
+ * The \p subtree_root itself is on depth 1 and its children have depth 2.
+ *
+ * \note When \p subtree_root is not part of the \p tree, the behavior is
+ * undefined.
+ *
+ * \remark For this function to work, the tree needs a specified \c search_data
+ * function, which might not be the case for wrapped trees
+ * (see #cxTreeCreateWrapped()).
+ *
+ * @param tree the tree
+ * @param data the data to search for
+ * @param subtree_root the node where to start
+ * @param max_depth the maximum search depth
+ * @return the first matching node, or \c NULL when the data cannot be found
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline void *cxTreeFindInSubtree(
+        CxTree *tree,
+        const void *data,
+        void *subtree_root,
+        size_t max_depth
+) {
+    return tree->cl->find(tree, subtree_root, data, max_depth);
+}
+
+/**
+ * Determines the size of the specified subtree.
+ *
+ * @param tree the tree
+ * @param subtree_root the root node of the subtree
+ * @return the number of nodes in the specified subtree
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+size_t cxTreeSubtreeSize(CxTree *tree, void *subtree_root);
+
+/**
+ * Determines the depth of the specified subtree.
+ *
+ * @param tree the tree
+ * @param subtree_root the root node of the subtree
+ * @return the tree depth including the \p subtree_root
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+size_t cxTreeSubtreeDepth(CxTree *tree, void *subtree_root);
+
+/**
+ * Determines the depth of the entire tree.
+ *
+ * @param tree the tree
+ * @return the tree depth, counting the root as one
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+size_t cxTreeDepth(CxTree *tree);
+
+/**
+ * Creates a depth-first iterator for the specified tree starting in \p node.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * @param tree the tree to iterate
+ * @param node the node where to start
+ * @param visit_on_exit true, if the iterator shall visit a node again when
+ * leaving the subtree
+ * @return a tree iterator (depth-first)
+ * @see cxTreeVisit()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeIterator cxTreeIterateSubtree(
+        CxTree *tree,
+        void *node,
+        bool visit_on_exit
+) {
+    return cx_tree_iterator(
+            node, visit_on_exit,
+            tree->loc_children, tree->loc_next
+    );
+}
+
+/**
+ * Creates a breadth-first iterator for the specified tree starting in \p node.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * @param tree the tree to iterate
+ * @param node the node where to start
+ * @return a tree visitor (a.k.a. breadth-first iterator)
+ * @see cxTreeIterate()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeVisitor cxTreeVisitSubtree(CxTree *tree, void *node) {
+    return cx_tree_visitor(
+            node, tree->loc_children, tree->loc_next
+    );
+}
+
+/**
+ * Creates a depth-first iterator for the specified tree.
+ *
+ * @param tree the tree to iterate
+ * @param visit_on_exit true, if the iterator shall visit a node again when
+ * leaving the subtree
+ * @return a tree iterator (depth-first)
+ * @see cxTreeVisit()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeIterator cxTreeIterate(
+        CxTree *tree,
+        bool visit_on_exit
+) {
+    return cxTreeIterateSubtree(tree, tree->root, visit_on_exit);
+}
+
+/**
+ * Creates a breadth-first iterator for the specified tree.
+ *
+ * @param tree the tree to iterate
+ * @return a tree visitor (a.k.a. breadth-first iterator)
+ * @see cxTreeIterate()
+ */
+cx_attr_nonnull
+cx_attr_nodiscard
+static inline CxTreeVisitor cxTreeVisit(CxTree *tree) {
+    return cxTreeVisitSubtree(tree, tree->root);
+}
+
+/**
+ * Sets the (new) parent of the specified child.
+ *
+ * If the \p child is not already member of the tree, this function behaves
+ * as #cxTreeAddChildNode().
+ *
+ * @param tree the tree
+ * @param parent the (new) parent of the child
+ * @param child the node to add
+ * @see cxTreeAddChildNode()
+ */
+cx_attr_nonnull
+void cxTreeSetParent(
+        CxTree *tree,
+        void *parent,
+        void *child
+);
+
+/**
+ * Adds a new node to the tree.
+ *
+ * If the \p child is already member of the tree, the behavior is undefined.
+ * Use #cxTreeSetParent() if you want to move a subtree to another location.
+ *
+ * \attention The node may be externally created, but MUST obey the same rules
+ * as if it was created by the tree itself with #cxTreeAddChild() (e.g. use
+ * the same allocator).
+ *
+ * @param tree the tree
+ * @param parent the parent of the node to add
+ * @param child the node to add
+ * @see cxTreeSetParent()
+ */
+cx_attr_nonnull
+void cxTreeAddChildNode(
+        CxTree *tree,
+        void *parent,
+        void *child
+);
+
+/**
+ * Creates a new node and adds it to the tree.
+ *
+ * With this function you can decide where exactly the new node shall be added.
+ * If you specified an appropriate search function, you may want to consider
+ * leaving this task to the tree by using #cxTreeInsert().
+ *
+ * Be aware that adding nodes at arbitrary locations in the tree might cause
+ * wrong or undesired results when subsequently invoking #cxTreeInsert() and
+ * the invariant imposed by the search function does not hold any longer.
+ *
+ * @param tree the tree
+ * @param parent the parent node of the new node
+ * @param data the data that will be submitted to the create function
+ * @return zero when the new node was created, non-zero on allocation failure
+ * @see cxTreeInsert()
+ */
+cx_attr_nonnull
+int cxTreeAddChild(
+        CxTree *tree,
+        void *parent,
+        const void *data
+);
+
+/**
+ * A function that is invoked when a node needs to be re-linked to a new parent.
+ *
+ * When a node is re-linked, sometimes the contents need to be updated.
+ * This callback is invoked by #cxTreeRemoveNode() and #cxTreeDestroyNode()
+ * so that those updates can be applied when re-linking the children of the
+ * removed node.
+ *
+ * @param node the affected node
+ * @param old_parent the old parent of the node
+ * @param new_parent the new parent of the node
+ */
+cx_attr_nonnull
+typedef void (*cx_tree_relink_func)(
+        void *node,
+        const void *old_parent,
+        const void *new_parent
+);
+
+/**
+ * Removes a node and re-links its children to its former parent.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * \note The destructor function, if any, will \em not be invoked. That means
+ * you will need to free the removed node by yourself, eventually.
+ *
+ * @param tree the tree
+ * @param node the node to remove (must not be the root node)
+ * @param relink_func optional callback to update the content of each re-linked
+ * node
+ * @return zero on success, non-zero if \p node is the root node of the tree
+ */
+cx_attr_nonnull_arg(1, 2)
+int cxTreeRemoveNode(
+        CxTree *tree,
+        void *node,
+        cx_tree_relink_func relink_func
+);
+
+/**
+ * Removes a node and it's subtree from the tree.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * \note The destructor function, if any, will \em not be invoked. That means
+ * you will need to free the removed subtree by yourself, eventually.
+ *
+ * @param tree the tree
+ * @param node the node to remove
+ */
+cx_attr_nonnull
+void cxTreeRemoveSubtree(CxTree *tree, void *node);
+
+/**
+ * Destroys a node and re-links its children to its former parent.
+ *
+ * If the node is not part of the tree, the behavior is undefined.
+ *
+ * It is guaranteed that the simple destructor is invoked before
+ * the advanced destructor.
+ *
+ * \attention This function will not free the memory of the node with the
+ * tree's allocator, because that is usually done by the advanced destructor
+ * and would therefore result in a double-free.
+ *
+ * @param tree the tree
+ * @param node the node to destroy (must not be the root node)
+ * @param relink_func optional callback to update the content of each re-linked
+ * node
+ * @return zero on success, non-zero if \p node is the root node of the tree
+ */
+cx_attr_nonnull_arg(1, 2)
+int cxTreeDestroyNode(
+        CxTree *tree,
+        void *node,
+        cx_tree_relink_func relink_func
+);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
--- a/src/list.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/list.c	Thu Nov 07 22:46:58 2024 +0100
@@ -212,33 +212,33 @@
 
 // <editor-fold desc="empty list implementation">
 
-static void cx_emptyl_noop(__attribute__((__unused__)) CxList *list) {
+static void cx_emptyl_noop(cx_attr_unused CxList *list) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_emptyl_at(
-        __attribute__((__unused__)) const struct cx_list_s *list,
-        __attribute__((__unused__)) size_t index
+        cx_attr_unused const struct cx_list_s *list,
+        cx_attr_unused size_t index
 ) {
     return NULL;
 }
 
 static ssize_t cx_emptyl_find_remove(
-        __attribute__((__unused__)) struct cx_list_s *list,
-        __attribute__((__unused__)) const void *elem,
-        __attribute__((__unused__)) bool remove
+        cx_attr_unused struct cx_list_s *list,
+        cx_attr_unused const void *elem,
+        cx_attr_unused bool remove
 ) {
     return -1;
 }
 
-static bool cx_emptyl_iter_valid(__attribute__((__unused__)) const void *iter) {
+static bool cx_emptyl_iter_valid(cx_attr_unused const void *iter) {
     return false;
 }
 
 static CxIterator cx_emptyl_iterator(
         const struct cx_list_s *list,
         size_t index,
-        __attribute__((__unused__)) bool backwards
+        cx_attr_unused bool backwards
 ) {
     CxIterator iter = {0};
     iter.src_handle.c = list;
@@ -419,10 +419,6 @@
     return 0;
 }
 
-void cxListDestroy(CxList *list) {
-    list->cl->destructor(list);
-}
-
 int cxListCompare(
         const CxList *list,
         const CxList *other
--- a/src/map.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/map.c	Thu Nov 07 22:46:58 2024 +0100
@@ -31,24 +31,24 @@
 
 // <editor-fold desc="empty map implementation">
 
-static void cx_empty_map_noop(__attribute__((__unused__)) CxMap *map) {
+static void cx_empty_map_noop(cx_attr_unused CxMap *map) {
     // this is a noop, but MUST be implemented
 }
 
 static void *cx_empty_map_get(
-        __attribute__((__unused__)) const CxMap *map,
-        __attribute__((__unused__)) CxHashKey key
+        cx_attr_unused const CxMap *map,
+        cx_attr_unused CxHashKey key
 ) {
     return NULL;
 }
 
-static bool cx_empty_map_iter_valid(__attribute__((__unused__)) const void *iter) {
+static bool cx_empty_map_iter_valid(cx_attr_unused const void *iter) {
     return false;
 }
 
 static CxIterator cx_empty_map_iterator(
         const struct cx_map_s *map,
-        __attribute__((__unused__)) enum cx_map_iterator_type type
+        cx_attr_unused enum cx_map_iterator_type type
 ) {
     CxIterator iter = {0};
     iter.src_handle.c = map;
--- a/src/mempool.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/mempool.c	Thu Nov 07 22:46:58 2024 +0100
@@ -138,6 +138,7 @@
 }
 
 void cxMempoolDestroy(CxMempool *pool) {
+    if (pool == NULL) return;
     struct cx_mempool_memory_s *mem;
     for (size_t i = 0; i < pool->size; i++) {
         mem = pool->data[i];
@@ -158,6 +159,10 @@
     *(cx_destructor_func *) ((char *) ptr - sizeof(cx_destructor_func)) = func;
 }
 
+void cxMempoolRemoveDestructor(void *ptr) {
+    *(cx_destructor_func *) ((char *) ptr - sizeof(cx_destructor_func)) = NULL;
+}
+
 struct cx_mempool_foreign_mem_s {
     cx_destructor_func destr;
     void* mem;
--- a/src/properties.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/properties.c	Thu Nov 07 22:46:58 2024 +0100
@@ -284,7 +284,7 @@
 }
 
 static int cx_properties_sink_map(
-        __attribute__((__unused__)) CxProperties *prop,
+        cx_attr_unused CxProperties *prop,
         CxPropertiesSink *sink,
         cxstring key,
         cxstring value
@@ -322,7 +322,7 @@
 }
 
 static int cx_properties_read_file(
-        __attribute__((__unused__)) CxProperties *prop,
+        cx_attr_unused CxProperties *prop,
         CxPropertiesSource *src,
         cxstring *target
 ) {
@@ -332,7 +332,7 @@
 }
 
 static int cx_properties_read_init_file(
-        __attribute__((__unused__)) CxProperties *prop,
+        cx_attr_unused CxProperties *prop,
         CxPropertiesSource *src
 ) {
     src->data_ptr = malloc(src->data_size);
@@ -341,7 +341,7 @@
 }
 
 static void cx_properties_read_clean_file(
-        __attribute__((__unused__)) CxProperties *prop,
+        cx_attr_unused CxProperties *prop,
         CxPropertiesSource *src
 ) {
     free(src->data_ptr);
--- a/src/string.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/src/string.c	Thu Nov 07 22:46:58 2024 +0100
@@ -65,6 +65,7 @@
 }
 
 void cx_strfree(cxmutstr *str) {
+    if (str == NULL) return;
     free(str->ptr);
     str->ptr = NULL;
     str->length = 0;
@@ -74,6 +75,7 @@
         const CxAllocator *alloc,
         cxmutstr *str
 ) {
+    if (str == NULL) return;
     cxFree(alloc, str->ptr);
     str->ptr = NULL;
     str->length = 0;
--- a/tests/test_allocator.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_allocator.c	Thu Nov 07 22:46:58 2024 +0100
@@ -98,7 +98,7 @@
 }
 
 CX_TEST(test_allocator_default_free) {
-    void *test = malloc(16);
+    void *test = cxMalloc(cxDefaultAllocator, 16);
     CX_TEST_DO {
         // we cannot assert sth. but valgrind will detect an error
         cxFree(cxDefaultAllocator, test);
@@ -181,9 +181,9 @@
 }
 
 static void *test_allocator_mock_failing_realloc(
-        __attribute__((__unused__))void *p,
-        __attribute__((__unused__))void *d,
-        __attribute__((__unused__))size_t n
+        cx_attr_unused void *p,
+        cx_attr_unused void *d,
+        cx_attr_unused size_t n
 ) {
     return NULL;
 }
--- a/tests/test_buffer.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_buffer.c	Thu Nov 07 22:46:58 2024 +0100
@@ -588,7 +588,7 @@
 static size_t mock_write_limited_rate(
         const void *ptr,
         size_t size,
-        __attribute__((unused)) size_t nitems,
+        cx_attr_unused size_t nitems,
         CxBuffer *buffer
 ) {
     // simulate limited target drain capacity
--- a/tests/test_hash_map.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_hash_map.c	Thu Nov 07 22:46:58 2024 +0100
@@ -265,7 +265,7 @@
 }
 
 static void test_advanced_destructor(
-        __attribute__((__unused__)) void *unused,
+        cx_attr_unused void *unused,
         void *data
 ) {
     strcpy(data, "OK");
--- a/tests/test_list.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_list.c	Thu Nov 07 22:46:58 2024 +0100
@@ -1165,7 +1165,7 @@
 }
 #define roll_out_test_combos(name, body) \
 static CX_TEST_SUBROUTINE(test_list_verify_##name, CxList *list, \
-    __attribute__((__unused__)) bool isptrlist) body \
+    cx_attr_unused bool isptrlist) body \
 roll_out_test_invokers(name)
 
 static void set_default_class_funcs(CxList *list, cx_list_class *defaulted_cl) {
@@ -1188,7 +1188,7 @@
     set_default_class_funcs(list, &defaulted_cl)
 #define roll_out_test_combos_with_defaulted_funcs(name, body) \
 static CX_TEST_SUBROUTINE(test_list_verify_##name, CxList *list, \
-    __attribute__((__unused__)) bool isptrlist) body \
+    cx_attr_unused bool isptrlist) body \
 roll_out_test_invokers(name) \
 CX_TEST(test_list_llm_##name) { \
     set_up_combo \
@@ -1387,7 +1387,7 @@
 })
 
 static unsigned test_remove_array_destr_ctr;
-static void test_remove_array_destr(__attribute__((__unused__)) void *d) {
+static void test_remove_array_destr(cx_attr_unused void *d) {
     test_remove_array_destr_ctr++;
 }
 
@@ -1767,7 +1767,7 @@
     destr_test_ctr++;
 }
 
-static void advanced_destr_test_fun(__attribute__((__unused__)) void *u, void *data) {
+static void advanced_destr_test_fun(cx_attr_unused void *u, void *data) {
     simple_destr_test_fun(data);
 }
 
--- a/tests/test_mempool.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_mempool.c	Thu Nov 07 22:46:58 2024 +0100
@@ -81,7 +81,7 @@
 
 static unsigned test_mempool_destructor_called;
 
-static void test_mempool_destructor(__attribute__((__unused__)) void *mem) {
+static void test_mempool_destructor(cx_attr_unused void *mem) {
     test_mempool_destructor_called++;
 }
 
@@ -119,10 +119,10 @@
         cxFree(pool->allocator, mem1);
         CX_TEST_ASSERT(pool->size == 0);
 
-        cxMalloc(pool->allocator, 16);
-        cxMalloc(pool->allocator, 16);
+        mem1 = cxMalloc(pool->allocator, 16);
         mem1 = cxMalloc(pool->allocator, 16);
-        cxMalloc(pool->allocator, 16);
+        mem1 = cxMalloc(pool->allocator, 16);
+        mem2 = cxMalloc(pool->allocator, 16);
         mem2 = cxMalloc(pool->allocator, 16);
 
         CX_TEST_ASSERT(pool->size == 5);
--- a/tests/test_tree.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/test_tree.c	Thu Nov 07 22:46:58 2024 +0100
@@ -1957,8 +1957,8 @@
 
 static void test_tree_remove_node_relink_mock(
         void *node,
-        __attribute__((__unused__)) const void *oldp,
-        __attribute__((__unused__)) const void *newp
+        cx_attr_unused const void *oldp,
+        cx_attr_unused const void *newp
 ) {
     tree_node_file * n = node;
     // this function fakes the relink logic in below test
@@ -2175,7 +2175,7 @@
         w->destructor_data = alloc;
         cxTreeDestroySubtree(w, w->root);
         CX_TEST_ASSERT(!cx_testing_allocator_verify(&talloc));
-        cxFree(alloc, w);
+        cxTreeDestroy(w);
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
     }
     cx_testing_allocator_destroy(&talloc);
--- a/tests/util_allocator.c	Thu Nov 07 20:22:56 2024 +0100
+++ b/tests/util_allocator.c	Thu Nov 07 22:46:58 2024 +0100
@@ -29,6 +29,11 @@
 #include "util_allocator.h"
 #include "cx/test.h"
 
+#if !defined(__clang__) && __GNUC__ > 11
+// this utility is explicitly designed to track UAF
+#pragma GCC diagnostic ignored "-Wuse-after-free"
+#endif
+
 static void cx_testing_allocator_track(CxTestingAllocator *alloc, void *ptr) {
     for (size_t i = 0; i < alloc->tracked_count; i++) {
         if (alloc->tracked[i] == ptr) return; // is already tracked
@@ -83,16 +88,9 @@
             data->alloc_failed++;
         } else {
             data->free_total++;
-#if !defined(__clang__) && __GNUC__ > 11
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wuse-after-free"
-#endif
             if (!cx_testing_allocator_untrack(data, mem)) {
                 data->free_failed++;
             }
-#if !defined(__clang__) && __GNUC__ > 11
-#pragma GCC diagnostic pop
-#endif
             cx_testing_allocator_track(data, ptr);
         }
         return ptr;

mercurial