'Is there a way to create a scope in a template or decltype in c++17, for a unit librairy?

Does anyone know if there is any feature in c++17 that would allow you to create a scope (namespace, class or function) inside of a template or decltype() declaration? (note: lambdas don't work inside decltype until c++20.)

The reason for that would be to hide the local variables from the unit base type with a using unit::s declaration, as unit names are often short single/double letter word that are already quite commonly used in code.

The unit type is created by a macro Unit(type, unitBase) that return a Unit_t<...> and a macro unit(value, unitBase) return a value of that type.

Unit bases are UnitExpr<UnitDim> {multiplier} values named correspondingly 's' for seconds, 'm' for meters, etc.. But if we don't want to hide these commonly used names, we are forced to change their name to 's_' or '_s'. (see the reason below)

So instead of this: Unit(int, m/s) speed = unit(10, km/h);

we have to write this: Unit(int, m_/s_) speed = unit(10, km_/h_);


Here is the implementation of the macros:

// for type declaration
#define Unit(T, ...) Unit_t<T, typename decltype(unit_ * __VA_ARGS__)::dim,                          \
                            std::ratio<(unit_ * __VA_ARGS__).mul.num, (unit_ * __VA_ARGS__).mul.den>>

// for value creation/convertion
#define unit(val, ...) Unit_t<decltype(val), typename decltype(unit_ * __VA_ARGS__)::dim,             \
                               std::ratio<(unit_ * __VA_ARGS__).mul.num, (unit_ * __VA_ARGS__).mul.den>> {val}

// get unit base of type
#define unitOf(unit) (UnitExpr<typename decltype(unit)::dim>{{ decltype(unit)::mul::num, decltype(unit)::mul::den }})


template<typename T, typename Dim, typename Mul>
struct Unit_t; // wrapper around a T value

template<typename Dim>
struct UnitExpr {
    using dim = Dim;
    Ratio mul; // runtime std::ratio
};

// use of variadic macros to allow for coma inside of {}, [] or <>

And here is what could be used to hide the name of the local variables:

#define Unit(T, ...) /*decltype or other*/ {                    \
                                  using type = T;               \
                                  using unitBases::s;           \
                                  using unitBases::m;           \
                                  ... -> include magic macro ?  \
                                                                \
                                  constexpr auto base = unit_* __VA_ARGS__;       \
                                  return Unit_t<...>(); or using unit = Unit_t<>; \
                              } /*get type back*/

// using type = T; before using unitBases:: to not hide the name of the type (ex: T in templates <> Tesla)

Is there a way to create such scope or another way to hide the names in c++17.


Here is an implementation of the code:

#include <numeric>
#include <ratio>

// unit dimension (with std::ratio dimensions)
template<typename LengthDim, typename AngleDim, typename TimeDim, ...>
struct UnitDim_t {};

template<typename UnitDim1, typename UnitDim2>
using mulUnitDim = UnitDim_t<std::ratio_add<UnitDim1::lengthDim, UnitDim2::lengthDim>, ...>;
template<typename UnitDim1, typename UnitDim2>
using divUnitDim = UnitDim_t<std::ratio_subtract<UnitDim1::lengthDim, UnitDim2::lengthDim>, ...>;

namespace UnitDim {
  using Unit   = UnitDim_t<0, 0, 0, ...>;
  using Length = UnitDim_t<1, 0, 0, ...>;
  using Angle  = UnitDim_t<0, 1, 0, ...>;
  using Time   = UnitDim_t<0, 0, 1, ...>;

  using Velocity = divUnitDim<Length, Time>; // useful for units like force in [N]
  ...
}

// Unit type is a wrapper around a T
template<typename T, typename Dim, typename Mul>
struct Unit_t {
    using type = T;
    using dim = Dim;
    using mul = Mul;

    T count;

    constexpr Unit_t() = default;
    constexpr Unit_t(T count) : count(count) {}

    // conversion from different type and multiplier but not dimension
    template<typename T2, typename Mul2>
    constexpr Unit_t(Unit_t<T2, Dim, Mul2> u) { // count*mul = u.count*Mul2 <=> count = u.count * Mul2 / mul;
        using factor = std::ratio_divide<Mul2, mul>;
        count = u.count * factor::num / factor::den;
        // -> add consideration to overflow for integers and constexpr optimize in general (precompute factor for floating point T)
    }
};
template<typename T1, typename Dim1, typename Mul1, typename T2, typename Dim2, typename Mul2>
constexpr auto operator*(Unit_t<T1, Dim1, Mul1> u1, Unit_t<T2, Dim2, Mul2> u2) {
    return Unit_t<decltype(u1.count* u2.count), mulUnitDim<Dim1, Dim2>, std::ratio_multiply<Mul1, Mul2>> { u1.count * u2.count };
}
// same for T * Unit and division


// naive implementation of a runtime std::ratio
struct Ratio {
    uintmax_t num, den = 1;
    // operators * and / for Ratio
};

// unit expression for the base units
template<typename Dim /*, typename PiExp*/> // add exponant for pi and support for sqrt of ratios (no floating point in template in c++17)
struct UnitExpr {
    using dim = Dim;
    Ratio mul; // necesary because there is no constrexpr function paramerters

    // operators * and / for UnitExpr
};

//namespace unitBases { // maybe namespace
constexpr UnitExpr<typename UnitDim::Unit>   unit_ = {{1}};
constexpr UnitExpr<typename UnitDim::Length> m_ = {{1}};
constexpr UnitExpr<typename UnitDim::Length> km_ = {{1000}};
constexpr UnitExpr<typename UnitDim::Angle>  rad_ = {{1}};
constexpr UnitExpr<typename UnitDim::Time>   s_ = {{1}};
constexpr UnitExpr<typename UnitDim::Time>   min_ = {{60}};
constexpr UnitExpr<typename UnitDim::Time>   h_ = {{3600}};
//}

Then we can do this:

Unit(int, m_) someLength = unit(10, km_); // count = 10000

Unit(float, 1/10*m_/s_) speed = someLength / unit(4.2, h_/15); // count = 99.2064  (9.92 m/s)

// note that we first multiply by unit_ in macro: "(unit_ * 1/10*m_/s_)" so that mul don't become zero, or another reduced value.

Unit(uint16_t, unitOf(someLength) / 100 / rad_) angularDist = 2 * speed / unit(M_PI/2.0, rad_) * unit(1, s_); // count = 1263

In c++20, decltype accept lambdas, so we can just change the macro to:

#define Unit(T, ...) decltype([](){                             \
                                  using type = T;               \
                                  using unitBases::s;           \
                                  using unitBases::m;           \
                                  ... -> include magic macro ?  \
                                                                \
                                  constexpr auto base = unit_* __VA_ARGS__;              \
                                  return Unit_t<type, typename base::dim,                \
                                                std::ratio<base.mul.num, base.mul.den>>; \
                              }())

More generally speaking, if c++20 is allowed, we can just simplify:

// use struct values instead of types
template<Ratio LengthDim, Ratio AngleDim, Ratio TimeDim, ...>
struct UnitDim_t {};

template<typename Dim, Ratio Mul>
struct UnitExpr {};


template<typename T, UnitExpr UnitBase>
struct Unit_t;

// no macro now viable, but with the name issue and another problem:
Unit_t<int, 1/10*m_/s_> speed; // Error: multiplier is 0 because of integer division

Unit_t<int, 5/2*m_/s_> speed; // OK: BUT STILL NOT CORRECT!

Unit_t<int, unit_*5/2*m_/s_> speed; // OK

And the macro becomes:

#define Unit(T, ...) Unit_t<T, decltype([](){                   \
                                   using unitBases::s;          \
                                   using unitBases::m;          \
                                   ... -> include magic macro ? \
                                                                \
                                   return (unit_* __VA_ARGS__); \
                               }())>

So is there any way to do the same with c++17?


Oh, and here is some macro magic madness for those who swing that way:

#define CONCAT_HELPER(a, b)  a##b
#define CONCAT(a, b)         CONCAT_HELPER(a, b)

#define EXPEND(x)  x  // because MSVC consider __VA_ARG__ as one argument for some reason
#define COUNT_ARGS_HELPER(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, n,...)  n
#define COUNT_ARGS(...) EXPEND(COUNT_ARGS_HELPER(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))
// add more numbers if needed

#define IMPORT_UNITS2(unitDim, unit)       using unitBases::unitDim::unit;
#define IMPORT_UNITS3(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS2(unitDim, __VA_ARGS__))
#define IMPORT_UNITS4(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS3(unitDim, __VA_ARGS__))
#define IMPORT_UNITS5(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS4(unitDim, __VA_ARGS__))
#define IMPORT_UNITS6(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS5(unitDim, __VA_ARGS__))
#define IMPORT_UNITS7(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS6(unitDim, __VA_ARGS__))
#define IMPORT_UNITS8(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS7(unitDim, __VA_ARGS__))
#define IMPORT_UNITS9(unitDim, unit, ...)  using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS8(unitDim, __VA_ARGS__))
#define IMPORT_UNITS10(unitDim, unit, ...) using unitBases::unitDim::unit; EXPEND(IMPORT_UNITS9(unitDim, __VA_ARGS__))
#define IMPORT_UNITS(...)                  CONCAT(IMPORT_UNITS, COUNT_ARGS(__VA_ARGS__)) (__VA_ARGS__)


#define IMPORT_UNIT_DIMS1(unitDim)       IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS))
#define IMPORT_UNIT_DIMS2(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS1(__VA_ARGS__))
#define IMPORT_UNIT_DIMS3(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS2(__VA_ARGS__))
#define IMPORT_UNIT_DIMS4(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS3(__VA_ARGS__))
#define IMPORT_UNIT_DIMS5(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS4(__VA_ARGS__))
#define IMPORT_UNIT_DIMS6(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS5(__VA_ARGS__))
#define IMPORT_UNIT_DIMS7(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS6(__VA_ARGS__))
#define IMPORT_UNIT_DIMS8(unitDim, ...)  IMPORT_UNITS(unitDim, CONCAT(unitDim, _UNITS)) EXPEND(IMPORT_UNIT_DIMS7(__VA_ARGS__))
#define IMPORT_UNIT_DIMS(...)            CONCAT(IMPORT_UNIT_DIMS, COUNT_ARGS(__VA_ARGS__)) (__VA_ARGS__)


#define IMPORT_UNIT_LIBS1(unitLib)       IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS))
#define IMPORT_UNIT_LIBS2(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS1(__VA_ARGS__))
#define IMPORT_UNIT_LIBS3(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS2(__VA_ARGS__))
#define IMPORT_UNIT_LIBS4(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS3(__VA_ARGS__))
#define IMPORT_UNIT_LIBS5(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS4(__VA_ARGS__))
#define IMPORT_UNIT_LIBS6(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS5(__VA_ARGS__))
#define IMPORT_UNIT_LIBS7(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS6(__VA_ARGS__))
#define IMPORT_UNIT_LIBS8(unitLib, ...)  IMPORT_UNIT_DIMS(CONCAT(unitLib, _DIMS)) IDENTITY(IMPORT_UNIT_LIBS7(__VA_ARGS__))
#define IMPORT_UNIT_LIBS(...)            CONCAT(IMPORT_UNIT_LIBS, COUNT_ARGS(__VA_ARGS__)) (__VA_ARGS__)


// main macro used instead of "using unitBase::unitDim1::unit1; ..."
#define IMPORT_ALL_UNITS  IMPORT_UNIT_LIBS(UNIT_LIBS)

// Why don't we have "using unitBase::*::*" instead of this madness?

#define STD_UNITS_DIMS  Length, Angle, Time, Mass //, ...

#define Length_UNITS  m, km, dm, cm, mm, um
#define Angle_UNITS   rad, deg
#define Time_UNITS    s, min, h, ms, us, ns
#define Mass_UNITS    g, kg, t, mg
//...


//===== unitConfig.h =====

// add your own unit libraries
#define UNIT_LIBS  STD_UNITS, DATA_UNITS

// add _DIMS for the unit dimensions
// STD_UNITS -> #define STD_UNITS_DIMS Length, ... (Dims are namespace names inside of namespace unitBases)
// add _UNITS for the units
// Length -> #define Length_UNITS  m, km, ... (units are names of units inside of namespace unitBases::Dim)

//ex:
#define DATA_UNITS_DIMS  Data
#define Data_UNITS  bit, B, kB, MB, GB, TB, KiB, MiB, GiB


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source