5
\$\begingroup\$

Broken out from a previous question (C++17 saturating integer (arithmetic) type library) is a small C++17 header-only library to deal with issues that arise working with arithmetic where signed, unsigned and floating point types are combined.

One such example is accidental promotion of signed types to unsigned: https://stackoverflow.com/a/25609711/6984656

A copy-past demo of above issue:

#include <limits>
#include <algorithm>
#include <arithmetic_type_tools/arithmetic_type_tools.hpp>

int main() {
    using Ta = arithmetic_type_tools::fit_all_t<int32_t, uint32_t>;

    // Will not fit `std::numeric_limits<int32_t>::lowest()`:
    using Tb = std::common_type_t<int32_t, uint32_t>;

    auto a = std::is_signed_v<Ta>;
    auto b = std::is_signed_v<Tb>;
    volatile auto keep1 = a; // 1
    volatile auto keep2 = b; // 0
    volatile auto keep3 = sizeof(Ta); // 8
    volatile auto keep4 = sizeof(Tb); // 4
}

The library code:

#pragma once

#include <cstddef>
#include <cstdint>
#include <limits>
#include <type_traits>
#include <utility>
#include <algorithm>

namespace arithmetic_type_tools {
    /**@{ Select an integral or floating point type by size */
    template <size_t S> struct signed_by_size { using type = void; };
    template <> struct signed_by_size<1> { using type = int8_t; };
    template <> struct signed_by_size<2> { using type = int16_t; };
    template <> struct signed_by_size<4> { using type = int32_t; };
    template <> struct signed_by_size<8> { using type = int64_t; };
    #ifdef __SIZEOF_INT128__
        template <> struct signed_by_size<16> { using type = __int128_t; };
    #endif
    #ifdef __SIZEOF_INT256__
        template <> struct signed_by_size<32> { using type = __int256_t; };
    #endif

    template <size_t S>
    using signed_by_size_t = typename signed_by_size<S>::type;

    template <size_t S> struct unsigned_by_size { using type = void; };
    template <> struct unsigned_by_size<1> { using type = uint8_t; };
    template <> struct unsigned_by_size<2> { using type = uint16_t; };
    template <> struct unsigned_by_size<4> { using type = uint32_t; };
    template <> struct unsigned_by_size<8> { using type = uint64_t; };
    #ifdef __SIZEOF_INT128__
        template <> struct unsigned_by_size<16> { using type = __uint128_t; };
    #endif
    #ifdef __SIZEOF_INT256__
        template <> struct unsigned_by_size<32> { using type = __uint256_t; };
    #endif

    template <size_t S>
    using unsigned_by_size_t = typename unsigned_by_size<S>::type;

    // The explicit size checks prevent ambiguity in (long) double sizes.
    template <size_t S> struct float_by_size { using type = void; };
    #if __SIZEOF_FLOAT__ == 4
        template <> struct float_by_size<4> { using type = float; };
    #endif
    #if __SIZEOF_DOUBLE__ == 8
        template <> struct float_by_size<8> { using type = double; };
    #endif
    #if __SIZEOF_LONG_DOUBLE__ == 16
        template <> struct float_by_size<16> { using type = long double; };
    #endif

    template <size_t S>
    using float_by_size_t = typename float_by_size<S>::type;
    /** @} */

    // Yes, it's not pretty, but it solves the chicken-egg issue caused by the broken `std::common_type`
    /**
     * Variadic `min` implementation for safely mixing unsigned, signed and floating point arguments
     * @returns Lowest value of type `fit_all_t<T...>`
     */
    template <typename... Ts,
              typename T = std::conditional_t<(std::is_signed_v<Ts> || ...) && (std::is_unsigned_v<Ts> || ...),
                                              signed_by_size_t<sizeof(std::common_type_t<Ts...>) *
                                                               ((sizeof(std::common_type_t<Ts...>) > std::max(
                                                                    { (std::is_unsigned_v<Ts> ? sizeof(Ts) : 0)... }
                                                                    )) ? 1 : 2)>,
                                              std::common_type_t<Ts...>>>
    constexpr T min(const Ts&... vals) {
        T temp = std::numeric_limits<T>::max();
        ((temp = static_cast<T>(temp) < static_cast<T>(vals) ? static_cast<T>(temp) : static_cast<T>(vals)), ...);
        return temp;
    }

    /**
     * Variadic `max` implementation for safely mixing unsigned, signed and floating point arguments
     * @returns Highest value of type `fit_all_t<T...>`
     */
    template <typename... Ts,
              typename T = std::conditional_t<(std::is_signed_v<Ts> || ...) && (std::is_unsigned_v<Ts> || ...),
                                              signed_by_size_t<sizeof(std::common_type_t<Ts...>) *
                                                               ((sizeof(std::common_type_t<Ts...>) > std::max(
                                                                    { (std::is_unsigned_v<Ts> ? sizeof(Ts) : 0)... }
                                                                    )) ? 1 : 2)>,
                                              std::common_type_t<Ts...>>>
    constexpr T max(const Ts... vals) {
        T temp = std::numeric_limits<T>::lowest();
        ((temp = static_cast<T>(vals) > static_cast<T>(temp) ? static_cast<T>(vals) : static_cast<T>(temp)), ...);
        return temp;
    }

    /**
     * `clamp` implementation for safely mixing unsigned, signed and floating point arguments
     * @returns Value of type `fit_all_t<T...>` clamped to `low...high`
     */
    template <typename T, typename U, typename V,
              typename C = std::conditional_t<(  std::is_signed_v<T> ||   std::is_signed_v<U> ||   std::is_signed_v<V>) &&
                                              (std::is_unsigned_v<T> || std::is_unsigned_v<U> || std::is_unsigned_v<V>),
                                              signed_by_size_t<sizeof(std::common_type_t<T, U, V>) *
                                                               ((sizeof(std::common_type_t<T, U, V>) > std::max(
                                                                    {(std::is_unsigned_v<T> ? sizeof(T) : 0),
                                                                     (std::is_unsigned_v<U> ? sizeof(U) : 0),
                                                                     (std::is_unsigned_v<V> ? sizeof(V) : 0)}
                                                                    )
                                                                ) ? 1 : 2)>,
                                              std::common_type_t<T, U, V>>>
    constexpr auto clamp(const T& low, const U& val, const V& high) {
        return static_cast<C>(low) < static_cast<C>(val)
                    ? (static_cast<C>(val) < static_cast<C>(high)
                            ? static_cast<C>(val)
                            : static_cast<C>(high))
                    : static_cast<C>(low);
    }

    /** Store the size of the largest signed type in a range of types in `value`. */
    template <typename... T>
    struct largest_signed {
        static constexpr size_t value = max((std::is_signed_v<T> ? sizeof(T) : 0)...);
    };

    /** Return the size of the largest signed type in a range of types. */
    template <typename... T>
    constexpr decltype(auto) largest_signed_v = largest_signed<T...>::value;

    /** Store the size of the largest unsigned type in a range of types in `value`. */
    template <typename... T>
    struct largest_unsigned {
        static constexpr size_t value = max((std::is_unsigned_v<T> ? sizeof(T) : 0)...);
    };

    /** Return the size of the largest signed type in a range of types. */
    template <typename... T>
    constexpr decltype(auto) largest_unsigned_v = largest_unsigned<T...>::value;

    /** Store the size of the largest floating point type in a range of types in `value`. */
    template <typename... T>
    struct largest_float {
        static constexpr size_t value = max((std::is_floating_point_v<T> ? sizeof(T) : 0)...);
    };

    /** Return the size of the largest floating point type in a range of types. */
    template <typename... T>
    constexpr decltype(auto) largest_float_v = largest_float<T...>::value;

    /** Use `next_up<T>` to obtain a type that is similar to `T` but twice the size. */
    template <typename T>
    class next_up {
    private:
        using BT = std::decay_t<T>;

    public:
        using type = std::conditional_t<std::is_floating_point_v<BT>,
                                        float_by_size_t<2 * sizeof(BT)>,
                                        std::conditional_t<std::is_signed_v<BT>,
                                                           signed_by_size_t<2 * sizeof(BT)>,
                                                           unsigned_by_size_t<2 * sizeof(BT)>>>;
    };

    /** Use `next_up_t<T>` to obtain a type that is similar to `T` but twice the size. */
    template <typename T>
    using next_up_t = typename next_up<T>::type;


    /** `fit_all` is a `std::common_type` replacement that combines signed and unsigned properly.
     *
     *  Combining an unsigned type and signed type sized identical or smaller to a common type will result in an
     *  unsigned type equal in size of the input unsigned, breaking any negative values. `fit_all` will instead return
     *  a signed type that can hold all types.
     */
    template <typename... T>
    class fit_all {
    private:
        static constexpr size_t ls = largest_signed_v<T...>;
        static constexpr size_t lu = largest_unsigned_v<T...>;
        static constexpr size_t lf = largest_float_v<T...>;

    public:
        using type = std::conditional_t<(lf > 0),
                                        float_by_size_t<lf>,
                                        std::conditional_t<(ls > 0),
                                                           std::conditional_t<(ls > lu),
                                                                              signed_by_size_t<ls>,
                                                                              signed_by_size_t<2 * lu>>,
                                                           std::conditional_t<(lu > 0),
                                                                              unsigned_by_size_t<lu>,
                                                                              void>>>;
    };

    /** `fit_all_t` is a `std::common_type_t` replacement that combines signed and unsigned properly.
     *
     *  Combining an unsigned type and signed type sized identical or smaller to a common type will result in an
     *  unsigned type equal in size of the input unsigned, breaking any negative values. `fit_all_t` will instead return
     *  a signed type that can hold all types.
     */
    template <typename... T>
    using fit_all_t = typename fit_all<T...>::type;
} // namespace arithmetic_type_tools

I'd love to hear any suggestions for improvement or (potential) errors.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This bit caused compilation errors:

template <typename... T>
constexpr decltype(auto) largest_float_v = largest_float<T...>::value;

GCC rejects it because

187868.cpp:142:30: error: ‘decltype(auto)’ cannot be cv-qualified
  142 |     constexpr decltype(auto) largest_float_v = largest_float<T...>::value;
      |                              ^~~~~~~~~~~~~~~

Replacing with constexpr auto fixes things.


The types from <cstdint> are consistently written as if in the global namespace. That's not a portable assumption - always write them as std::size_t, std::uint8_t, etc.

It's not portable to assume that the fixed-width types all exist. In particular, std::int8_t and std::uint8_t are unlikely to be defined on platforms where CHAR_BIT is larger than 8.


Similarly, it's not portable to assume that only particular sizes are possible for the floating-point types.

Somewhat more portable:

    template<std::size_t S> struct float_by_size { using type = void; };
    template<> struct float_by_size<sizeof (float)> { using type = float; };
#if __SIZEOF_DOUBLE__ != __SIZEOF_FLOAT__
    template<> struct float_by_size<sizeof (double)> { using type = double; };
#endif
#if __SIZEOF_LONG_DOUBLE__ != __SIZEOF_DOUBLE__
    template<> struct float_by_size<sizeof (long double)> { using type = long double; };
#endif

There's no consideration of maintaining precision when converting from integer to floating-point types. This means that arithmetic_type_tools::fit_all_t<float, std::uintmax_t> will use the narrowest floating type, losing a large amount of precision.


I don't think we need any of the _by_size classes. We can get rid of the float and unsigned ones by selecting the widest type in each class, rather that the largest size, like this:

// filter function adapted from https://stackoverflow.com/a/18366475/4850040

template<typename, typename> struct Cons;

template<typename T, typename ...Args>
struct Cons<T, std::tuple<Args...>>
{
    using type = std::tuple<T, Args...>;
};

template<template<class> class, typename...> struct filter;

template<template<class> class Pred> struct filter<Pred> { using type = std::tuple<>; };

template<template<class> class Pred, typename Head, typename ...Tail>
struct filter<Pred, Head, Tail...>
{
    using type = typename std::conditional<Pred<Head>::value,
                                           typename Cons<Head, typename filter<Pred, Tail...>::type>::type,
                                           typename filter<Pred, Tail...>::type
                                           >::type;
};

template<typename...> struct widest_type;
template<> struct widest_type <std::tuple<>> { using type = void; };
template<typename T> struct widest_type <std::tuple<T>> { using type = T; };
template<typename T1, typename T2, typename... Rest> struct widest_type<std::tuple<T1, T2, Rest...>> {
    using type = widest_type<std::conditional_t<sizeof (T1) < sizeof (T2), T2, T1>, Rest...>;
};

template<typename... T>
struct widest_float {
    using type = widest_type<typename filter<std::is_floating_point, T...>::type>::type;
};

template<typename... T>
using widest_float_t = widest_float<T...>::type;

template<typename... T>
struct widest_signed {
    using type = widest_type<typename filter<std::is_signed, T...>::type>::type;
};

template<typename... T>
using widest_signed_t = widest_signed<T...>::type;

template<typename... T>
struct widest_unsigned {
    using type = widest_type<typename filter<std::is_unsigned, T...>::type>::type;
};

template<typename... T>
using widest_unsigned_t = widest_unsigned<T...>::type;

Then we can use these functions in fit_all like this:

template <typename... T>
class fit_all {
private:
    using signed_type = widest_signed<T...>;
    using unsigned_type = widest_unsigned<T...>;
    using float_type = widest_float_t<T...>;

public:
    using type = std::conditional_t<std::is_void_v<float_type>,
                                    std::conditional_t<std::is_void_v<signed_type>,
                                                       std::conditional_t<std::is_void_v<unsigned_type>,
                                                                          void, unsigned_type>,
                                                       std::conditional_t<std::is_void_v<unsigned_type> || sizeof (unsigned_type) < sizeof (signed_type),
                                                                          signed_type,
                                                                          signed_by_size_t<2 * sizeof (unsigned_type)>>>,
                                    float_type>;
};

I think it should be possible to eliminate signed_by_size_t<> if we implement a next-larger-signed-type metafunction, which we can do by walking a list of types. If we're really thorough, we could even sort and deduplicate the type list at compile time.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.