Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utilities to forward declare compound units #342

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions au/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ cc_library(
hdrs = ["code/au/packs.hh"],
includes = ["code"],
deps = [
":fwd",
":stdx",
":utility",
],
Expand Down
36 changes: 36 additions & 0 deletions au/code/au/fwd.hh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ namespace au {

struct Zero;

template <typename B, std::intmax_t N>
struct Pow;

template <typename B, std::intmax_t N, std::intmax_t D>
struct RatioPow;

template <typename... BPs>
struct Dimension;

Expand All @@ -38,6 +44,36 @@ struct QuantityPointMaker;
template <typename UnitT, typename RepT>
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 <typename... UnitPowers>
struct UnitProduct;
template <typename... UnitPowers>
struct ForwardDeclareUnitProduct {
using unit_type = UnitProduct<UnitPowers...>;
};

//
// 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 <typename U, std::intmax_t N, std::intmax_t D = 1>
struct ForwardDeclareUnitPow {
using unit_type = RatioPow<U, N, D>;
};
template <typename U, std::intmax_t N>
struct ForwardDeclareUnitPow<U, N, 1> {
using unit_type = Pow<U, N>;
};

//
// Quantity aliases to set a particular Rep.
//
Expand Down
3 changes: 2 additions & 1 deletion au/code/au/fwd_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
10 changes: 7 additions & 3 deletions au/code/au/fwd_test_lib.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Meters> &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<MetersPerSecond> &q) {
std::ostringstream oss;
oss << q;
return oss.str();
}

} // namespace au
} // namespace xyz
13 changes: 10 additions & 3 deletions au/code/au/fwd_test_lib.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<Meters> &q);
using InverseSecondsFwd = au::ForwardDeclareUnitPow<au::Seconds, -1>;
using InverseSeconds = typename InverseSecondsFwd::unit_type;

} // namespace au
using MetersPerSecondFwd = au::ForwardDeclareUnitProduct<au::Meters, InverseSeconds>;
using MetersPerSecond = typename MetersPerSecondFwd::unit_type;

std::string print_to_string(const au::QuantityI<MetersPerSecond> &q);

} // namespace xyz
1 change: 1 addition & 0 deletions au/code/au/packs.hh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <ratio>
#include <utility>

#include "au/fwd.hh"
#include "au/stdx/experimental/is_detected.hh"
#include "au/stdx/type_traits.hh"
#include "au/utility/type_traits.hh"
Expand Down
12 changes: 12 additions & 0 deletions au/code/au/unit_of_measure.hh
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,18 @@ using UnitInverseT = UnitPowerT<U, -1>;
template <typename U1, typename U2>
using UnitQuotientT = UnitProductT<U1, UnitInverseT<U2>>;

template <typename... Us>
constexpr bool is_forward_declared_unit_valid(ForwardDeclareUnitProduct<Us...>) {
return std::is_same<typename ForwardDeclareUnitProduct<Us...>::unit_type,
UnitProductT<Us...>>::value;
}

template <typename U, std::intmax_t ExpNum, std::intmax_t ExpDen>
constexpr bool is_forward_declared_unit_valid(ForwardDeclareUnitPow<U, ExpNum, ExpDen>) {
return std::is_same<typename ForwardDeclareUnitPow<U, ExpNum, ExpDen>::unit_type,
UnitPowerT<U, ExpNum, ExpDen>>::value;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Unit arithmetic on _instances_ of Units and/or Magnitudes.

Expand Down
Binary file added docs/assets/fwd_declare_compiler_error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions docs/howto/forward-declarations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# 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<U, R>` type (note that you'll need forward-declared _units_ for this to be useful)
- `QuantityD<U>`, `QuantityI<U>`, and the other "rep-named aliases" for `Quantity`
- `QuantityPoint<U, R>` type (again: requires _units_ to be forward declared separately)
- `QuantityPointD<U>`, `QuantityPointI<U>`, 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<Kilo<Meters>>&` 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<Meters, Seconds>`). 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we quantify what is the too slow and what is the target?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (0bb43ee).


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<YourUnits, -3>;
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<YourUnits, 1, 2>`.

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<OtherUnits, Pow<YourUnits, -3>>`, you would forward declare it like this (using the
existing `InverseYourUnitsCubed` that you would have defined in step 2):

```cpp
using OtherUnitsPerYourUnitsCubedFwd = ForwardDeclareUnitProduct<OtherUnits, InverseYourUnitsCubed>;
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<UnitQuotientT<Kilo<Meters>, Hours>>`. However,
`UnitQuotientT` needs the full machinery of the library. Instead, let's create an alias,
`KilometersPerHour`, so we can write `QuantityD<KilometersPerHour>`.

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<Meters>` and `Pow<Hours, -1>`, 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 <string>

#include "au/fwd.hh"
#include "au/units/hours_fwd.hh"
#include "au/units/meters_fwd.hh"

namespace my_library {

using InverseHoursFwd = au::ForwardDeclareUnitPow<au::Hours, -1>;
using InverseHours = typename InverseHoursFwd::unit_type;

using KilometersPerHourFwd = au::ForwardDeclareUnitProduct<au::Kilo<au::Meters>, InverseHours>;
using KilometersPerHour = typename KilometersPerHourFwd::unit_type;

std::string print_to_string(const au::QuantityD<KilometersPerHour>& speed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: I was hoping that the fwd types would be named the same as the underlying types. But I guess that wouldn't have the checks that we care about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this was the cleanest way I could think of to make it easy to check the answers robustly.


} // namespace my_library
```

Now, the implementation file:

```cpp
// print_speed.cc

#include "print_speed.hh"

#include <sstream>
#include <string>

#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<KilometersPerHour>& 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 `<string>`.
3 changes: 3 additions & 0 deletions docs/howto/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/howto/new-units.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pi>{} / 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:

Expand Down
Loading