From c8ff7f02f10fb931adef8ddacce015619529346e Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Sun, 9 Jul 2023 13:28:07 +0200 Subject: [PATCH] docs: "Generic Interfaces" chapter added --- .../framework_basics/generic_interfaces.md | 195 ++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 196 insertions(+) create mode 100644 docs/users_guide/framework_basics/generic_interfaces.md diff --git a/docs/users_guide/framework_basics/generic_interfaces.md b/docs/users_guide/framework_basics/generic_interfaces.md new file mode 100644 index 00000000..3ee4c600 --- /dev/null +++ b/docs/users_guide/framework_basics/generic_interfaces.md @@ -0,0 +1,195 @@ +# Generic Interfaces + +Using a concrete unit in the interface often has a lot of sense. It is especially useful if we +store the data internally in the object. In such a case, we have to select a specific unit anyway. + +For example, let's consider a simple storage tank: + +```cpp +class StorageTank { + quantity base_; + quantity height_; + quantity density_ = air_density; +public: + constexpr StorageTank(const quantity& base, const quantity& height) : + base_(base), height_(height) + { + } + + // ... +}; +``` + +As the quantities provided in the function's interface are then stored in the class, there is probably +no sense in using generic interfaces here. + + +## The issues with unit-specific interfaces + +However, in many cases, using a specific unit in the interface is counterproductive. Let's consider +the following function: + +```cpp +quantity avg_speed(quantity distance, + quantity duration) +{ + return distance / duration; +} +``` + +Everything seems fine for now. It also works great if we call it with: + +```cpp +quantity s1 = avg_speed(220 * km, 2 * h); +``` + +However, if the user starts doing the following: + +```cpp +quantity s2 = avg_speed(140 * mi, 2 * h); +quantity s3 = avg_speed(20 * m, 2 * s); +``` + +some issues start to be clearly visible: + +1. The arguments must be converted to units mandated by the function's parameters at each call. + This involves potentially expensive multiplication/division operations at runtime. +2. After the function returns the speed in a unit of `km/h`, another potentially expensive + multiplication/division operations have to be performed to convert the resulting quantity into + a unit being the derived unit of the initial function's arguments. +3. Besides the obvious runtime cost, some unit conversions may result in a data truncation which + means that the result will not be exactly equal to a direct division of the function's arguments. +4. We have to use a floating-point representation type (the `quantity` class template by default uses + `double` as a representation type) which is considered + [value preserving](../value_conversions/#value-preserving-conversions). + Trying to use an integral type in this scenario will work only for `s1`, while `s2` and `s3` + will fail to compile. Failing to compile is a good thing here as the library tries to prevent + the user from doing a clearly wrong thing. To make the code compile, the user needs to use + a dedicated [`value_cast`](../value_conversions/#value-truncating-conversions) like this: + + ```cpp + quantity s2 = avg_speed(value_cast(140 * mi), 2 * h); + quantity s3 = avg_speed(value_cast(20 * m), value_cast(2 * s)); + ``` + + but the above will obviously provide an incorrect behavior (i.e. division by `0` in the evaluation + of `s3`). + + +## A naive solution + +A naive solution here would be to implement the function as an unconstrained function template: + +```cpp +auto avg_speed(auto distance, auto duration) +{ + return distance / duration; +} +``` + +Beware that there are better solutions than this. The above code is too generic. Such a function template +accepts everything: + +- quantities of other types + - the compiler will not prevent accidental reordering of the function's arguments + - quantities of different types can be passed as well +- plain `double` arguments +- `std::vector` and `std::lock_guard` will be accepted as well (of course, this will fail in the + function's body later in the compilation process) + + +!!! note + + The usage of `auto` instead of a function parameter type is a C++20 feature. It makes such + a code a function template where the type of such a parameter will be deduced during + the template instantiation process from the argument type passed by the user. + + +## Constraining function template arguments with concepts + +Much better generic code can be implemented using [basic concepts](../basic_concepts) +provided with the library: + +```cpp +auto avg_speed(QuantityOf auto distance, + QuantityOf auto duration) +{ + return isq::speed(distance / duration); +} +``` + +This explicitly states that the arguments passed by the user must not only satisfy +a [`Quantity`](../basic_concepts/#quantity) concept but also their quantity specification must +be implicitly convertible to `isq::length` and `isq::time` accordingly. This no longer leaves +room for error while still allowing the compiler to generate the most efficient code. + +!!! tip + + Please note that now it is safe just to use integral types all the way which again + improves the runtime performance as the multiplication/division operations are often + faster on integral rather than floating-point types. + + +## Constraining function template return type + +The above function template resolves all of the [issues described before](#the-issues-with-unit-specific-interfaces). +However, we can do even better here by additionally constraining the return type: + +```cpp +QuantityOf auto avg_speed(QuantityOf auto distance, + QuantityOf auto duration) +{ + return isq::speed(distance / duration); +} +``` + +Doing so has two important benefits: + +1. It informs the users of our interface about what to expect to be the result of a function + invocation. It is superior to just returning `auto`, which does not provide any hint about + the thing being returned there. +2. Such a concept constrains the type returned from the function. This means that it works as + a unit test to verify if our function actually performs what it is supposed to do. If there is + an error in [quantity equations](../../appendix/glossary/#quantity-equation), we will learn + about it right away. + + +## Constraining a variable on the stack + +If we know exactly what the function does in its internals and if we know the exact argument types +passed to such a function, we often know the exact type that will be returned from its invocation. + +However, if we care about performance, we should often use the generic interfaces described in this +chapter. A side effect is that we sometimes are unsure about the return type. Even if we know it +today, it might change a week from now due to some code refactoring. + +In such cases, we can again use `auto` to denote the type: + +```cpp +auto s1 = avg_speed(220 * km, 2 * h); +auto s2 = avg_speed(140 * mi, 2 * h); +auto s3 = avg_speed(20 * m, 2 * s); +``` + +In this case, it is probably OK to do so as the `avg_speed` function name explicitly provides +the information on what to expect as a result. + +In other scenarios where the returned quantity type is not so obvious, it is again helpful to +constrain the type with a concept like so: + +```cpp +QuantityOf auto s1 = avg_speed(220 * km, 2 * h); +QuantityOf auto s2 = avg_speed(140 * mi, 2 * h); +QuantityOf auto s3 = avg_speed(20 * m, 2 * s); +``` + +Again this explicitly provides additional information about the quantity we are dealing with in +the code, and it serves as a unit test checking if the "thing" returned from a function is actually +what we expected here. + + +!!! note + + The `QuantityOf` and `QuantityPointOf` concepts are probably the most useful, but there + are a few more to play with. A list of all the concepts can be found in + [the "Basic Concepts" chapter](../basic_concepts). diff --git a/mkdocs.yml b/mkdocs.yml index f2e4b0cd..4d2ef9a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -113,6 +113,7 @@ nav: - Value Conversions: users_guide/framework_basics/value_conversions.md - Character of a Quantity: users_guide/framework_basics/character_of_a_quantity.md - Quantity Arithmetics: users_guide/framework_basics/quantity_arithmetics.md + - Generic Interfaces: users_guide/framework_basics/generic_interfaces.md - Faster-than-lightspeed Constants: users_guide/framework_basics/faster_than_lightspeed_constants.md - Dimensionless Quantities: users_guide/framework_basics/dimensionless_quantities.md - The Affine Space: users_guide/framework_basics/the_affine_space.md