- Notifications
You must be signed in to change notification settings - Fork1
C++ style smart pointers for C23
License
josugoar/csp
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
©️ 🧠 👉 C++ style smart pointers for C23
CSP (C Smart Pointers) is a C++ inspired smart pointer library for C23 (although it can be backported to C11 but wanted to check out the new features) specifically inspired byBoost.SmartPtr and C++20 smart pointers from thememory header and the correspondinglibc++ implementation. It makes extensive use of modern C23 features such asnullptr
,constexpr
, attributes, unnamed arguments andauto
type inference, empty initializers, as well as C11 atomics and threads, so a C23 compiler is required to build it. At the time of writing, only GCC is able to be used due to the lack of support of Clang, verify C23 support atcppreference.
Strict ownership semantics up to what the C language allows. This is to say that ownership modifying operations must be explicitly stated in code and force the programmer to make the right decisions. Thanks to the "pass by value" nature of csp smart pointers, it is not possible (in a conventional fashion) to mix up types when passing pointers around.
RAII (Resource Acquisition Is Initialization), thanks to the use of GCC's cleanup attribute. This is mainly for convenience and to avoid having to call each object's "destructor" (or equivalent in C) manually. Inspired by its use in thesystemd codebase. Avoid unless specifically usingGNUC-std=gnu* instead of plain C. Also, having defined the API, it is possible to use a custom static analyzer to check whether smart pointer destructors are called, which would "entirely" solve the memory allocation problem while defining strict ownership semantics.
Asimilar interface to C++ smart pointers, with the exception of having to acquire the pointer from the smart pointer using the
get
function to manipulate it. This is due to the lack of operator overloading in C. Aside from that, it follows the standard "object oriented" interface commonly used in C libraries that is similar to theinit
anddestroy
functions originally present in posix, the C standard library and others, except that constructors return a copy due to the inexpensive nature of smart pointers (this allows for avery succinct calling style).Thread safe reference counting, using C11 atomics and threads. Non local shared pointers make use of atomic operations to ensure thread safety and employ some of the optimizations present in libc++, although not extensive.
Atomic specializations of shared pointers. Shared pointers themselves are not atomic by default, but their reference counters are, which can cause data races if accessed concurrently. Atomic shared pointers are useful for when the shared pointer itself needs to be accessed concurrently, but they are implemented using mutexes, which are slower than intrinsic atomic operations, due to the (very heavily) increased complexity of pure atomics. They even have modern wait/notify support!
Generic pointers thanks to the use of
void *
. This allows the use of any type of pointer for storage, not including function pointers (technically undefined behavior because of no guarantees of both sharing the same size). This approach, however, limits type safety, and while it is possible to use macro and_Generic
magic to dispatch to the correct function depending on the number and type of the arguments at compile type, or generate the entire code from macros or use gnuc macro extensions, it gets messy extremely quickly andvoid *
is usually the preferred solution, even by the standard.Boost.SmartPtrextended smart pointers in the form of pointer types not present in the C++ standard library. They don't matter a whole lot in C++ because of weird class member packing optimizations with templates, but since C cannot do such things, they are useful to have as little (to practically none) overhead as possible.- This would require splitting the API into multiple similarly flavoured pointer types (eg. local, nonlocal) and would kill generalizability.
Implements commonoptimizations found in libc++ and Boost.SmartPtr, as well as the usual single allocations for the control block and the object itself using
csp_make_*
andcsp_allocate_*
type functions.Error handling is achieved through out error pointers to preserve return values and allow for a very terse syntax which allows for straight forward ownership semantics. Similar toGLib error reporting approach without theabsolutely crazy decision of aborting the program on insufficient memory.
Custom allocation support. This is very easy to implement but would require and additional overhead of at least one function pointer in each control block of shared pointers, since it is not possible to explicitelly doEBO (Empty Base class Optimization) and it would require to add the corresponding allocation and deallocation functions inside it. CSP smart pointers already have an overhead of one function pointer when using the default deleter (free), which is minuscule in comparison to other "generic" solutions that always require memory allocation.- Since the overhead is negligible while the benefits are immense, like being able to use smart pointers on some freestanding libc implementations that don't have malloc (even though now it seem to be required anyway), it is now implemented.
C++20 wait/notify atomic functions. They are not trivial to implement and are hard to test, so they are not a priority at the moment.- Wait/notify is supported but the implementation does not use condition variables because of the overhead, instead, it uses a thread polling with timed backoff algorithm, so it's not atomic unfortunately.
Lock-free atomic pointer specializations. Harder to implement. SeeInside STL: The atomic shared_ptr.
// [Optional] Ensure that the cleanup attribute is present// as already explained#if defined__has_attribute#if__has_attribute(cleanup)#defineHAS_CLEANUP_ATTRIBUTE#endif#endif#ifdefHAS_CLEANUP_ATTRIBUTE#definecsp_unique_ptr csp_unique_ptr_cleanup#definecsp_shared_ptr csp_shared_ptr_cleanup#definecsp_weak_ptr csp_weak_ptr_cleanup#endif// You can include the entire library with the csp header#include<csp/csp.h>// Or include only the specific smart pointer you want#include<csp/unique_ptr>#include<csp/shared_ptr>// Smart pointers are passed by value (like normal pointers)voidunique_pointer_consumer(csp_unique_ptr);voidshared_pointer_consumer(csp_shared_ptr);structfoo {intfoo;intbar;intbaz;int*allocated_memory;};voidfoo_destroy(structfoo*constp){free(p->allocated_memory);free(p);}// When construction the smart pointers notice how const// correctness can still be respected without having to// jump through hoopsintmain(void){// CSP returns an error code if something goes wrongcsp_exceptione;// Create a unique pointer to a struct foo with a custom// deleter that is able to free the internal memory managed// by the pointer itselfcsp_unique_ptru=csp_unique_ptr_init_pd(malloc(sizeof(structfoo)),foo_destroy,&e);// Recommended to check for errorsif (e!=CSP_SUCCESS) {// Handle error }// Initialize allocated memory to nothing to not crash the// program. Pointer can be initialized now or before making// it smartstructfoo*foo=csp_unique_ptr_get(&u);foo->allocated_memory=nullptr;// Ownership is fully delegated to the consumer// when moving a unique pointerunique_pointer_consumer(csp_unique_ptr_init_move_u(&u));// Cleanup pointers don't need to be manually destroyed, but should// be avoided unless using gnuc extensions// Only one allocation is made (optimization)csp_shared_ptrr=csp_make_shared_for_overwrite(sizeof(int),&e);if (e!=CSP_SUCCESS) {// Handle error }// Now it is even possible to initialize pointer directly, which is much more ergonomicintmember=10; autoother=csp_make_shared(sizeof(int),&member,&e);if (e!=CSP_SUCCESS) {// Handle error }// "Constructors" type functions return a copy (inexpensive) of the// created shared pointer that stores a pointer to the internally// allocated memory// Access the pointer using the get function// You can manipulate the value this wayint*constp=csp_shared_ptr_get(&r);// <-- CAN'T use type inference here// Access the deleter that will be used to destroy the objectconst autod=csp_shared_ptr_get_deleter(&r);// Share ownership of the pointer with another functionshared_pointer_consumer(csp_shared_ptr_init_copy_s(&r));// Let go of your ownership and pass it to the consumershared_pointer_consumer(csp_shared_ptr_init_move_s(&r));// Remember, this check is not necessary, it is here for illustrative purposes only#ifndefHAS_CLEANUP_ATTRIBUTEcsp_shared_ptr_destroy(&other);#endif// This will decrement the internal reference counter and destroy the// object if it reaches 0. A cleanup pointer does not need to call it.#ifndefHAS_CLEANUP_ATTRIBUTEcsp_shared_ptr_destroy(&r);#endif// Now destroy will not do anything because u does not own the pointer// Again, recommended to destroy the pointer manually, there is no way// around it in standard C without using extremely costly abstractions// (glib) or macro magic, which is arguably worse than anything else#ifndefHAS_CLEANUP_ATTRIBUTEcsp_unique_ptr_destroy(&u);#endifreturn0;}
# Clone the repositorygit clone https://github.com/josugoar/csp.git# Enter the repositorycd csp# Build Doxygen documentationdoxygen Doxyfile# Create a build directorymkdir build# Enter the build directorycd build# Configure the buildcmake ..# Build the librarycmake --build.# Run the testsctest# Install the librarycmake --install.# Or run CPack to generate the installers# for .tar.gz, .sh or .tar.z generatorscpack -C CPackConfig.cmake
The brief documentation is "adapted" (copied) fromcppreference.
Some of the reference counting code was inspired mostly bylibc++ and alsoMSVC andlibstdc++. It was interesting seeing the different but similar approaches to the same problem.
About
C++ style smart pointers for C23