diff --git a/au/BUILD.bazel b/au/BUILD.bazel index 1ee74f1f..f5d34c47 100644 --- a/au/BUILD.bazel +++ b/au/BUILD.bazel @@ -404,6 +404,7 @@ cc_library( hdrs = ["code/au/packs.hh"], includes = ["code"], deps = [ + ":fwd", ":stdx", ":utility", ], diff --git a/au/code/au/fwd.hh b/au/code/au/fwd.hh index cb173822..85b5891d 100644 --- a/au/code/au/fwd.hh +++ b/au/code/au/fwd.hh @@ -20,6 +20,12 @@ namespace au { struct Zero; +template +struct Pow; + +template +struct RatioPow; + template struct Dimension; @@ -38,6 +44,36 @@ struct QuantityPointMaker; template class Quantity; +// +// Machinery for forward-declaring a unit product. +// +// To use, make an alias with the correct unit powers in the correct order, in the `_fwd.hh` file. +// In the `.hh` file, call `is_forward_declared_unit_valid(...)` (defined in `unit_of_measure.hh`) +// on an instance of that alias. +// +template +struct UnitProduct; +template +struct ForwardDeclareUnitProduct { + using unit_type = UnitProduct; +}; + +// +// Machinery for forward-declaring a unit power. +// +// To use, make an alias with the same unit and power(s) that `UnitPowerT` would produce, in the +// `_fwd.hh` file. In the `.hh` file, call `is_forward_declared_unit_valid(...)` (defined in +// `unit_of_measure.hh`) on that alias. +// +template +struct ForwardDeclareUnitPow { + using unit_type = RatioPow; +}; +template +struct ForwardDeclareUnitPow { + using unit_type = Pow; +}; + // // Quantity aliases to set a particular Rep. // diff --git a/au/code/au/fwd_test.cc b/au/code/au/fwd_test.cc index 67c5ded4..9a884aa7 100644 --- a/au/code/au/fwd_test.cc +++ b/au/code/au/fwd_test.cc @@ -15,6 +15,7 @@ #include "au/fwd_test_lib.hh" #include "au/quantity.hh" #include "au/units/meters.hh" +#include "au/units/seconds.hh" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -23,7 +24,7 @@ using ::testing::StrEq; namespace au { TEST(Fwd, CanCallFunctionDeclaredWithOnlyFwdFiles) { - EXPECT_THAT(print_to_string(meters(1)), StrEq("1 m")); + EXPECT_THAT(xyz::print_to_string((meters / second)(1)), StrEq("1 m / s")); } } // namespace au diff --git a/au/code/au/fwd_test_lib.cc b/au/code/au/fwd_test_lib.cc index 14ea6a9d..5f4c1cc7 100644 --- a/au/code/au/fwd_test_lib.cc +++ b/au/code/au/fwd_test_lib.cc @@ -20,13 +20,17 @@ #include "au/io.hh" #include "au/quantity.hh" #include "au/units/meters.hh" +#include "au/units/seconds.hh" -namespace au { +namespace xyz { -std::string print_to_string(const QuantityI &q) { +static_assert(is_forward_declared_unit_valid(InverseSecondsFwd{}), ""); +static_assert(is_forward_declared_unit_valid(MetersPerSecondFwd{}), ""); + +std::string print_to_string(const au::QuantityI &q) { std::ostringstream oss; oss << q; return oss.str(); } -} // namespace au +} // namespace xyz diff --git a/au/code/au/fwd_test_lib.hh b/au/code/au/fwd_test_lib.hh index c870897d..63266058 100644 --- a/au/code/au/fwd_test_lib.hh +++ b/au/code/au/fwd_test_lib.hh @@ -18,9 +18,16 @@ #include "au/fwd.hh" #include "au/units/meters_fwd.hh" +#include "au/units/seconds_fwd.hh" -namespace au { +namespace xyz { -std::string print_to_string(const QuantityI &q); +using InverseSecondsFwd = au::ForwardDeclareUnitPow; +using InverseSeconds = typename InverseSecondsFwd::unit_type; -} // namespace au +using MetersPerSecondFwd = au::ForwardDeclareUnitProduct; +using MetersPerSecond = typename MetersPerSecondFwd::unit_type; + +std::string print_to_string(const au::QuantityI &q); + +} // namespace xyz diff --git a/au/code/au/packs.hh b/au/code/au/packs.hh index 28718bc8..d8e18772 100644 --- a/au/code/au/packs.hh +++ b/au/code/au/packs.hh @@ -18,6 +18,7 @@ #include #include +#include "au/fwd.hh" #include "au/stdx/experimental/is_detected.hh" #include "au/stdx/type_traits.hh" #include "au/utility/type_traits.hh" diff --git a/au/code/au/unit_of_measure.hh b/au/code/au/unit_of_measure.hh index 538a2bab..2850842c 100644 --- a/au/code/au/unit_of_measure.hh +++ b/au/code/au/unit_of_measure.hh @@ -328,6 +328,18 @@ using UnitInverseT = UnitPowerT; template using UnitQuotientT = UnitProductT>; +template +constexpr bool is_forward_declared_unit_valid(ForwardDeclareUnitProduct) { + return std::is_same::unit_type, + UnitProductT>::value; +} + +template +constexpr bool is_forward_declared_unit_valid(ForwardDeclareUnitPow) { + return std::is_same::unit_type, + UnitPowerT>::value; +} + //////////////////////////////////////////////////////////////////////////////////////////////////// // Unit arithmetic on _instances_ of Units and/or Magnitudes. diff --git a/docs/assets/fwd_declare_compiler_error.png b/docs/assets/fwd_declare_compiler_error.png new file mode 100644 index 00000000..da833a44 Binary files /dev/null and b/docs/assets/fwd_declare_compiler_error.png differ diff --git a/docs/howto/forward-declarations.md b/docs/howto/forward-declarations.md new file mode 100644 index 00000000..35ca2ca1 --- /dev/null +++ b/docs/howto/forward-declarations.md @@ -0,0 +1,209 @@ +# Forward declarations + +Forward declarations can meaningfully speed up the compilation of C++ programs in some situations, +but creating them manually can also be error prone. Au includes authoritatively correct and tested +forward declarations. This page explains how to use them. + +!!! tip + Au itself is generally pretty fast to compile, costing under a second on most modern + configurations. Most users won't need to forward declare Au's types. However, for situations + where you really do need every bit of speed, these forward declarations can help. + +## How to use {#how-to-use} + +First, identify the file in your project that could benefit from forward declarations. _This is +usually a file that gets included in several other files,_ and the point is to speed up compilation +for those other files. The header file (`*.hh`) for a library target can sometimes benefit from +forward declarations. A few library targets in your project might even provide _their own_ forward +declaration files (`*_fwd.hh`) --- in these instances, you'll definitely want to use Au's forward +declarations. + +!!! note + If you're defining compound units (as explained below), you'll _also_ need to identify + a _corresponding_ file that will include the full Au headers. If there's an associated `.cc` + file, this is always a good choice. Alternatively, if this is a library target that provides + a `_fwd.hh` file, then you can use the corresponding `.hh` file. + +Once you've identified which file(s) will be using the forward declarations, you'll include the +appropriate `"fwd"` headers from Au in that file, while using the "full" Au headers in the other +files for that target. + +Here are Au's forward declaration headers, and what each provides. + +### Core library types: `"au/fwd.hh"` + +**Every use of Au forward declarations must include this file.** It provides both core library +types, and a few utilities for combining other units. + +Here's a partial listing of what's included in the core library forward declarations. + +- `Quantity` type (note that you'll need forward-declared _units_ for this to be useful) +- `QuantityD`, `QuantityI`, and the other "rep-named aliases" for `Quantity` +- `QuantityPoint` type (again: requires _units_ to be forward declared separately) +- `QuantityPointD`, `QuantityPointI`, and the other "rep-named aliases" for `QuantityPoint` +- `Kilo`, `Mega`, and the other SI prefixes +- `Zero` type (but not the `ZERO` instance) +- `ForwardDeclareUnitPow` and `ForwardDeclareUnitProduct` to help forward declare _compound units_ + (we'll explain how to use these further below). + +### Unit types: `"au/units/xyz_fwd.hh"` + +Any unit that can be found in, say, `"au/units/xyz.hh"` will have a corresponding forward +declaration file, `"au/units/xyz_fwd.hh"`. + +When combined with the core library forward declarations, this is enough to declare an interface +that takes, say, `const QuantityD>&` as an argument. + +This is _not_ enough to forward declare a _compound unit_, such as meters per second. Our [best +practices](./new-units.md#alias-vs-strong) for new units suggests using a simple _alias_ in this +case, rather than a strong type (for example, `UnitQuotientT`). However, this +cannot be computed without the full machinery of the library, which can cost tens of milliseconds. +This may not sound like much, but it's far too slow for a forward declaration file. + +??? note "How slow is \"too slow\"?" + Time measurements will of course vary based on the hardware and toolchain, but here are some + numbers from one modern development machine. + + We found that the cost to include `"au/fwd.hh"` was about 5 ms. The vast majority of this was + the cost of including ``. If your file already includes `` by some other + pathway --- which is extremely common --- then there is no additional cost to include it + a second time via `"au/fwd.hh"`. In this case, the remaining cost was about 1 ms. This is the + target that we aim for. + + For calibration purposes, the cost of including `` on this same configuration was about + 100 ms. And the cost of including `"au/au.hh"` --- essentially, "the whole library" (without + individual units) --- was around 500 ms. + +We resolve this with a "warrant and check" approach, explained in the next section. + +### Compound units + +The only way to forward declare a compound unit is to specify the exact types that go into the +`UnitProduct<...>` template, and in the exact correct order. We generally avoid having end users do +this, both because it's hard to get right, and because it's an encapsulated implementation detail +which could change. However, for cases where the added speed from forward declaration really +matters, we can do the next best thing: make it easy to check that it's right. + +Here is a series of steps to follow to forward declare compound units. + +1. **Find the types in `UnitProduct<...>`.** One trick to do this is to assign an instance of the + compound unit type itself to another type, say, an `int`. The _compiler error_ will contain the + correct type name. Look for `UnitProduct<...>` in the error message. + +2. **Forward declare the powers with `ForwardDeclareUnitPow<...>`.** If any of the types in + `UnitProduct<...>` are instances of `Pow` or `RatioPow`, you'll want to make an alias for those + types. If your compound unit is, say, the inverse cube of a unit, you can forward declare it + like this: + + ```cpp + using InverseYourUnitsCubedFwd = ForwardDeclareUnitPow; + using InverseYourUnitsCubed = YourUnitsCubedFwd::unit_type; + ``` + + If you add a third template parameter to `ForwardDeclareUnitPow`, it will be treated as the + denominator for the power. For example, to form a square root of the above unit, you could use + `ForwardDeclareUnitPow`. + +3. **Forward declare the product itself with `ForwardDeclareUnitProduct<...>`.** This is similar + to the above. For example, if the full product type from step 1 was + `UnitProduct>`, you would forward declare it like this (using the + existing `InverseYourUnitsCubed` that you would have defined in step 2): + + ```cpp + using OtherUnitsPerYourUnitsCubedFwd = ForwardDeclareUnitProduct; + using OtherUnitsPerYourUnitsCubed = OtherUnitsPerYourUnitsCubedFwd::unit_type; + ``` + +4. **Add appropriate checks.** These will go in the "corresponding file" (see the Note in the + [above section](#how-to-use)). You can pass an instance of the `ForwardDeclare...<...>` type to + the utility `is_forward_declared_unit_valid(...)`, which can be used with `static_assert`. For + example: + + ```cpp + // In whatever file _corresponds to_ the one with the forward declarations: + static_assert(is_forward_declared_unit_valid(InverseYourUnitsCubedFwd{})); + static_assert(is_forward_declared_unit_valid(OtherUnitsPerYourUnitsCubedFwd{})); + ``` + +## Full worked example + +!!! note + We are omitting the `au::` namespace in the text of this example, for conciseness and + readability. + +Suppose we want to make a library target that can print a speed, in km/h, to a `std::string`. +Suppose, too, that we want our library to be as lightweight as possible: maybe some client targets +are interacting with all of their `Quantity` types by const-ref, so they don't actually need to see +Au's definitions. + +Normally, we'd refer to our speed type as `QuantityD, Hours>>`. However, +`UnitQuotientT` needs the full machinery of the library. Instead, let's create an alias, +`KilometersPerHour`, so we can write `QuantityD`. + +The first step is to find out which types go inside `UnitProduct<...>`, and in which order. This +[compiler explorer link](https://godbolt.org/z/cW3Gs7YzT) shows how to do this. Note the +highlighted portion of the error message: + +![Compiler error providing info for fwd decls](../assets/fwd_declare_compiler_error.png) + +We can see that the types are `Kilo` and `Pow`, in that order. We'll need to +start by defining an alias for the latter alone. Then, we can define our `KilometersPerHour` alias, +and declare our function signature. Here's what it looks like all together. + +First, the header file: + +```cpp +// print_speed.hh + +#pragma once + +#include + +#include "au/fwd.hh" +#include "au/units/hours_fwd.hh" +#include "au/units/meters_fwd.hh" + +namespace my_library { + +using InverseHoursFwd = au::ForwardDeclareUnitPow; +using InverseHours = typename InverseHoursFwd::unit_type; + +using KilometersPerHourFwd = au::ForwardDeclareUnitProduct, InverseHours>; +using KilometersPerHour = typename KilometersPerHourFwd::unit_type; + +std::string print_to_string(const au::QuantityD& speed); + +} // namespace my_library +``` + +Now, the implementation file: + +```cpp +// print_speed.cc + +#include "print_speed.hh" + +#include +#include + +#include "au/io.hh" +#include "au/quantity.hh" +#include "au/units/hours.hh" +#include "au/units/meters.hh" + +namespace my_library { + +static_assert(is_forward_declared_unit_valid(InverseHoursFwd{})); +static_assert(is_forward_declared_unit_valid(KilometersPerHourFwd{})); + +std::string print_to_string(const au::QuantityD& speed) { + std::ostringstream oss; + oss << speed; + return oss.str(); +} + +} // namespace my_library +``` + +At this point, the cost of including `print_speed.hh` in other files should be almost completely +negligible, beyond the cost of including ``. diff --git a/docs/howto/index.md b/docs/howto/index.md index d8d4b046..34bdb718 100644 --- a/docs/howto/index.md +++ b/docs/howto/index.md @@ -10,6 +10,9 @@ using the library. Here's a summary of what you'll find. - **[New dimensions](./new-dimensions.md).** How to add a new, independent base dimension. +- **[Forward declarations](./forward-declarations.md).** How to use the forward declarations + provided by the library. + - **[Inter-library Interoperation](./interop/index.md).** How to set up automatic correspondence between equivalent types in Au and any other units library. diff --git a/docs/howto/new-units.md b/docs/howto/new-units.md index 3f087a65..83fcba78 100644 --- a/docs/howto/new-units.md +++ b/docs/howto/new-units.md @@ -149,7 +149,7 @@ Here are some example unit expressions we might reach for to define various comm - Miles: `Feet{} * mag<5280>()` - Degrees: `Radians{} * Magnitude{} / mag<180>()` -## Aliases vs. strong types: best practices +## Aliases vs. strong types: best practices {#alias-vs-strong} A shorter method of defining units is as _aliases_ for a compound unit. For example: