|
C Container Collection (CCC)
|
Follow the links to the C Container Collection headers.
Traditionally, dynamic memory is obtained, resized, and freed through an OS or language provided interface such as malloc, realloc, and free; these three functions manage a global allocator. However, these three core functionalities of an allocator can be united into one function. This is the approach that the C Container Collection takes. This is the implementation of the standard library allocator through the CCC_Allocator_interface type function.
Then, containers that allocate accept a reference to a CCC_Allocator which is a simple bundle for the CCC_Allocator_interface and context.
The ability to pass context along with a CCC_Allocator, and have that same context provided as an argument to the allocator function call, is significant. This opens up the possibility for non-global allocators, or allocators with context that is not available until runtime. Consider the stack allocator that the CCC test suite uses to test container code paths that use an allocator, but avoid the overhead and cleanup that comes with the standard library allocator.
It is basically a bump allocator that never frees. It requires context because it only exists within the stack frame of a function.
Full knowledge of the stack allocator is not required to understand the gist of this code block. There is an allocation function and a structure that manages the requested block of types the user plans to use. Context is a powerful concept for allocators that can provide fine-grained control and flexibility.
Passing allocators at call sites also ensures that the reader knows where in the code memory allocation may occur. Different types of allocators with various levels of debugging or safety features can also be swapped in at runtime or compile time easily. Finally, no boiler plate is required if the user plans on using only fixed size containers or managing their own memory.
An empty &(CCC_Allocator){} can be passed to any function and means the following.
When reading user written C Container Collection code, the presence of NULL at a function or macro call site indicates a programmer error. Consider the following function call inserting an element into a flat hash map.
The call site of this function helps the reader understand the operation of this code without any special knowledge of the CCC API: insert or assign this key value pair into the map and if the table needs to re-size use the provided allocator. What if the user has a fixed size map, or has otherwise reserved all the space they need, and wants to ensure no further memory is used? The user might be tempted to write the following.
WARNING! The following example shows incorrect usage of the CCC API.
The user is trying to express that they don't want this function to allocate if the table is full. They want to keep the return value so they can check if this insertion failure has taken place and proceed with their program. However, using NULL is a programmer error and will return an error status because it obscures intent and would force a reader unfamiliar with the collection to consult documentation. Instead use this construction.
This tells the reader that a key value pair will be inserted or assigned to an existing slot in the table and that resizing of the table is forbidden because a default initialized, empty CCC_Allocator is passed to the function. This is also a common pattern to use when clearing a container of its allocations and no advanced destructor functionality is required to act on each element before they are all freed.
No destructor has been implemented, but we follow the no passing NULL rule. An empty compound literal reference is the correct way to express that no user provided implementation of that argument exists. The API will respond accordingly. See documentation for details.
When passing an empty allocator some details must be remembered. One difference between Flat_ and Array_ based containers and the standard intrusive containers is that the former copy user provided types into a table-like structure. When an empty allocator is passed to these flat and array based containers resizing of the underlying table is forbidden. For intrusive containers the contract is different.
Consider this insertion into an intrusive priority queue.
Here, the user type is passed to the function as an inline compound literal reference. This is OK because an allocator is provided. Internally, the container will try to allocate a new node for the priority queue with the provided allocator and copy in the provided values in the user provided type argument. This would not work if allocation is forbidden.
WARNING! The following example shows possible incorrect usage of the CCC API.
Because allocation has been forbidden, the container does not assume anything about the scope and lifetime of the provided user element. It simply operates on the intrusive element the user is providing to maintain internal data structure invariants. Because an inline compound literal reference has been provided the scope and lifetime of that element is the enclosing block, and that is almost always a programmer error. The user must guarantee the lifetime of the element.
This required attention to scope and lifetime applies identically to all intrusive containers.
Building upon the last rule, that NULL should never appear at a function or macro call site, the user should apply this rule to initializers. Every container offers a variety of initializers. If the container will be initialized as empty use the _default() initializer. Compare the following wrong choice with a subsequent correct choice.
WARNING! The following example shows incorrect usage of the CCC API.
This misuse breaks rule 2, and it is difficult to understand what the two zeros and NULL mean just from reading the code. Use the correct initializer.
For the Flat_ and Array_ based containers, consider using the _with_storage() initializers for expressive compile time initialization of fixed size containers.
The _default() initializers can be used at compile or runtime as well. The correct initializer will always present the more readable and expressive code. More complex initializers such as the _for() family of initializers are in place to support well-documented but rare corner cases of user memory management strategies. Always read the container initialization section of the container documentation to pick the right initializer for the job.
There are three important concepts about references the user must understand in the C Container Collection: pointers that may be invalidated, pointers that are stable, and handles that are stable. Here is a table summarizes the container types the user will encounter in this collection.
| Container | Memory Layout | Pointer Stable | Handle Stable |
|---|---|---|---|
| Intrusive | Non-Contiguous | Yes | N/A |
| Flat | Contiguous | No | N/A |
| Array | Contiguous | No | Yes |
Pointer invalidation occurs only when a container is permitted to allocate. When a flat container is passed an allocator for an insert or push type operation, assume that all references are invalidated after that operation. For a simple dynamic buffer this may be obvious.
After the push operation, the reference *front may be invalid if the buffer has resized to fit the new element. This is also true for more complex containers such as the flat hash map.
After the insert or assign operation, the reference *a may have been invalidated.
Intrusive containers assume that the user defined types upon which they intrude are pointer stable. That means that when other elements are inserted or removed, the location in memory of an existing element in the container never changes. Consider an intrusive map.
The map assumes the pointer *a is still valid after the insertion of *b. The container assumption is the same whether an allocator has been provided or the user is managing memory and has passed an empty &(CCC_Allocator){}.
The special Array_ based containers promise handle stability. This is similar to pointer stability, but provides slightly greater flexibility to the container implementation. A CCC_Handle and its unwrapped CCC_Handle_index remain stable for the lifetime of the element in the container which they represent.
After the second element is inserted, a remains valid and can be provided to the container API to obtain a pointer to the user element. The underlying storage may have been resized or moved, but the location in the array remains stable. Therefore, it is important to remember that only the handle index remains stable, never the pointer obtained with that index.
If you are looking to contribute, tests that increase coverage are a great start. View the coverage report here.
The report is generating by running the test suite. See the tests folder for more.