Add documentation (#107)

This commit is contained in:
Bailey Thompson
2020-08-16 22:03:30 -04:00
committed by GitHub
parent eade6e4586
commit 044a853994
2 changed files with 148 additions and 7 deletions

View File

@@ -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.

126
documentation.md Normal file
View File

@@ -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;
}
```