diff --git a/src/core/include/mp-units/framework/customization_points.h b/src/core/include/mp-units/framework/customization_points.h index 47befb9ae..8fa89e6e8 100644 --- a/src/core/include/mp-units/framework/customization_points.h +++ b/src/core/include/mp-units/framework/customization_points.h @@ -66,9 +66,11 @@ namespace detail { * templates cannot be "deleted" like functions can. */ struct undefined_t {}; - inline constexpr undefined_t undefined{}; +template +concept specified = !std::same_as, undefined_t>; + template 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 -[[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 +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 +concept has_ambiguous_order = has_vector_indexing && has_matrix_indexing; + } // 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 -constexpr std::size_t tensor_order = detail::detect_tensor_order(); +constexpr detail::undefined_t tensor_order; + +template + requires(!detail::has_ambiguous_order) +constexpr std::size_t tensor_order = detail::has_matrix_indexing ? std::size_t{2} + : detail::has_vector_indexing ? std::size_t{1} + : std::size_t{0}; /////////////// numeric_field /////////////// namespace detail { -template - requires(sizeof...(Is) >= 1) -[[nodiscard]] consteval auto element_type_of(std::index_sequence) +template + requires has_vector_indexing +using vector_element_t = std::remove_cvref_t()[std::size_t{}])>; + +template + requires has_matrix_indexing +[[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::size_t{}])>>{}; - else - return std::type_identity()(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()[Is...])>>{}; - else + if constexpr (requires(const T& t) { t[std::size_t{}, std::size_t{}]; }) + return std::type_identity()[std::size_t{}, std::size_t{}])>>{}; + else #endif - return std::type_identity()(Is...))>>{}; - } + return std::type_identity()(std::size_t{}, std::size_t{}))>>{}; } template -using element_type_of_t = - typename decltype(element_type_of(std::make_index_sequence()>{}))::type; + requires has_matrix_indexing +using matrix_element_t = typename decltype(matrix_element())::type; template +concept field_reachable = + specified)> && (tensor_order == 0 || (tensor_order == 1 && has_vector_indexing) || + (tensor_order == 2 && has_matrix_indexing)); + +template + requires field_reachable [[nodiscard]] consteval quantity_field detect_numeric_field() { - if constexpr (detect_tensor_order() >= 1) - return detect_numeric_field>(); + if constexpr (tensor_order == 1) + return detect_numeric_field>(); + else if constexpr (tensor_order == 2) + return detect_numeric_field>(); else if constexpr (requires(const T& v) { ::mp_units::real(v); ::mp_units::imag(v); @@ -359,13 +371,16 @@ template } // 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 -constexpr quantity_field numeric_field = detail::detect_numeric_field(); +constexpr detail::undefined_t numeric_field; + +template + requires detail::field_reachable +constexpr quantity_field numeric_field = detail::detect_numeric_field(); /////////////// disable_representation /////////////// diff --git a/src/core/include/mp-units/framework/quantity_point.h b/src/core/include/mp-units/framework/quantity_point.h index 6b4119030..411084a11 100644 --- a/src/core/include/mp-units/framework/quantity_point.h +++ b/src/core/include/mp-units/framework/quantity_point.h @@ -274,8 +274,7 @@ template template constexpr bool has_quantity_bounds_v = [] { if constexpr (PO::_quantity_spec_.character == quantity_character{}) { - if constexpr (requires { PO::_bounds_; } && - !std::is_same_v, undefined_t>) { + if constexpr (requires { PO::_bounds_; } && specified) { static_assert( requires { PO::_bounds_.min; } || requires { PO::_bounds_.max; }, "bounds policy must have at least a 'min' or 'max' member"); diff --git a/src/core/include/mp-units/framework/quantity_point_concepts.h b/src/core/include/mp-units/framework/quantity_point_concepts.h index c19f1fce7..19c4e083e 100644 --- a/src/core/include/mp-units/framework/quantity_point_concepts.h +++ b/src/core/include/mp-units/framework/quantity_point_concepts.h @@ -153,9 +153,9 @@ concept QuantityPointLike = !QuantityPoint && detail::QuantityLikeImpl -concept HasFrameProjection = AbsolutePointOrigin && - AbsolutePointOrigin && - !std::is_same_v)>, undefined_t>; +concept HasFrameProjection = + AbsolutePointOrigin && + AbsolutePointOrigin && specified)>; } // namespace detail diff --git a/test/static/concepts_test.cpp b/test/static/concepts_test.cpp index 2891f6314..897e8c883 100644 --- a/test/static/concepts_test.cpp +++ b/test/static/concepts_test.cpp @@ -427,10 +427,12 @@ static_assert(!RepresentationOf>, static_assert(!RepresentationOf); static_assert(!RepresentationOf); -// `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` 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 +concept order_defined = (tensor_order < 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 == 2); #if __cpp_multidimensional_subscript && MP_UNITS_COMP_GCC != 12 static_assert(tensor_order == 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); +static_assert(order_detection::order_defined); +static_assert(!order_detection::order_defined); // 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>); #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 = 1; +static_assert(mp_units::tensor_order == 1);