How to best convert an underlying reference type to an lvalue quantity reference? #779
-
|
Hello community, I have an interoperability use case of mp-units with other libraries in FrancoisCarouge/TypedLinearAlgebra. The library provides strong type support at the library interface API boundaries. The library stores the numerical values in the composed third party linear algebra (E.g.: Eigen, std::linalg). The typed linear algebra library composes mp-units and linear algebra libraries. The typed linear algebra provides type erased interoperability. The library supports the conversions between, to and from, quantity and numeric types. A customization point object supports the injection of the conversions into the type-erased library. The conversions corresponds to the type libraries (e.g. mp-units). The conversions are supplied in the form of a template structure with a static call operator: template <typename To, typename From> struct element_caster;A default implementation for the conversions available for types that are implicitly convertible (or no conversion needed) can be found as the following. template <typename To, typename From>
[[nodiscard]] static constexpr To element_caster<To, From>::operator()(From value) {
return value;
}The end-user provides the element caster specialization(s) according to the library type used. For mp-units, they may look like the following for quantity, quantity point, and quantity reference types converted to and from built-in scalar types. The required explicit conversion type safety remains as expected. // (1)
template <typename To, mp_units::Quantity From>
struct element_caster<To, From> {
[[nodiscard]] static constexpr auto operator()(const From &value) -> To {
return value.numerical_value_in(value.unit);
}
};
// (2)
template <mp_units::Quantity To, typename From>
struct element_caster<To, From> {
[[nodiscard]] static constexpr auto operator()(const From &value) -> To {
return value * To::reference;
}
};
template <typename To, mp_units::QuantityPoint From>
struct element_caster<To, From> {
[[nodiscard]] static constexpr auto operator()(const From &value) -> To {
return value.quantity_from_zero().numerical_value_in(value.unit);
}
};
template <mp_units::QuantityPoint To, typename From>
struct element_caster<To, From> {
[[nodiscard]] static constexpr auto operator()(const From &value) -> To {
return {value * To::unit, mp_units::default_point_origin(To::unit)};
}
};
template <typename To, mp_units::Reference From>
struct element_caster<To, From> {
[[nodiscard]] static constexpr auto
operator()([[maybe_unused]] From value) -> To {
return 1.;
}
};These specializations support the conversion to and from storage for construction, assignment, read of the elements of the matrices. state x0{3. * m, 2. * m / s, 1. * m / s2}; // (1) Constructor converts the quantity to double storage types.
quantity v = x0[1_i]; // (2) Accessor converts the double storage to quantity types.More examples can be seen in the longer Eigen/mp-units sample. Now, here is an interesting, expected use case access pattern for interoperability through an (lvalue?) reference quantity value: x0[1_i] = 2. * m / s; // (A) Assign the quantity to the element's value underlying storage. At the moment, the conversion can be implemented as the following: // (A)
template <mp_units::Quantity To, typename From>
struct element_caster<To &, From &> {
[[nodiscard]] static constexpr auto operator()(From &value) -> To & {
return reinterpret_cast<To &>(value);
}
};I understand this implementation is problematic due to the lack of guarantees provided by
Thank you! |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 12 replies
-
|
I'm pretty sure mp-units has something for this, because I remember having design discussions about Au's I remember coming away from the discussion thinking mp-units had a function that played this role (i.e., get an lvalue reference to the underlying stored value), but I can't remember what it's called. Anyway, that should be the way to go! |
Beta Was this translation helpful? Give feedback.
-
|
Hi @FrancoisCarouge! I have followed your work for a while, and I always forward people who ask about heterogeneous vectors to your library. However, I never had time to analyze the design in detail.
If that is true, then you might want to constrain with this assumption. Also, in such a case, it is probably better to pass quantities by value and not
Your caster is probably needed for other needs as well, but you could consider using mp-units interoperability traits as well.
Actually, it is not 😉 using Q = quantity<si::metre>;
static_assert(std::is_standard_layout_v<Q>);
static_assert(std::is_pointer_interconvertible_with_class(&Q::numerical_value_is_an_implementation_detail_));https://godbolt.org/z/Gf3Tx35bT Of course, please also consider
Probably yes, but you need to point me to the code you would like to optimize. |
Beta Was this translation helpful? Give feedback.
-
|
Thank you @mpusz and @chiphogg for the discussion and forwarding people. ☕🥐 Re: Conversion Performance Yes, it is indeed probably better to pass quantities and underlying storage types by values instead of const reference parameters in this conversion case. I now address this feedback in this pull request [236]. This sample could indeed be constrained with narrower specializations of conversion for both the Re: Terminology Yes, and I found the last full Re: Casting I may be missing something obvious! The more common, present use case is the opposite. I believe this may not correspond to the conversion where a representation reference converts to a quantity reference. The quantity reference is returned. The representation reference is input. The inverse conversion is needed: get an lvalue quantity reference from a representation value/reference. Perhaps something equivalent to the semantic of a free-function like // The conversion under discussion:
// E.g.: double & --> quantity &
template <mp_units::Quantity To, typename From>
struct element_caster<To &, From &> {
[[nodiscard]] static constexpr auto operator()(From &value) -> To & {
return reinterpret_cast<To &>(value); // Is there better?
}
};Given confirmation the // Minimal illustrative conversion equivalent example:
double v{42.};
quantity<mp_units::isq::length[m], double> & q = reinterpret_cast<quantity<isq::length[m], double>&>(v);
q = 17. * m;
// Should there be an mp-units conversion defined in place of `reinterpret_cast`?
// quantity & q = to_quantity_ref_in<mp_units::isq::length[m], double>(v, m); Trying to write more of the conversions, we get the following permutations.
Early on in the development of the typed linear algebra library, I had used the mp-units interoperability traits. I realized the use case was not about converting between mp-units and another unit type library. The linear algebra storage is not a quantity-like type. It may be a built-in type. It may be more exotic like I also considered using a strong type to represent the storage and provide the traits against that. Nonetheless the conversion injection and needs remained ultimately identical. It is really a quantity-representation conversion needed. I suppose it could be a quantity-representation use case which permits the generalization to non-quantity domains (e.g. linear algebra memory storage types). 🍺 |
Beta Was this translation helpful? Give feedback.
-
|
You were right! I was wrong! And today I learned 😄 You said it: I cannot conjure an object that is not type-compatible reference at an address, and the exceptions to the rules are not applicable. It is now so obvious. I can't believe I didn't internalize this information many many years ago. I went down the rabbit hole: template <mp_units::Quantity To, typename From>
struct element_caster<To &, From &> {
[[nodiscard]] static constexpr auto operator()(From &value) -> To & {
// Problem: Is this cast undefined behavior?
return reinterpret_cast<To &>(value); // (1)
// (2) return *reinterpret_cast<To *>(&value);
}
// Analysis scope reduction for sanity:
static_assert(std::same_as<double, From>,
"In this particular sample, we only deal in doubles. We "
"restrict the analysis to double for now, before generalizing "
"to others.");
// Usage pre-requisites:
static_assert(std::same_as<typename To::rep, From>,
"Only conversion between identical mp-units quantity type "
"representation and the linear algebra storage type is "
"intended and expected to be supported here.");
// Known properties:
static_assert(std::is_standard_layout_v<To>,
"mp-units quantities are standard layout.");
static_assert(std::is_standard_layout_v<From>,
"Linear algebra storage types are standard layout.");
// Strict Aliasing
// The strict aliasing rule is a provision that allows the compiler to assume
// that pointers (or references) of unrelated types do not point to the same
// memory location. This assumption is critical for Type-Based Alias Analysis
// (TBAA), enabling aggressive optimizations such as reordering instructions
// and keeping values in registers.
// Every object has an effective type, which determines which lvalue accesses
// are valid and which violate the strict aliasing rules.
// Obvious pre-condition:
static_assert(
sizeof(To) == sizeof(From),
"The mp-units quantity type and the linear algebra storage type "
"must have the same size as one of the early conditions for this cast to "
"be correct: memory can't be accessed out of bounds.");
// Given an object with effective type T1, using an lvalue expression
// (typically, dereferencing a pointer) of a different type T2 is undefined
// behavior, unless:
// * T2 and T1 are compatible types.
// --> No:
// https://en.cppreference.com/w/c/language/compatible_type.html#Compatible_types
// * T2 is cvr-qualified version of a type that is compatible with T1.
// --> No
// * T2 is a signed or unsigned version of a type that is compatible with T1.
// --> No
// * T2 is a character type (char, signed char, or unsigned char).
// --> No
// * T2 is an aggregate type or union type type that includes one of the
// aforementioned types among its members (including, recursively, a member of
// a subaggregate or contained union).
// --> Maybe?
// Alignment requirements are also necessary:
static_assert(
alignof(To) == alignof(From),
"The mp-units quantity type and the linear algebra storage type "
"must have the same alignment as one of the early conditions for "
"this cast to be correct: memory can't be accessed misaligned.");
// mp-units quantity types are structural types with this (simplified)
// declaration: `struct quantity { double value; };`
// https://github.com/mpusz/mp-units/blob/4130644b3162912b793d38a1e529d7ca4c290b4e/src/core/include/mp-units/framework/quantity.h#L230
// Uh oh!? Expected. Not relevant here.
static_assert(not std::is_layout_compatible_v<To, From>);
// --> No. Not an aggregate type.
// It turns out this was not the even applicable exception to consider!
// https://en.cppreference.com/w/cpp/language/aggregate_initialization.html
static_assert(not std::is_aggregate_v<To>,
"mp-units quantity types are not agregate types: they have "
"user-declared constuctors!");
// Another learning:
// is_pointer_interconvertible_with_class actually means
// is_member_pointer_convertible_to_class_pointer! And it is not commutative.
static_assert(
std::is_pointer_interconvertible_with_class(
&To::numerical_value_is_an_implementation_detail_),
"The mp-units quantity type is pointer-interconvertible with its "
"first non-static data member, which is the numerical value.");
// Conclusion:
// This conversion does not fall under the exception to the rule of:
// Given an object with effective type T1, using an lvalue expression
// of a different type T2 is undefined behavior.
// The conversion dereferences the pointer, uses the reference and makes it
// undefined behavior. It becomes obvious when we note that there does not
// exists a quantity object for the pointer. The compiler is allowed to assume
// the pointers point to the different memory location. The program is not
// correct.
// The project uses various optimization level, sanitizers, and the
// "-fstrict-aliasing" flag. The UB did not manifest in tooling or behavior.
// I needed to crank up the compilers aliasing level warning to its aggressive
// setting "-Wstrict-aliasing=1" to start receiving feedback. The default
// level was "3" from the compilers because this warning gives false
// positives.
// Where to now?
// We can use the non-standard "may_alias" attribute to tell the compiler that
// the type may alias other types. The undefined behavior is avoided.
// It permits to use an lvalue quantity reference. The usage is natural for
// users. We avoid ergonomics issues with solutions returning an auxiliary
// type. But was it worth it?
};Do you have experience with I have some more follow-up to wrap up this discussion. Thank you! |
Beta Was this translation helpful? Give feedback.
-
|
To the future reader who found this discussion, to the question: How to best convert an underlying reference type to an lvalue quantity reference? TL;DR answer: Don't, you shouldn't. Details: Reinterpreting Out of the compliance with the standard, using compiler extension to alias pointers, will result in non-portable code and prevent constant expression, constexpr code. The issue is a design defect manifesting in the code as this coupling of reference. You may want to think about the design. ❤️ |
Beta Was this translation helpful? Give feedback.
To the future reader who found this discussion, to the question: How to best convert an underlying reference type to an lvalue quantity reference?
TL;DR answer: Don't, you shouldn't.
Details:
Reinterpreting
double &toquantity<..., double> &is undefined behavior, type-punning. No amount of size, alignment, weakening aliasing, implicit lifetime, or explicit lifetime can make two objects share the same memory. The compiler may reorder, optimize the reads and writes of these two objects. Different tools, platforms, today, or in the future will impact this non-compliant code. You may diagnose these issues with the-fstrict-aliasing -Wstrict-aliasing=1compiler options.Out of the compliance…