diff --git a/docs/design/algorithm.md b/docs/design/algorithm.md new file mode 100644 index 0000000..796cda2 --- /dev/null +++ b/docs/design/algorithm.md @@ -0,0 +1,47 @@ +# algorithms + +Source code: https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/algorithm.hpp +Documentation: https://intel.github.io/cpp-std-extensions/#_algorithm_hpp + +## Variadic algorithms + +When ranges were introduced, standard (non-range) algorithms stopped getting +many updates. They are spotty in their overloads (iterator pair vs iterator & +count) and they don't tend to be variadic (although some are binary, like +`std::transform`). + +All the algorithms here are variadic, and adhere to the law of useful return: +they return all the iterators that they advanced. + +## `for_each` + +Like `std::for_each`, `stdx::for_each` returns the operation it's given. This is +for the use case of passing a mutable lambda or other function object that +updates its internal state as it works. + +## `for_each_butlast` + +A (surprising?) omission from the standard. Sometimes we want to do something +for each element of a sequence except the last. It turns out that the more +general case, `for_each_butlastn` is just as easy to write. + +This doesn't exist in ranges either (it can be achieved of course for sized +ranges, but not generally). But the same idea exists in other languages +(Haskell's `init` or Common Lisp's `butlast`/`butlastn`). + +## `initial_medial_final` + +Sometimes in embedded systems/hardware interaction we need to do one thing at +first, then do another thing for the bulk elements (but the same for all), +then do something else at the end. Typically interacting with different +start/end registers, or something like that. This is the envisioned use case for +`initial_medial_final`. + +It is clear what to do with a range of 3 or more elements. When we have a +smaller range, we have to make (somewhat arbitrary) decisions. The decisions at +the moment are: + +- For a range of 0 elements, do nothing. Obviously. +- For a range of 2 elements, the medials are omitted. This also seems sensible. +- The real question is what to do for a range of 1 element. At the moment we treat it like an + initial element. Should it be final? Or something else? diff --git a/docs/design/rollover.md b/docs/design/rollover.md new file mode 100644 index 0000000..32c39bc --- /dev/null +++ b/docs/design/rollover.md @@ -0,0 +1,83 @@ +# rollover_t + +Source code: https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/rollover.hpp +Documentation: https://intel.github.io/cpp-std-extensions/#_rollover_hpp + +## Hardware counters wrap around + +To measure time, microcontrollers often have continually-incrementing hardware +counters. A common version might be a 32-bit value that increments every +microsecond. The problem arises: how to deal with when this wraps around? + +Because we need to measure time in the past as well as in the future (because we +need timers to actually expire!), we need to treat time values as signed. +`rollover_t` defines a type that models "now" as always being the central value +in a window that is rolling. Half the bit space is always in the future, and +half is in the past. + +## An arithmetic type + +`rollover_t` is supposed to behave like an integral type, with all the usual +arithmetic operations defined, mod 2ⁿ. + +## Comparisons + +`rollover_t` is equality comparable. This is fine. However, other comparison +operators are deleted. + +The reason is that comparisons on `rollover_t` don't define a strict weak order, +which is required by STL algorithms like `std::sort`. And trying to sort things +with a misbehaving comparison operator can lead to undefined behaviour. + +Concretely, comparisons on `rollover_t` are neither antisymmetric nor +transitive. To see why, consider a `rollover_t` limited to 3 bits. + +With such a `rollover_t`, all of the following are true: + +`0 < 1` +`1 < 2` +`2 < 3` +`3 < 4` +`4 < 5` +`5 < 6` +`6 < 7` +`7 < 0` + +Note also that `5` is less than each of `6`,`7`,`0`,`1`, +_and_ `5` is greater than each of `1`,`2`,`3`,`4`. + +So comparison is not antisymmetric (`5 < 1` and `5 > 1`). +And comparison is non-transitive (like rock-paper-scissors). + +Naively using `std::sort` is not something that is safe, so we delete +`operator<` and friends. However, sometimes it is necessary to sort +`rollover_t`s and in that case we use `cmp_less` as the comparator. + +`cmp_less` doesn't magically fix the properties so that `std::sort` works; it's +just spelt differently so that it stands out on code review. + +Undefined behaviour is a _runtime_ property of a program: with a given data set, +it may not occur. So when we need to sort `rollover_t`s we are warranting that +the data is such that UB will not occur, even though the comparison operation +doesn't have the right properties. It is still UB-safe to sort `rollover_t`s for +example when they all lie within one half of the bit space. In that case they +can be correctly ordered. + +### Other ideas + +Is it perhaps sensible to restore antisymmetry? Can this be done in the same way +as `int`, i.e. by having "now" as a notional zero, with n values less but only +n-1 values greater? + +e.g. for a (signed) char, 128 values are less than zero but only 127 are greater? + +Note that this would not restore transitivity. + +## Use with `std::chrono` + +Since we're measuring time, we want to use `rollover_t` as the `rep` type inside +a `std::chrono::duration` and its corresponding `time_point_t`. + +To do this requires a specialization of `std::common_type` for `rollover_t`. + + diff --git a/docs/design/tuple.md b/docs/design/tuple.md new file mode 100644 index 0000000..57e984d --- /dev/null +++ b/docs/design/tuple.md @@ -0,0 +1,59 @@ +# tuple + +Source code: https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/tuple.hpp +Documentation: https://intel.github.io/cpp-std-extensions/#_tuple_hpp + +## A faster `tuple_cat` + +One of the original reasons for writing our own `tuple` was compilation speed. +In particular, CIB nexus leans heavily on `tuple_cat`. + +The current best known strategy for implementing variadic `tuple_cat` is to +generate the "outer indices" and "inner indices", then zip them together and use +the resulting pairs to pull the elements from each input tuple. + +## The workhorse + +`stdx::tuple` is a basic dependency for many things; for that reason `tuple.hpp` +doesn't include very much, and in particular must avoid circular dependencies. +For example, it can't use formatted `static_assert`s (because they make use of +`tuple`). + +## `tuplelike` vs `has_tuple_protocol` + +`tuplelike` means the type exposes `is_tuple`, which in practice means +`tuplelike` is modelled by `stdx::tuple` and `stdx::indexed_tuple` only. + +`has_tuple_protocol` means that a type models the `get` protocol. This applies +to much more: `stdx::tuple` but also `std::tuple`, `std::pair`, std::array`, +etc. + +## `one_of` + +A common use case is to determine whether or not a value is in a small set. +One way is to just `or` together equality checks: + +```cpp +auto b = x == 1 + or x == 2 + or x == 3; +``` + +But this can be not very easy to read, and begets more complex conditions. +So we used to have code that looked like: + +```cpp +auto b = is_one_of(x, 1, 2, 3); +``` + +This is better, but still not the best. The first function argument is one thing +here and the rest are something else, which doesn't particularly come across in +the formatting or structure. + +To improve this, we settled on making `one_of` a type and overloading equality: + +```cpp +auto b = x == one_of{1, 2, 3}; +``` + +...and this is quite a natural formulation. diff --git a/docs/design/type_traits.md b/docs/design/type_traits.md new file mode 100644 index 0000000..afe6810 --- /dev/null +++ b/docs/design/type_traits.md @@ -0,0 +1,21 @@ +# type_traits + +Source code: https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/type_traits.hpp +Documentation: https://intel.github.io/cpp-std-extensions/#_type_traits_hpp + +## `conditional_t` + +The standard defines `conditional` as a +`struct`. This means that for every combination of `` the compiler +instantiates a template and creates a type: a relatively expensive compile-time +operation. + +We define `conditional_t` as an alias template. +And we define `conditional` parameterized on the `bool` only, with an internal +alias template used to select either `T` or `U`. This means that the actual +template is only instantiated twice (for `true` and `false`) and otherwise the +operations are done through alias templates, which are much cheaper. + +STL implementations typically do the same thing for internal use, but the +standard constrains them to provide the more expensive interface. +