feat: reject ambiguous tensor order; field and order share an undefined default

tensor_order's primary template is now left undefined: a partial specialization detects the order
structurally for a type that exposes exactly one indexing shape, while a type exposing both (an
N x 1 matrix modeling a vector, as Eigen does) is ambiguous, has no default, and must be specialized
by an adapter or an ordinary template<>. numeric_field consults tensor_order to reach a scalar
element rather than a structural guess, and is defined only where the order is, so an ambiguous
unspecialized type is rejected on both axes (SFINAE-friendly, closing the cross-TU ODR hazard of
guessing). A shared detail::specified concept replaces the open-coded undefined_t checks in
frame_projection and quantity_point bounds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mateusz Pusz
2026-07-05 12:00:58 +02:00
parent 4350baba78
commit 8a505080b5
4 changed files with 101 additions and 58 deletions
@@ -66,9 +66,11 @@ namespace detail {
* templates cannot be "deleted" like functions can.
*/
struct undefined_t {};
inline constexpr undefined_t undefined{};
template<typename T>
concept specified = !std::same_as<std::remove_cvref_t<T>, undefined_t>;
template<typename>
struct cond_underlying_type {};
@@ -285,69 +287,79 @@ MP_UNITS_EXPORT inline constexpr ::mp_units::detail::modulus_impl::modulus_t mod
/////////////// tensor_order ///////////////
namespace detail {
template<typename T>
[[nodiscard]] consteval std::size_t detect_tensor_order()
{
// two-index access (the call operator or the C++23 multidimensional subscript) is order 2
if constexpr (requires(const T& t) { t(std::size_t{}, std::size_t{}); }) return 2;
// GCC 12 defines `__cpp_multidimensional_subscript` but does not implement it: `t[i, j]` is still
// parsed as the deprecated comma-subscript, which fails under `-Werror=comma-subscript` even inside
// this `requires`. Skip the multidimensional-subscript probe there (two-index `t(i, j)` still works).
concept has_vector_indexing = requires(const T& t) { t[std::size_t{}]; };
template<typename T>
concept has_matrix_indexing = requires(const T& t) { t(std::size_t{}, std::size_t{}); }
#if __cpp_multidimensional_subscript && MP_UNITS_COMP_GCC != 12
else if constexpr (requires(const T& t) { t[std::size_t{}, std::size_t{}]; })
return 2;
|| requires(const T& t) { t[std::size_t{}, std::size_t{}]; }
#endif
// one-index access is order 1; everything else is a scalar (order 0)
else if constexpr (requires(const T& t) { t[std::size_t{}]; })
return 1;
else
return 0;
}
;
// A type is structurally ambiguous about its order when it exposes *both* indexing shapes (an N x 1
// matrix modeling a vector, as Eigen does): the same accessors fit a vector and a matrix, and only
// the type's compile-time extents can tell them apart.
template<typename T>
concept has_ambiguous_order = has_vector_indexing<T> && has_matrix_indexing<T>;
} // namespace detail
// The intrinsic tensor order of a representation: 0 scalar, 1 vector, 2 tensor. It is detected from
// the type's structure (two-index access is order 2, one-index access is order 1, otherwise order
// 0) and may be specialized for a third-party representation (e.g. an Eigen adapter reading
// `RowsAtCompileTime` / `ColsAtCompileTime`). This is what replaces the old `disable_vector` /
// `disable_tensor` opt-outs: "a tensor is not a vector" is simply `order 2`, and `2 <= 1` is false.
// The intrinsic tensor order of a representation: 0 scalar, 1 vector, 2 tensor. The primary template
// is left *undefined* (`detail::undefined_t`); a partial specialization detects the order structurally
// for a type that exposes exactly one indexing shape (single-index `t[i]` -> 1, two-index `t(i, j)` ->
// 2, neither -> 0), and a third-party representation may specialize it (e.g. an Eigen adapter reading
// `RowsAtCompileTime` / `ColsAtCompileTime`). A type that exposes *both* shapes is ambiguous - only
// its extents can decide - so it matches neither and stays `undefined` unless specialized: guessing
// would disagree with an adapter, an ODR hazard across translation units.
MP_UNITS_EXPORT template<typename T>
constexpr std::size_t tensor_order = detail::detect_tensor_order<T>();
constexpr detail::undefined_t tensor_order;
template<typename T>
requires(!detail::has_ambiguous_order<T>)
constexpr std::size_t tensor_order<T> = detail::has_matrix_indexing<T> ? std::size_t{2}
: detail::has_vector_indexing<T> ? std::size_t{1}
: std::size_t{0};
/////////////// numeric_field ///////////////
namespace detail {
template<typename T, std::size_t... Is>
requires(sizeof...(Is) >= 1)
[[nodiscard]] consteval auto element_type_of(std::index_sequence<Is...>)
template<typename T>
requires has_vector_indexing<T>
using vector_element_t = std::remove_cvref_t<decltype(std::declval<const T&>()[std::size_t{}])>;
template<typename T>
requires has_matrix_indexing<T>
[[nodiscard]] consteval auto matrix_element()
{
if constexpr (sizeof...(Is) == 1) {
// single-index access uses a plain `t[i]` / `t(i)`: the C++23 pack subscript `t[Is...]` is a
// syntax error before C++23 even for a one-element pack, so it is confined to the branch below.
if constexpr (requires(const T& t) { t[std::size_t{}]; })
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()[std::size_t{}])>>{};
else
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()(std::size_t{}))>>{};
} else {
#if __cpp_multidimensional_subscript && MP_UNITS_COMP_GCC != 12
if constexpr (requires(const T& t) { t[Is...]; })
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()[Is...])>>{};
else
if constexpr (requires(const T& t) { t[std::size_t{}, std::size_t{}]; })
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()[std::size_t{}, std::size_t{}])>>{};
else
#endif
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()(Is...))>>{};
}
return std::type_identity<std::remove_cvref_t<decltype(std::declval<const T&>()(std::size_t{}, std::size_t{}))>>{};
}
template<typename T>
using element_type_of_t =
typename decltype(element_type_of<T>(std::make_index_sequence<detect_tensor_order<T>()>{}))::type;
requires has_matrix_indexing<T>
using matrix_element_t = typename decltype(matrix_element<T>())::type;
template<typename T>
concept field_reachable =
specified<decltype(tensor_order<T>)> && (tensor_order<T> == 0 || (tensor_order<T> == 1 && has_vector_indexing<T>) ||
(tensor_order<T> == 2 && has_matrix_indexing<T>));
template<typename T>
requires field_reachable<T>
[[nodiscard]] consteval quantity_field detect_numeric_field()
{
if constexpr (detect_tensor_order<T>() >= 1)
return detect_numeric_field<element_type_of_t<T>>();
if constexpr (tensor_order<T> == 1)
return detect_numeric_field<vector_element_t<T>>();
else if constexpr (tensor_order<T> == 2)
return detect_numeric_field<matrix_element_t<T>>();
else if constexpr (requires(const T& v) {
::mp_units::real(v);
::mp_units::imag(v);
@@ -359,13 +371,16 @@ template<typename T>
} // namespace detail
// The numeric field of a type: real or complex.
// Field detection reads the field off a scalar *element*, never off a container's surface, and that is
// why it consults the order. A linear-algebra vector or matrix (Eigen, Blaze) exposes `real()`/`imag()`
// even when it is real, so trusting that API at the container level would misclassify a real matrix as
// complex.
// The numeric field of a type: real or complex. The primary is left *undefined*; a specialization
// defines it for a type whose order is known, reading the field off a scalar *element* (never off a
// container's surface, since a linear-algebra vector or matrix exposes `real()`/`imag()` even when it
// is real).
MP_UNITS_EXPORT template<typename T>
constexpr quantity_field numeric_field = detail::detect_numeric_field<T>();
constexpr detail::undefined_t numeric_field;
template<typename T>
requires detail::field_reachable<T>
constexpr quantity_field numeric_field<T> = detail::detect_numeric_field<T>();
/////////////// disable_representation ///////////////
@@ -274,8 +274,7 @@ template<PointOrigin PO>
template<PointOrigin PO>
constexpr bool has_quantity_bounds_v = [] {
if constexpr (PO::_quantity_spec_.character == quantity_character{}) {
if constexpr (requires { PO::_bounds_; } &&
!std::is_same_v<std::remove_cvref_t<decltype(PO::_bounds_)>, undefined_t>) {
if constexpr (requires { PO::_bounds_; } && specified<decltype(PO::_bounds_)>) {
static_assert(
requires { PO::_bounds_.min; } || requires { PO::_bounds_.max; },
"bounds policy must have at least a 'min' or 'max' member");
@@ -153,9 +153,9 @@ concept QuantityPointLike = !QuantityPoint<T> && detail::QuantityLikeImpl<T, qua
namespace detail {
template<auto From, auto To>
concept HasFrameProjection = AbsolutePointOrigin<MP_UNITS_REMOVE_CONST(decltype(From))> &&
AbsolutePointOrigin<MP_UNITS_REMOVE_CONST(decltype(To))> &&
!std::is_same_v<std::remove_cvref_t<decltype(frame_projection<From, To>)>, undefined_t>;
concept HasFrameProjection =
AbsolutePointOrigin<MP_UNITS_REMOVE_CONST(decltype(From))> &&
AbsolutePointOrigin<MP_UNITS_REMOVE_CONST(decltype(To))> && specified<decltype(frame_projection<From, To>)>;
} // namespace detail
+34 -5
View File
@@ -427,10 +427,12 @@ static_assert(!RepresentationOf<utility::cartesian_vector<quantity<si::metre>>,
static_assert(!RepresentationOf<std::chrono::seconds, quantity_tensor_order::scalar>);
static_assert(!RepresentationOf<std::string, quantity_tensor_order::scalar>);
// `tensor_order` is detected from a type's structure: one-index access is a vector (order 1),
// two-index access a tensor (order 2), otherwise a scalar (order 0). A type with two-index access
// that is conceptually a vector (an Eigen N×1 column matrix) therefore defaults to order 2 and must
// declare order 1 by specializing `tensor_order` (the Eigen integration does exactly that).
// `tensor_order` is detected from a type's structure: single-index access `t[i]` is a vector
// (order 1), two-index access `t(i, j)` a tensor (order 2), otherwise a scalar (order 0). A type
// that exposes *both* shapes (an Eigen N×1 column matrix models a vector yet also offers `t(i, j)`)
// is ambiguous and has no default; it must specialize `tensor_order` (the Eigen integration does).
// The single-shape types below classify without a specialization; the ambiguous both-shape case
// (`ambiguous_shaped`) is checked afterwards.
namespace order_detection {
struct scalar_shaped {};
struct vector_shaped {
@@ -439,8 +441,18 @@ struct vector_shaped {
struct matrix_shaped {
double operator()(std::size_t, std::size_t) const;
};
// Exposes *both* indexing styles (an N x 1 column matrix, as Eigen models a vector). Its order is
// ambiguous, so the primary `tensor_order` is left undefined for it - it has no default.
struct ambiguous_shaped {
double operator[](std::size_t) const;
double operator()(std::size_t, std::size_t) const;
};
// Whether `tensor_order<T>` yields a usable value. For the ambiguous case the primary is left
// undefined, so this is `false` via a substitution failure (SFINAE-friendly), not a hard error.
template<typename T>
concept order_defined = (tensor_order<T> < 3);
// GCC 12 prematurely defines `__cpp_multidimensional_subscript` without implementing `t[i, j]`, so
// the library skips that probe there (see `detect_tensor_order`); keep this test in step.
// the library skips that probe there (see `has_matrix_indexing`); keep this test in step.
#if __cpp_multidimensional_subscript && MP_UNITS_COMP_GCC != 12
struct multidim_subscript_shaped {
double operator[](std::size_t, std::size_t) const;
@@ -453,6 +465,10 @@ static_assert(tensor_order<order_detection::matrix_shaped> == 2);
#if __cpp_multidimensional_subscript && MP_UNITS_COMP_GCC != 12
static_assert(tensor_order<order_detection::multidim_subscript_shaped> == 2); // C++23 t[i, j]
#endif
// Single-shape types have a defined order; the ambiguous both-shape type does not (soft-rejected).
static_assert(order_detection::order_defined<order_detection::vector_shaped>);
static_assert(order_detection::order_defined<order_detection::matrix_shaped>);
static_assert(!order_detection::order_defined<order_detection::ambiguous_shaped>);
// The legacy flat spelling still selects the right (order, field): `vector` -> (vector, real),
// `complex_scalar` -> (scalar, complex), etc. (converted at a function argument in `order_of` /
@@ -722,3 +738,16 @@ static_assert(detail::UsesIntegerScaling<utility::cartesian_vector<int>>);
#endif
} // namespace
// A full explicit specialization of `tensor_order` for an ambiguous type is permitted by the
// undefined primary (a constrained-out primary would reject `template<>` with "does not match any
// declaration"). At global scope, which encloses `mp_units`, so it can specialize the trait.
namespace order_spec_test {
struct ambiguous {
double operator[](std::size_t) const;
double operator()(std::size_t, std::size_t) const;
};
} // namespace order_spec_test
template<>
constexpr std::size_t mp_units::tensor_order<order_spec_test::ambiguous> = 1;
static_assert(mp_units::tensor_order<order_spec_test::ambiguous> == 1);