Skip to content

Instantly share code, notes, and snippets.

@2b-t
Last active November 19, 2023 14:31
Show Gist options
  • Save 2b-t/02daa85ea5d83fc2cb96bfcf0570ab71 to your computer and use it in GitHub Desktop.
Save 2b-t/02daa85ea5d83fc2cb96bfcf0570ab71 to your computer and use it in GitHub Desktop.
Constexpr units in the last place (ULP) floating number comparison in C++20 similar to GoogleTest
/**
* \file is_almost_equal_ulps.hpp
* \brief Constexpr implementation of a floating number point comparison based on units in last place (ULPs)
* \mainpage Contains traits and a constexpr function for comparing two floating point numbers based on units
* in the last place (ULP) similar to the implementation in GoogleTest but more modular and constexpr:
* https://github.com/google/googletest/blob/main/googletest/include/gtest/internal/gtest-internal.h
*/
#ifndef IS_ALMOST_EQUAL_ULPS_HPP
#define IS_ALMOST_EQUAL_ULPS_HPP
#pragma once
#include <bit>
#include <concepts>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <limits>
#include <type_traits>
#include <utility>
#include <vector>
namespace detail {
// Trait for excluding incomplete types: See https://stackoverflow.com/a/44229779/9938686
template <typename T, std::size_t = sizeof(T)>
consteval std::true_type is_complete(T*) noexcept;
consteval std::false_type is_complete(...) noexcept;
}
template <typename T>
using is_complete = decltype(detail::is_complete(std::declval<T*>()));
template <typename T>
static constexpr bool is_complete_v = is_complete<T>::value;
namespace detail {
// Class for determining the corresponding unsigned integer type with equal length to the floating type
template <std::size_t N>
class UIntEquiv {
protected:
UIntEquiv() = delete;
UIntEquiv(UIntEquiv const&) = delete;
UIntEquiv(UIntEquiv&&) = delete;
UIntEquiv& operator= (UIntEquiv const&) = delete;
UIntEquiv& operator= (UIntEquiv&&) = delete;
template<std::size_t M, typename std::enable_if_t<(M==sizeof(std::uint8_t))>* = nullptr>
static consteval std::uint8_t determineUIntType() noexcept;
template<std::size_t M, typename std::enable_if_t<(M==sizeof(std::uint16_t))>* = nullptr>
static consteval std::uint16_t determineUIntType() noexcept;
template<std::size_t M, typename std::enable_if_t<(M==sizeof(std::uint32_t))>* = nullptr>
static consteval std::uint32_t determineUIntType() noexcept;
template<std::size_t M, typename std::enable_if_t<(M==sizeof(std::uint64_t))>* = nullptr>
static consteval std::uint64_t determineUIntType() noexcept;
public:
using type = decltype(determineUIntType<N>());
};
// You can potentially add specialisation of UIntEquiv for longer unsigned integer types here (e.g. for long double support).
// e.g. GCC's __uint128_t: https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html
// or Boost: https://www.boost.org/doc/libs/1_67_0/libs/multiprecision/doc/html/boost_multiprecision/tut/ints/cpp_int.html
//
// template <>
// class UIntEquiv<sizeof(__uint128_t)> {
// public:
// using type = __uint128_t;
// };
//
// As long as std::numeric_limits<T> is specialized for the corresponding floating type and your architecture respects IEEE754 and stores
// your floating point numbers with little-endian the code should compile correctly.
// Therefore in case you have particular proprietary floating types with a different mantissa and exponent such as
// e.g. GCC's __float128: https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.html
// the fastest solution is probably to specialize the std::numeric_limits<T> trait yourself.
// Boost should already provide the fully specialized traits: https://www.boost.org/doc/libs/1_65_1/libs/multiprecision/doc/html/boost_multiprecision/tut/floats/float128.html
template <std::size_t N>
using UIntEquiv_t = typename UIntEquiv<N>::type;
// In case your floating type does not respect IEEE754 or is not stored with little endian you will have to specialise the entire
// FloatTrait yourself:
template <typename T>
class FloatTrait;
// Specialised trait for floating point number types according to IEEE754 stored with little endian
template <typename T>
requires std::is_floating_point_v<T> && std::numeric_limits<T>::is_iec559 && (std::endian::native == std::endian::little)
class FloatTrait<T> {
public:
static constexpr std::size_t number_of_bytes {sizeof(T)};
static constexpr std::size_t number_of_bits {number_of_bytes*std::numeric_limits<std::uint8_t>::digits};
using Bytes = UIntEquiv_t<number_of_bytes>;
static constexpr std::size_t number_of_sign_bits {1};
static constexpr std::size_t number_of_fraction_bits {std::numeric_limits<T>::digits-1};
static constexpr std::size_t number_of_exponent_bits {number_of_bits - number_of_sign_bits - number_of_fraction_bits};
static constexpr Bytes sign_mask {Bytes{1} << (number_of_bits - 1)};
static constexpr Bytes fraction_mask {~Bytes{0} >> (number_of_exponent_bits + 1)};
static constexpr Bytes exponent_mask {~(sign_mask | fraction_mask)};
static constexpr bool isNan(T const t) noexcept {
auto const bytes {std::bit_cast<Bytes>(t)};
auto const exponent_bytes {extractExponent(bytes)};
auto const fraction_bytes {extractFraction(bytes)};
return (exponent_bytes == exponent_mask) && (fraction_bytes != 0);
}
static constexpr bool isPosInf(T const t) noexcept {
return isPos(t) && isInf(t);
}
static constexpr bool isNegInf(T const t) noexcept {
return isNeg(t) && isInf(t);
}
static constexpr bool isNeg(T const t) noexcept {
auto const bytes {std::bit_cast<Bytes>(t)};
auto const sign_bytes {extractSign(bytes)};
return sign_bytes != 0;
}
// Optional helper functions
static constexpr bool isPos(T const t) noexcept {
auto const bytes {std::bit_cast<Bytes>(t)};
auto const sign_bytes {extractSign(bytes)};
return sign_bytes == 0;
}
static constexpr bool isInf(T const t) noexcept {
auto const bytes {std::bit_cast<Bytes>(t)};
auto const exponent_bytes {extractExponent(bytes)};
auto const fraction_bytes {extractFraction(bytes)};
return (exponent_bytes == exponent_mask) && (fraction_bytes == 0);
}
static constexpr Bytes extractSign(Bytes const bytes) noexcept {
return bytes & sign_mask;
}
static constexpr Bytes extractExponent(Bytes const bytes) noexcept {
return bytes & exponent_mask;
}
static constexpr Bytes extractFraction(Bytes const bytes) noexcept {
return bytes & fraction_mask;
}
protected:
FloatTrait() = delete;
FloatTrait(FloatTrait const&) = delete;
FloatTrait(FloatTrait&&) = delete;
FloatTrait& operator= (FloatTrait const&) = delete;
FloatTrait& operator= (FloatTrait&&) = delete;
};
template <typename T>
requires is_complete_v<FloatTrait<T>>
class FloatView {
public:
using Trait = FloatTrait<T>;
using Bytes = typename FloatTrait<T>::Bytes;
explicit constexpr FloatView(T const t) noexcept
: value{t} {
return;
}
FloatView() = default;
FloatView(FloatView const&) = default;
FloatView(FloatView&&) = default;
FloatView& operator= (FloatView const&) = default;
FloatView& operator= (FloatView&&) = default;
constexpr bool isAlmostEqual(FloatView const rhs, std::uint8_t const max_distance = 4) const noexcept {
if (Trait::isNan(value) || Trait::isNan(rhs.value)) {
return false;
} else if (Trait::isNegInf(value) != Trait::isNegInf(rhs.value)) {
return false;
} else if (Trait::isPosInf(value) != Trait::isPosInf(rhs.value)) {
return false;
}
return computeDistance(value, rhs.value) <= max_distance;
}
protected:
T value;
static constexpr Bytes signMagnitudeToBiased(T const t) noexcept {
auto const b {std::bit_cast<Bytes>(t)};
if (Trait::isNeg(t)) {
return ~b + Bytes{1};
} else {
return Trait::sign_mask | b;
}
}
static constexpr Bytes computeDistance(T const a, T const b) noexcept {
auto const biased1 = signMagnitudeToBiased(a);
auto const biased2 = signMagnitudeToBiased(b);
return (biased1 >= biased2) ? (biased1 - biased2) : (biased2 - biased1);
}
};
}
template <typename T>
constexpr bool isAlmostEqual(T const lhs, T const rhs, std::uint8_t const max_distance = 4) noexcept {
detail::FloatView<T> const a {lhs};
detail::FloatView<T> const b {rhs};
return a.isAlmostEqual(b, max_distance);
}
#endif // IS_ALMOST_EQUAL_ULPS_HPP
@2b-t
Copy link
Author

2b-t commented Feb 10, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment