diff --git a/README.md b/README.md index e24ef8d..ae6b90b 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,35 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bkthomps/Containers/blob/master/LICENSE) # Containers -This library provides various containers. Each container has utility functions to manipulate the data it holds. This is an abstraction as to not have to manually manage and reallocate memory. +This library provides various containers. Each container has utility functions +to manipulate the data it holds. This is an abstraction as to not have to +manually manage and reallocate memory. -Inspired by the C++ standard library; however, implemented using C with different function interfaces as the C++ standard library but with the same container names. +Inspired by the C++ standard library; however, implemented using C with +different function interfaces as the C++ standard library but with the same +container names. ## Setup It is possible to compile this library as either static `.a` or dynamic `.so`: -1. A static library is slightly faster than a dynamic one, however, if the library is modified, the entire project codebase which uses it will need to be recompiled. -2. A dynamic library can be changed without recompiling the codebase, assuming no function definitions have changed. +1. A static library is slightly faster than a dynamic one, however, if the +library is modified, the entire project codebase which uses it will need to be +recompiled. +2. A dynamic library can be changed without recompiling the codebase, assuming +no function definitions have changed. The installation process is as follows: 1. Clone this repository and navigate to it. -2. Run `make static_clang`/`make static_gcc` or `make dynamic_clang`/`make dynamic_gcc` for either a static or dynamic library. -3. Then, you can copy-paste `containers.h` and `containers.a`/`containers.so` into your project to include the containers. -4. Finally, you remember to link the library by including `containers.a -ldl`/`containers.so -ldl` as an argument. +2. Run `make static_clang`/`make static_gcc` or +`make dynamic_clang`/`make dynamic_gcc` for either a static or dynamic library. +3. Then, you can copy-paste `containers.h` and `containers.a`/`containers.so` +into your project to include the containers. +4. Finally, you remember to link the library by including +`containers.a -ldl`/`containers.so -ldl` as an argument. + +## Documentation +For high-level documentation and usage, visit the +[documentation](documentation.md) page. For in-depth documentation, visit the +[code docs](https://codedocs.xyz/bkthomps/Containers/) page. ## Container Types The container types that this library contains are described below. diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..f73e16d --- /dev/null +++ b/documentation.md @@ -0,0 +1,126 @@ +# Documentation +For setup and compilation instructions visit the [readme](README.md). For +in-depth documentation, visit the +[code docs](https://codedocs.xyz/bkthomps/Containers/) page. + +## General Overview +Each container has an initialization function which returns the container +object. For a deque, this would be `deque_init()` which returns a `deque`. The +returned object is a pointer to an internal struct which contains data and +book keeping information. However, this is abstracted away to reduce mistakes, +and since it is not stored in the most easy manner. More information about the +initialization type of function is presented below in its own section. + +Once this object is initialized, it is possible to manipulate it using the +provided functions, which have in-depth documentation available. Each container +has adding and retrieval type functions, and each type of container has its own +specific set of function interfaces, which are explained in-depth at the +function level in the code docs link above. More high-level information will be +explained in its own section below. + +Finally, each container will have to be destroyed to free the memory associated +with it. + +# Container Initialization +When creating a container, first you must decide what type of data you wish to +store in it (or types for the case of a map). Then, you must either decide if +you wish to store a copy of the data in the container, or a pointer to it. The +benefit of a copy is that you don't have to manage the memory, and it is easier +to reason. However, it might only be reasonable to store copies if the data type +is relatively cheap to copy. Using pointers, you can either store references to +automatic variables (make sure the lifetime stays valid while the data is +stored), or dynamically-allocated variables (make sure to free at some point). +Either way, you must pass in a pointer of what you wish to store, be it a +pointer to a value, or a pointer to a pointer. Keep in mind that for some +containers, changing data which is stored will cause undefined behavior. Fret +not, as all containers document their potential undefined behavior in the +function documentation comments. + +Going back to the initialization function and ways we can initialize containers +and and store data, we can use an example of a `deque`: +``` +deque a = deque_init(sizeof(int)); /* OK: cheap to copy */ +deque b = deque_init(sizeof(struct expensive_data)); /* Bad: valid, but expensive to copy */ +deque c = deque_init(sizeof(struct expensive_data *)); /* OK: pointer to expensive data, but be careful about memory management */ +``` + +Also, if the arguments which you passed in to the initialization function are +invalid, or the system is out of memory, the initialization function may return +NULL. Therefore, it is good to check for a return of NULL from the +initialization functions. + +# Container Operations +Next, the two basic container manipulation functions of containers are to add +and remove elements. To add elements, pass a pointer of the element you wish to +copy to the add function. To remove an element, pass a pointer to a variable +which can store the size of element that is stored in the container. + +Using the `deque` example, adding and removal can be done this way (if storing +int): +``` +deque d = deque_init(sizeof(int)); +... +int add = 5; +int rc = deque_push_back(d, &add); /* 5 has been added to the back of the deque */ +... +int retrieve; +int rc = deque_pop_back(&retrieve, d); /* retrieve now is equal to 5 */ +... +``` + +Functions can fail for various reasons, such as the provided index argument +being out of bounds, or the system running out of memory. The in-depth +documentation linked above provides the exhaustive list of return codes for each +function, which are present in the `errno.h` header file. For example, an +invalid argument would return `-EINVAL`, and on success 0 would be returned. + +# Comparators and Hash Functions +The associative containers and the priority queue require the user to initialize +the container with a comparator, and the unordered associative containers also +require a hash function to be passed in. State should not be modified in +comparators or in hash functions, or else it would lead to undefined behavior. + +When a comparator function is called, two arguments are passed in, being two +elements to compare. The comparator must return 0 is they are equal, a negative +value if the first is less than the second, and a positive value is the first is +greater than the second. To be valid, a comparator must obey the following +rules: +1. Reflexive: `arg_1 == arg_1` and `arg_2 == arg_2` must always be true +2. Symmetric: if `arg_1 == arg_2` is true, then `arg_2 == arg_1` is true +3. Transitive: if the comparator is called twice with three distinct objects, +the first time being `(arg_1, arg_2)` and the second `(arg_2, arg_3)`, then if +`arg_1 == arg_2` and `arg_2 == arg_3` then `arg_1 == arg_3` must be true +4. Consistent: the truth of `arg_1 == arg_2` shall never change over time if +both arguments do not change + +When a hash function is called, it is provided one argument, and must hash its +attributes. A hash is a mapping from the object to a number. Two objects which +are equal must always result in the same hash being produced. The inverse is not +required, but should be sufficiently improbable (meaning, two distinct objects +may produce the same hash, but it should be very rare). + +Using unordered set as an example (and storing int), the comparator and hash +function can be passed in as follows when initializing: +``` +unordered_set_init(sizeof(int), hash_int, compare_int) +``` + +A comparator can be as follows: +``` +static int compare_int(const void *const one, const void *const two) +{ + const int a = *(int *) one; + const int b = *(int *) two; + return a - b; +} +``` + +And a hash function can be as follows: +``` +static unsigned long hash_int(const void *const key) +{ + unsigned long hash = 17; + hash = 31 * hash + *(int *) key; + return hash; +} +```