Skip to content

Decompose a Vector Quantity into Components

This guide shows how to split a vector quantity into named, strongly-typed 1D-vector component quantities, reachable by index (get<Idx>), by quantity spec (get<QS>), and through structured bindings, and how to build the whole back up from those components. It covers how the quantity hierarchy must be formed, what the representation must provide, and the tuple_size / tuple_element protocol for compile-time code. Naming the axes is optional, and the guide also covers what a vector quantity that does not name them can still do.

A vector quantity stores one physical vector, such as a drone's velocity, under a single unit. Sooner or later you need to talk about its individual axes: the forward speed, the lateral drift, the rate of climb. The unsafe way is to reach into the representation and pull out v[2], which gives you a bare number with no unit and no idea which axis it came from. Swap two indices and the compiler says nothing.

For background on the concepts used here, see:

The Problem: Components Lose Their Types

Consider a drone's body-frame velocity, modeled as a named flight_velocity quantity (defined in full below) stored as a vector quantity:

const quantity v = flight_velocity(cartesian_vector{30.0, 10.0, -2.0} * km / h);

The whole is type-safe. Suppose you want its third axis. The representation is indexable, so you can reach through to it:

const auto vertical = v.numerical_value_in(km / h)[2];   // a bare double: no unit, no axis

vertical is now a plain double. The fact that it is the drone's rate of climb in km/h lives only in a comment, and nothing stops you from adding it to the forward speed.

A quantity::operator[] would not rescue this. At best v[2] could return the value tagged with the whole's reference, never the specific axis kind, because a runtime index is not known at compile time, so it cannot select between forward_velocity, lateral_velocity, and vertical_velocity. The one piece of information you actually want, which axis this is, is exactly what a runtime subscript cannot carry. That is why the library does not provide quantity::operator[], and why the access shown below selects the axis at compile time instead.

The Core Idea: Name the Axes

The fix is to give each axis its own quantity spec and tell the library how the whole decomposes into them. Access then returns a 1D-vector component quantity of the right axis type, in the whole's unit, with no manual indexing:

const auto [forward, lateral, vertical] = v;   // three typed component quantities

Each binding is a quantity carrying its axis spec and unit. Because each axis is a distinct kind, the type system now rejects the combinations that never made physical sense, while same-axis arithmetic still works:

const quantity total = forward + forward;    // OK: two forward velocities are the same kind
const quantity wrong = forward + vertical;   // error: a forward velocity is not a rate of climb
const bool aligned = forward == vertical;    // error: distinct kinds are not even comparable

Forming the Quantity Hierarchy

Decomposition rests entirely on how you model the quantities. The whole and its axes must satisfy a small set of rules, all checked at compile time:

  1. The whole is a vector quantity. Its quantity spec has vector character.
  2. Each axis is a vector quantity as well. A component of a 3D vector is a 1D vector, not a scalar, so the axes keep vector character.
  3. Every axis shares the whole's hierarchy root. All axes, and the whole, descend from the same ancestor (here isq::velocity). This is what makes them the same kind of physical quantity.
  4. Each axis is a distinct kind. Declaring an axis with is_kind starts a new kind, so the forward, lateral, and vertical velocities cannot be substituted for one another.
  5. Each axis differs in kind from the whole. The whole stays a plain child of the root, while the axes branch off as their own kinds.

Applied to the drone:

// the whole: a plain child of isq::velocity (vector character, same kind as velocity)
inline constexpr struct flight_velocity : quantity_spec<isq::velocity> {} flight_velocity;

// the axes: each its own kind, all sharing the isq::velocity hierarchy root
inline constexpr struct forward_velocity : quantity_spec<isq::velocity, is_kind> {} forward_velocity;
inline constexpr struct lateral_velocity : quantity_spec<isq::velocity, is_kind> {} lateral_velocity;
inline constexpr struct vertical_velocity : quantity_spec<isq::velocity, is_kind> {} vertical_velocity;

These sit in the isq::speed kind as follows. The whole flight_velocity is a plain child of velocity, so it stays in the speed kind. Each axis branches off as a kind of its own, drawn as a separate box reached by a dashed edge, the same way the User's Guide marks an is_kind boundary:

flowchart TD
    speed["<b>speed</b><br><i>(length / time)</i><br>[m/s]"]
    speed --- velocity["<b>velocity</b><br><i>(displacement / duration)</i><br>{vector}"]
    velocity --- flight_velocity["<b>flight_velocity</b>"]
    subgraph kind_vertical[" "]
        vertical_velocity["<b>vertical_velocity</b>"]
    end
    subgraph kind_lateral[" "]
        lateral_velocity["<b>lateral_velocity</b>"]
    end
    subgraph kind_forward[" "]
        forward_velocity["<b>forward_velocity</b>"]
    end
    velocity -.- forward_velocity
    velocity -.- lateral_velocity
    velocity -.- vertical_velocity

The three axes are siblings of flight_velocity in the type hierarchy, not its children. The decomposition that pairs them with the whole is the component_vectors specialization below, not an inheritance relationship.

Why is_kind on the axes

Without is_kind, all three axes would still belong to the isq::velocity kind, and the library would let you add a forward velocity to a vertical one. is_kind makes each axis a kind of its own, so cross-axis arithmetic is rejected, while same-axis arithmetic (adding two forward velocities, for example) still works. See Systems of Quantities for the full semantics.

Declaring the Decomposition

Opt the whole into decomposition by specializing the component_vectors customization point and inheriting from vector_axes, listing the axes in coordinate order (axis 0 first):

template<>
struct mp_units::component_vectors<flight_velocity> :
    mp_units::vector_axes<forward_velocity, lateral_velocity, vertical_velocity> {};

vector_axes enforces the axis-intrinsic rules (2, 3, and 4 above) at the point of definition: an axis list that mixes characters, spans different hierarchy roots, or repeats a kind is ill-formed right here. The rules that also need the whole and the representation (1 and 5, plus the representation requirements below) are checked when you actually decompose a quantity.

Accessing Components

By index

get<Idx> returns the component at coordinate Idx as a 1D-vector quantity of that axis's spec:

const quantity forward = get<0>(v);   // quantity<forward_velocity[km/h], double>

The index is bounds-checked against the number of declared axes. get<3>(v) does not compile for a three-axis decomposition.

Why get<Idx> and not operator[]?

get<Idx> takes the index as a compile-time template argument, so each call has a precise, distinct return type (get<0> a forward velocity, get<2> a vertical velocity). A runtime operator[] cannot, for the reason shown in The Problem above. This mirrors the standard library. std::tuple is indexed only through std::get<I> (its elements have distinct types, like the axes here), and even the homogeneous std::array adds std::get<I> for the compile-time bounds checking that its operator[] cannot provide. get<Idx> here delivers both at once.

By quantity spec

When you know the axis but not its position, index by the spec instead. This is order-independent and reads better at the call site:

const quantity climb = get<vertical_velocity>(v);   // same as get<2>(v) here

Passing a spec that is not one of the declared axes (get<isq::acceleration>(v)) does not compile.

Through structured bindings

Because a decomposable quantity models the tuple protocol, structured bindings work directly and are usually the most readable option:

const auto [forward, lateral, vertical] = v;

Each name is bound to a quantity of the corresponding axis spec, copied out of the whole.

A Component Is a 1D Vector

All of the above (get<Idx>, get<QS>, and structured bindings) yield a 1D-vector quantity, not a scalar: a component keeps the whole's vector character, with the vector's element type (such as double) as its representation. It holds a single value, so read it with numerical_value_in(unit):

const double climb_rate = get<vertical_velocity>(v).numerical_value_in(km / h);

Constructing From Components

Decomposition has an inverse: the whole can be built from its component quantities, each checked against the axis declared for its position.

const quantity<flight_velocity[km / h], cartesian_vector<double>> v = {forward_velocity(30 * km / h),
                                                                       lateral_velocity(10 * km / h),
                                                                       vertical_velocity(-2 * km / h)};

Getting the order wrong does not compile. Compare that with the alternative:

// error: position 1 wants a lateral velocity, and a vertical velocity is a different kind
// auto swapped = quantity<flight_velocity[km / h], cartesian_vector<double>>{forward_velocity(30 * km / h),
//                                                                            vertical_velocity(-2 * km / h),
//                                                                            lateral_velocity(10 * km / h)};

// compiles, and silently means something else: the drone now climbs at 10 km/h and drifts by -2
const quantity oops = flight_velocity(cartesian_vector{30.0, -2.0, 10.0} * km / h);

The second line is the spelling you are stuck with when the components are bare numbers in positional order. Nothing in it is wrong as far as the type system can tell, and naming the axes is what changes that.

Each argument has to be implicitly convertible to the quantity spec declared for its position. A plain number is not, and neither is a quantity of the whole's own spec, since isq::velocity converts to an axis only explicitly:

// error: neither names an axis of this decomposition
// auto by_number = quantity<flight_velocity[km / h], cartesian_vector<double>>{30.0, 10.0, -2.0};
// auto by_whole = quantity<flight_velocity[km / h], cartesian_vector<double>>{isq::velocity(30 * km / h),
//                                                                             isq::velocity(10 * km / h),
//                                                                             isq::velocity(-2 * km / h)};

Simple quantities forfeit the order check

A quantity kind converts into every quantity of its hierarchy, so 30 * km / h fits every axis and any order of such arguments compiles:

// compiles: a kind quantity names no position
const quantity<flight_velocity[km / h], cartesian_vector<double>> unchecked = {30 * km / h,
                                                                               10 * km / h,
                                                                               -2 * km / h};

This is the same trade the library offers everywhere: reaching for a simple quantity opts out of kind safety, and this constructor does not second-guess that choice. Pass axis-typed components when you want the positions checked.

The type being constructed pins the unit, so each argument is converted into it under the ordinary conversion rules, and a truncating argument is caught the same way it would be anywhere else:

// 10 m/s is converted into the km/h this type stores
const quantity<flight_velocity[km / h], cartesian_vector<double>> mixed = {forward_velocity(30 * km / h),
                                                                           lateral_velocity(10 * m / s),
                                                                           vertical_velocity(-2 * km / h)};

There is no CTAD for this: neither the whole nor the representation can be deduced from the components, since an axis list has no reverse mapping back to a whole and the library has no opinion about which vector type you want. Naming the type is where both come from.

From one value holding the components

When the components already travel together, in a std::tuple, a std::array, or equivalent containers, pass that one value instead of unpacking it by hand:

using velocity = quantity<flight_velocity[km / h], cartesian_vector<double>>;

const velocity from_tuple = std::tuple{forward_velocity(30 * km / h),
                                       lateral_velocity(10 * km / h),
                                       vertical_velocity(-2 * km / h)};

Any type that a structured binding decomposes through std::tuple_size is accepted, and its elements are read the way such a binding reads them: through a member get<Idx>() when the type has one, and otherwise through a get<Idx> found by argument-dependent lookup. A third-party vector therefore works as well as the standard types. Since a decomposable quantity decomposes the same way, a typed vector built from one composes back to it.

The rules are those of the separate arguments, element by element. The count must match, each element must be implicitly convertible to the component quantity of its position (so a value with two axes swapped does not compile), and each element is converted into the unit the type pins, with truncation rejected as anywhere else. The conversion is implicit for the same reason the separate-argument form is.

Three kinds of such arguments are deliberately not composed element by element. A quantity converts as a whole, under the quantity specification rules, rather than as its components. A type registered through quantity_like_traits converts as a whole through those traits, as it does when it does not decompose. A value convertible to the representation, such as a cartesian_vector, keeps going through the value constructors.

std::make_from_tuple and std::apply cannot do this job. Since C++23 they accept only the standard library's own tuple-like types and call std::get, so a third-party vector is rejected, and generic code is left writing its own index_sequence helper.

Working Without Naming the Axes

Declaring the axes is optional. A vector quantity whose spec has no component_vectors specialization still takes apart and goes back together, and each component then carries the whole's kind:

const quantity v = isq::velocity(cartesian_vector{30.0, 10.0, -2.0} * km / h);

const quantity forward = get<0>(v);           // a 1D-vector quantity of the isq::speed kind
const auto [x, y, z] = v;                     // three components of that same kind

const quantity<isq::velocity[km / h], cartesian_vector<double>> back = {x, y, z};   // and back again

Both directions stay available, and both check less. Everything that needs an axis to refer to is gone: access by quantity spec, and the positional checking of the constructor's arguments. Index access, structured bindings, the tuple protocol, and component-wise construction remain, with the count read from the representation and each argument converted into the unit the type pins.

This mode is weaker than a declared decomposition, and deliberately so. All the components share one kind, so what a declared decomposition rejects is accepted here in both directions:

const quantity nonsense = get<0>(v) + get<1>(v);   // compiles, and means nothing physically

// compiles: with one kind for every position, no order is more correct than another
const quantity<isq::velocity[km / h], cartesian_vector<double>> reordered = {z, x, y};

It is still a strict improvement on the alternative it replaces, which is reaching into the representation by hand:

const double forward = v.numerical_value_in(km / h)[0];   // no unit, no quantity, no bound

The undeclared mode keeps the unit, the character, the component count, and the compile-time bound. Naming the axes adds the one thing it cannot have: a distinct type per axis, and with it the positional safety in both directions.

Why the whole's kind rather than the whole's own spec?

Because a component is not the whole. An undeclared decomposition knows only that each component belongs to the whole's kind, and handing back isq::velocity would claim more than it knows: a single component could then stand in wherever the complete vector quantity is expected.

The kind does not cost the component its character, even though the kind of a vector quantity is rooted in the scalar one it descends from:

static_assert(get_kind(isq::velocity) == get_kind(isq::speed));

A kind admits every character and leaves the choice to the representation, so the vector character that a component keeps survives.

Note get_kind(isq::velocity) and not kind_of<isq::velocity>. The latter is ill-formed, because kind_of requires its argument to be its own kind-tree root, which a vector spec is not.

What the Representation Must Satisfy

Decomposition reuses the same representation that already backs the vector quantity. On top of the normal vector representation requirements (copyable, bool-returning equality, a Euclidean norm, and a value_type), the type must be indexable by a compile-time constant index: Idx is a template argument, not a runtime value, which is what lets each axis have its own return type. The access itself need not be constexpr. The library reads a member get first, as a structured binding does, and falls back to a subscript:

  • a member get<Idx>(), when the type has one,
  • otherwise a get<Idx>(rep) found by argument-dependent lookup,
  • otherwise a subscript rep[Idx].

It must also state how many components it holds, through std::tuple_size. That is where the library reads the length of an undeclared decomposition, and a declared decomposition must name exactly that many axes.

cartesian_vector, Eigen::Vector3d, glm::dvec3, and blaze::StaticVector<double, 3> all satisfy both, so decomposition works against every backend the linear algebra example uses. cartesian_vector and Blaze provide the tuple protocol themselves, while the Eigen and GLM plugins map each library's own compile-time shape onto it.

A representation with no compile-time length, such as Eigen::VectorXd or blaze::DynamicVector<double>, is deliberately left out. A compile-time index cannot be bounds-checked against a length that only exists at run time.

The component quantity's representation is the vector's element type, its value_type. A component of a cartesian_vector<double> is therefore a quantity<…, double>: a 1D-vector quantity holding a single value.

The axes match the representation exactly

A declared decomposition names one axis per element of the representation, so a cartesian_vector<double> of three elements needs three axes. A representation that holds more elements than there are axes is not decomposable. Building it from its component vectors would leave the remaining elements to the representation's constructor, and Eigen::Vector3i reads two integers in a braced list as a shape.

Compile-Time Introspection

The tuple protocol is available for your own compile-time logic, not only for structured bindings. std::tuple_size_v gives the axis count and std::tuple_element_t gives a component's type:

static_assert(std::tuple_size_v<decltype(v)> == 3);

using climb_t = std::tuple_element_t<2, decltype(v)>;
static_assert(QuantityOf<climb_t, vertical_velocity>);

std::tuple_element_t is bounded the same way get is: an out-of-range index is not a valid specialization, so it never silently yields a wrong type.

What Does Not Compile

Beyond the cross-axis arithmetic shown in The Core Idea, access itself is constrained. An out-of-range index and a spec that is not one of the declared axes are both rejected at compile time:

get<3>(v);                  // error: only three axes exist
get<isq::acceleration>(v);  // error: not one of the declared axes

See Also