Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/design/algorithm.md
Original file line number Diff line number Diff line change
@@ -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?
83 changes: 83 additions & 0 deletions docs/design/rollover.md
Original file line number Diff line number Diff line change
@@ -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`.


59 changes: 59 additions & 0 deletions docs/design/tuple.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions docs/design/type_traits.md
Original file line number Diff line number Diff line change
@@ -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<bool B, typename T, typename U>` as a
`struct`. This means that for every combination of `<B, T, U>` the compiler
instantiates a template and creates a type: a relatively expensive compile-time
operation.

We define `conditional_t<bool B, typename T, typename U>` 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.