From f70d70335cfff451e9e88c202b09c86233b78303 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Thu, 31 Oct 2024 10:47:17 -0400 Subject: [PATCH] Support Quantity non-type template parameters (#318) Technically, we can't get non-type template parameters of custom class type until C++20. But for a `Quantity` whose rep `R` is integral, we can get the next best thing! This PR introduces a public member typedef, `Quantity::NTTP`, which _can_ be used as a template parameter, because it's an enumeration. We support _bidirectional, implicit_ conversion between `Quantity` and `Quantity::NTTP`, as long as there's an exact match for `U` and `R`. In all other cases, users must use the usual `Quantity` conversion operators to get it into the right type. They can also explicitly request conversion from the NTTP to the `Quantity` by passing the former to `from_nttp(...)`. It's a niche use case, but it's finally possible to use a `Quantity` as a template parameter in a unit-safe way! There just was no other comparable solution before this. Compile time measurements are ongoing, but so far they suggest a small but nonzero penalty. It appears that it varies from file to file, but is generally less than 50 ms. I think that's an OK price to pay for this feature. Fixes #316. --- au/code/au/quantity.hh | 28 +++++++++++++++++++++++ au/code/au/quantity_test.cc | 16 ++++++++++++++ docs/reference/quantity.md | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/au/code/au/quantity.hh b/au/code/au/quantity.hh index bebc9245..bb85748f 100644 --- a/au/code/au/quantity.hh +++ b/au/code/au/quantity.hh @@ -385,6 +385,34 @@ class Quantity { CorrespondingQuantityT{*this}.in(typename CorrespondingQuantity::Unit{})); } + //////////////////////////////////////////////////////////////////////////////////////////////// + // Pre-C++20 Non-Type Template Parameter (NTTP) functionality. + // + // If `Rep` is a built in integral type, then `Quantity::NTTP` can be used as a template + // parameter. + + enum class NTTP : std::conditional_t::value, Rep, bool> { + ENUM_VALUES_ARE_UNUSED + }; + + constexpr Quantity(NTTP val) : value_{static_cast(val)} { + static_assert(std::is_integral::value, + "NTTP functionality only works when rep is built-in integral type"); + } + + constexpr operator NTTP() const { + static_assert(std::is_integral::value, + "NTTP functionality only works when rep is built-in integral type"); + return static_cast(value_); + } + + template + constexpr operator C() const = delete; + // If you got here ^^^, then you need to do your unit conversion **manually**. Check the type + // of the template parameter, and convert it to that same unit and rep. + + friend constexpr Quantity from_nttp(NTTP val) { return val; } + private: template static constexpr void warn_if_integer_division() { diff --git a/au/code/au/quantity_test.cc b/au/code/au/quantity_test.cc index e9dec5cd..74513b9f 100644 --- a/au/code/au/quantity_test.cc +++ b/au/code/au/quantity_test.cc @@ -748,6 +748,22 @@ TEST(Quantity, CommonUnitAlwaysCompletelyIndependentOfOrder) { check_units(kilo(meters), miles, milli(meters)); } +template ::NTTP Length> +struct TemplateOnLength { + QuantityI value = Length; +}; + +TEST(QuantityNTTP, SupportsPreCpp20NttpTypes) { + constexpr auto length = TemplateOnLength{}.value; + EXPECT_THAT(length, SameTypeAndValue(meters(18))); +} + +TEST(QuantityNTTP, CanConvertFromNttpToAnyCompatibleQuantityType) { + constexpr QuantityI::NTTP LengthNTTP = meters(18); + constexpr QuantityI> length = from_nttp(LengthNTTP); + EXPECT_THAT(length, SameTypeAndValue(milli(meters)(18'000))); +} + TEST(Quantity, CommonTypeRespectsImplicitRepSafetyChecks) { // The following test should fail to compile. Uncomment both lines to check. // constexpr auto feeters = QuantityMaker>{}; diff --git a/docs/reference/quantity.md b/docs/reference/quantity.md index 7e93356d..692c9a78 100644 --- a/docs/reference/quantity.md +++ b/docs/reference/quantity.md @@ -377,6 +377,50 @@ These functions also support an explicit template parameter: so, `.coerce_as( Prefer **not** to use the "coercing versions" if possible, because you will get more safety checks. The risks which the "base" versions warn about are real. +## Non-Type Template Parameters (NTTPs) {#nttp} + +A _non-type template parameter_ (NTTP) is a template parameter that is not a _type_, but rather some +kind of _value_. Common examples include `template`, or `template`. Before C++20, +only a small number of types could be used as NTTPs: very roughly, these were _integral_ types, +_pointer_ types, and _enumerations_. + +Au provides a workaround for pre-C++20 users that lets you _effectively_ encode any `Quantity` +as an NTTP, _as long as_ its rep `R` is an **integral** type. To do this, use the +`Quantity::NTTP` type as the template parameter. You will be able to assign between +`Quantity` and `Quantity::NTTP`, _in either direction_, but only in the case of exact +match of both `U` and `R`. For all other cases, you'll need to perform a conversion (using the +usual mechanisms for `Quantity` described elsewhere on this page). + +!!! warning + It is undefined behavior to invoke `Quantity::NTTP` whenever `std::is_integral::value` + is `false`. + + We cannot strictly prevent users from doing this. However, in practice, it is very unlikely for + this to happen by accident. Both conversion operators between `Quantity` and + `Quantity::NTTP` would fail with a hard compiler error, based on a `static_assert` that + explains this situation. So users can name this type, but they cannot assign to it or from it + without prohibitive difficulty. + +??? example "Example: defining and using a template with a `Quantity` NTTP" + ```cpp + template ::NTTP Frequency> + struct TemplatedOnFrequency { + QuantityI value = Frequency; // Assigning `Quantity` from NTTP + }; + + using T = TemplatedOnFrequency; // Setting template parameter from `Quantity` + ``` + +### `from_nttp(Quantity::NTTP)` + +Calling `from_nttp` on a `Quantity::NTTP` will convert it back into the corresponding +`Quantity` that was encoded in the template parameter. This lets it automatically participate +in all of the usual `Quantity` operations and conversions. + +!!! note + If you are simply _assigning_ a `Quantity::NTTP` to a `Quantity`, where `U` and `R` + are identical, you do not need to call `from_nttp`. We support implcit conversion in that case. + ## Operations Au includes as many common operations as possible. Our goal is to avoid incentivizing users to