Unit Conversion and Dimensional Analysis Library 3.6.1
A compile-time, header-only C++23 dimensional-analysis library
Loading...
Searching...
No Matches
Internals: how named unit types are built

This page explains internals. To use the library — including defining your own units — you do not need any of this; see defining new units. This document is the reference for unit authors and contributors: how the readable-diagnostics machinery is built. Nothing here is required to write correct code with the library.

The 3.x line produces readable compiler diagnostics: an error names meters<double>, not unit<conversion_factor<std::ratio<1>, dimension_t<...>>, double, linear_scale>. That naming is not cosmetic string formatting — it falls out of a type-design decision (named units are classes, not aliases) supported by a trait layer that keeps the name attached as values flow through construction, conversion, and arithmetic. This page walks that machinery.

All line references are to include/units/core.h unless noted.

1. Named units are classes, not aliases

The pivotal decision: a named unit such as meters is a class template deriving from its unit<...>, generated by UNIT_ADD_SCALED_UNIT_DEFINITION (core.h:211):

template<class Underlying = UNIT_LIB_DEFAULT_TYPE>
struct unitName : ::units::unit<traits::strong_t<__VA_ARGS__>, Underlying, scale>
{
using base = ::units::unit<traits::strong_t<__VA_ARGS__>, Underlying, scale>;
using base::base;
// ... defaulted special members, a scalar operator=, and a `rebind` alias ...
template<class NewUnderlying>
using rebind = unitName<NewUnderlying>;
};
Definition core.h:2735

In 2.x a named unit was an alias template (using meters = unit<...>). An alias is transparent: the compiler sees straight through it to the aliased type, so a diagnostic printed the full unit<...> spelling. A class is opaque — it has its own name — so the compiler prints meters<double>. This is the entire reason the type is a class. The class adds no data members and no behavior beyond what it inherits; it exists to be a name.

UNIT_ADD (core.h:360) composes the full definition: the strong conversion factor (UNIT_ADD_STRONG_CONVERSION_FACTOR), the class (UNIT_ADD_UNIT_DEFINITIONUNIT_ADD_SCALED_UNIT_DEFINITION), the name/abbreviation traits, the reverse-map registration (UNIT_REGISTER_NAMED_CLASS), the literals, and the constant.

Note: the pure dimensionless unit (dimensionless, ratio 1) is the one exception — it stays a plain alias to unit<...> (core.h:3230), because it must remain identity-equal to its base and fully interchangeable with int/double. Named ratio-dimensionless units (percent, ppm, …) are still classes: they carry a meaningful name.

2. Deduction guides make the bare name work

Because a named unit is now a class template, writing the bare name requires class template argument deduction (CTAD). An alias template resolved a bare meters through its default template argument; a class template does not do that on its own on every compiler (GCC 13, for instance, will not deduce from an arithmetic argument without help). UNIT_ADD_SCALED_UNIT_DEFINITION therefore emits four deduction guides (core.h:248268):

template<class Arg> requires ::std::is_arithmetic_v<Arg>
unitName(Arg) -> unitName<Arg>; // meters(5.0) -> meters<double>
unitName() -> unitName<UNIT_LIB_DEFAULT_TYPE>; // meters{} -> meters<default>
template<class OtherUnit> requires (/* same-dimension unit */)
unitName(const OtherUnit&) -> unitName</* deduced underlying */>; // meters(feet{3.0})
template<class Rep, class Period>
unitName(const ::std::chrono::duration<Rep, Period>&) -> unitName<UNIT_LIB_DEFAULT_TYPE>; // time units

Each guide is constrained so it never competes with another: the arithmetic guide only fires for scalars, the from-unit guide only for same-dimension units, the chrono guide only for std::chrono::duration. The from-unit guide's deduced underlying is computed by detail::deduced_named_underlying_t (core.h:2293): the source's own underlying when the conversion is lossless, otherwise its floating-point promotion — so radians(degrees{1}) deduces radians<double> (the degrees→radians conversion is not integer-lossless), while a lossless same-underlying conversion keeps the source's type.

#include <units/length.h>
#include <units/angle.h>
#include <units/time.h>
#include <chrono>
#include <type_traits>
int main()
{
auto a = units::length::meters(5); // meters<int>
auto b = units::length::meters(5.0); // meters<double>
units::length::meters d{}; // meters<double>
auto r = units::angle::radians(units::angle::degrees{1}); // radians<double>
auto n = units::time::nanoseconds(std::chrono::nanoseconds(10)); // nanoseconds<double>
static_assert(std::is_same_v<decltype(a), units::length::meters<int>>);
static_assert(std::is_same_v<decltype(b), units::length::meters<double>>);
static_assert(std::is_same_v<decltype(d), units::length::meters<double>>);
static_assert(std::is_same_v<decltype(r), units::angle::radians<double>>);
static_assert(std::is_same_v<decltype(n), units::time::nanoseconds<double>>);
return 0;
}
units representing length values
units representing time values

3. strong_name: an ADL overload set, not a specialization (the #357 fix)

The unit's first template parameter is the strong conversion factor — the named tag such as meters_ rather than a raw conversion_factor<...>. traits::strong_t<Cf> (core.h:916) resolves a conversion_factor to its registered strong type. The resolution mechanism is central to the design; this section describes how it is built.

traits::strong<T> (core.h:905) does not specialize a trait. It performs an unqualified call to a function strong_name and takes its return type:

template<ConversionFactorType T> requires std::is_same_v<T, std::remove_cv_t<T>>
struct strong { using type = decltype(strong_name(static_cast<T*>(nullptr))); };

There is a fallback overload (detail::strong_name, core.h:756) that is the worst match — it deduces the conversion factor from its pointer argument and has a trailing ellipsis:

template<class ConversionFactor>
ConversionFactor strong_name(ConversionFactor*, ...);

Each dimension header, via UNIT_ADD_STRONG_CONVERSION_FACTOR (core.h:164), declares a better-matching overload — an exact-pointer parameter with no ellipsis — that returns the named strong type:

namespace detail { ::units::namespaceName::namePlural##_ strong_name(__VA_ARGS__*); }

The call is unqualified so that argument-dependent lookup (ADL) finds every such overload: a conversion_factor's associated namespaces are units and units::detail, exactly where the registrations live. Overload resolution then picks the exact-parameter registration when its dimension header is visible, and the ellipsis fallback (identity) otherwise. All of these functions are declared, never defined — they are used only inside decltype.

Design rationale (#357 — "explicit specialization after instantiation"): the earlier design used an explicit specialization of a trait to register a strong type. That is order-sensitive: once strong<Cf> is implicitly instantiated, a later-included header that specializes it for the same Cf is ill-formed ("explicit specialization after instantiation"). An overload set has no such rule. A header included later merely contributes another candidate to strong_name; overload resolution re-runs at each instantiation point and finds the better match. There is no "declared after instantiation" trap because nothing is being specialized — this is the structural fix for issue #357, and it is why an expression that reduces to a not-yet-included dimension still compiles cleanly and then names the named type once the dimension header is in scope.

4. The trait layer that preserves the name

A named unit is a derived class. The exact-pattern trait specializations that the library relies on — std::common_type, replace_underlying, floating_point_promotion — are written against the literal pattern unit<Cf, T, Ns>, which a derived class does not match. Without help, every trait result would decay to the plain unit<...> base and the name would be lost the moment you did arithmetic. Four pieces keep it attached.

4.1 unit_base_t — recover the canonical base

detail::unit_base_t<T> (core.h:2753) reconstructs the canonical unit<Cf, U, Ns> from any unit type's inherited member typedefs:

template<class T>
using unit_base_t = unit<typename T::conversion_factor, typename T::underlying_type, typename T::numerical_scale_type>;

For a named unit this yields its base; for a plain unit<...> it is the identity. The exact-pattern traits use it to unwrap first, so they work uniformly for named and plain units.

4.2 is_named_unit_v — is this a named class?

detail::is_named_unit_v<T> (core.h:2768) is true exactly when T is a unit that is not its own canonical base — i.e. a derived, named class. It is guarded on traits::is_unit first so a plain arithmetic type (which has no conversion_factor) yields false rather than a hard error (core.h:2760).

4.3 rewrap_to_named_t — forward map, via named_class_of

Arithmetic operators compute a plain unit<Cf, U, Ns> result (e.g. multiplying two meters gives a unit<square_meters_, …>). To report that result under its named type, detail::rewrap_to_named_t (core.h:2801) maps a conversion factor forward to its named class using a reverse-map ADL function, detail::named_class_of. UNIT_REGISTER_NAMED_CLASS (core.h:300) emits one registration per named unit:

namespace detail {
::units::namespaceName::namePlural<UNIT_LIB_DEFAULT_TYPE> named_class_of(
typename ::units::namespaceName::namePlural<>::conversion_factor*,
typename ::units::namespaceName::namePlural<>::numerical_scale_type*);
}

Note (keyed on conversion factor and scale): the map takes two pointer parameters — the conversion factor and the numerical scale. This is required because the linear and decibel forms of a unit share one conversion factor: watts and dBW are both watts_, differing only by scale (core.h:303). Keying the reverse map on the conversion factor alone would make watts and dBW collide. Adding the scale as a second key disambiguates them, which is also why UNIT_ADD_DECIBEL registers its own class and UNIT_REGISTER_NAMED_CLASS is applied to both the linear and decibel names independently.

Like strong_name, named_class_of is an ADL overload set with an identity fallback (core.h:2792, returning void to signal "no named class for this CF"), declared and never defined. The forward map rebinds the registered class to the result's underlying type so the storage type flows through (core.h:2806). The unit's own name() and abbreviation() members go through rewrap_to_named_t first (core.h:2715, core.h:2726) so a named unit reports its name instead of the base's null.

4.4 rewrap_named and the common_type specialization

For std::common_type, detail::rewrap_named_t<Base, Named> (core.h:2775) re-wraps a computed base result into a candidate operand's named type when they share a conversion factor (via the class's rebind). The std::common_type specialization for named units (core.h:3014) computes the common type of the canonical bases, then tries to re-wrap it into the left operand's name, then the right's — so common_type<meters<int>, meters<double>> is meters<double>, not the plain unit<...>. It is SFINAE-constrained to fire only when both are units, at least one is named, and a base common type actually exists — matching the plain unit<...> behavior for mismatched dimensions (no type member).

replace_underlying (core.h:2822) and floating_point_promotion (core.h:2834) get analogous named-unit specializations that rebind the name to the new underlying instead of decaying to the base.

#include <units/length.h>
#include <units/power.h>
#include <type_traits>
int main()
{
using units::length::meters;
// named class, not the same type as its unit<...> base:
static_assert(!std::is_same_v<meters<double>, units::detail::unit_base_t<meters<double>>>);
static_assert(std::is_base_of_v<units::detail::unit_base_t<meters<double>>, meters<double>>);
static_assert(units::detail::is_named_unit_v<meters<double>>);
static_assert(!units::detail::is_named_unit_v<double>);
// the name survives common_type and rebind:
static_assert(std::is_same_v<std::common_type_t<meters<int>, meters<double>>, meters<double>>);
static_assert(std::is_same_v<meters<double>::rebind<int>, meters<int>>);
// watts and dBW share ONE conversion_factor; only the scale differs:
static_assert(std::is_same_v<units::power::watts<double>::conversion_factor,
units::power::dBW<double>::conversion_factor>);
static_assert(!std::is_same_v<units::power::watts<double>::numerical_scale_type,
units::power::dBW<double>::numerical_scale_type>);
return 0;
}
unit< typename T::conversion_factor, typename T::underlying_type, typename T::numerical_scale_type > unit_base_t
Maps any unit type to the canonical unit<Cf, Underlying, Scale> it represents.
Definition core.h:3194
units representing power values

5. Triviality is preserved by explicitly defaulting the special members

A unit must stay a trivial value type — memcpy-able, zero-overhead, indistinguishable from its underlying scalar at run time. Making the named unit a class threatens that: declaring the converting constructor unitName(const base&) would suppress the trivial default constructor, and inheriting the base's constructors (using base::base;) leaves the special members implicit. The macro therefore explicitly defaults all of them (core.h:223227):

unitName() = default;
unitName(const unitName&) = default;
unitName(unitName&&) = default;
unitName& operator=(const unitName&) = default;
unitName& operator=(unitName&&) = default;

That restores std::is_trivial. Combined with __declspec(empty_bases) on MSVC (core.h:2381) so empty base classes take no space, a named unit is the same size as its underlying storage:

#include <units/length.h>
#include <type_traits>
int main()
{
static_assert(std::is_trivial_v<units::length::meters<double>>);
static_assert(sizeof(units::length::meters<double>) == sizeof(double));
return 0;
}

Design rationale: the name is a compile-time property of the type; it imposes zero run-time cost. Explicitly defaulting the special members is what keeps the class the name is built on the same size and triviality as the double it stores. The readable diagnostics carry no run-time cost.

See also