Commit c382ca4e authored by Yang Zhang's avatar Yang Zhang Committed by Facebook Github Bot

Add constexpr_clamp_cast<> in folly/ConstexprMath.h

Summary:
constexpr_clamp_cast<> provides sane numeric conversions from float point numbers to
integral numbers, and between different types of integral numbers.

When doing clamp_cast<DstT>(value), if `value` is in valid range of DstT, it will give correct result in DstT, equal to `value`.

If `value` is outside the representable range of DstT, it will be clamped to MAX or MIN in DstT.

Float NaNs are converted to 0 in integral type.

For comparison, static_cast<> will return garbage results when SrcT is float point number and it's outside DstT valid range. This is because float-cast-overflow is undefined behavior.

Reviewed By: yfeldblum

Differential Revision: D7716736

fbshipit-source-id: 5899e4b20cdd997d0c7b121ddaababbc8c748301
parent 70ea8045
......@@ -231,4 +231,158 @@ constexpr T constexpr_sub_overflow_clamped(T a, T b) {
// clang-format on
}
// clamp_cast<> provides sane numeric conversions from float point numbers to
// integral numbers, and between different types of integral numbers. It helps
// to avoid unexpected bugs introduced by bad conversion, and undefined behavior
// like overflow when casting float point numbers to integral numbers.
//
// When doing clamp_cast<Dst>(value), if `value` is in valid range of Dst,
// it will give correct result in Dst, equal to `value`.
//
// If `value` is outside the representable range of Dst, it will be clamped to
// MAX or MIN in Dst, instead of being undefined behavior.
//
// Float NaNs are converted to 0 in integral type.
//
// Here's some comparision with static_cast<>:
// (with FB-internal gcc-5-glibc-2.23 toolchain)
//
// static_cast<int32_t>(NaN) = 6
// clamp_cast<int32_t>(NaN) = 0
//
// static_cast<int32_t>(9999999999.0f) = -348639895
// clamp_cast<int32_t>(9999999999.0f) = 2147483647
//
// static_cast<int32_t>(2147483647.0f) = -348639895
// clamp_cast<int32_t>(2147483647.0f) = 2147483647
//
// static_cast<uint32_t>(4294967295.0f) = 0
// clamp_cast<uint32_t>(4294967295.0f) = 4294967295
//
// static_cast<uint32_t>(-1) = 4294967295
// clamp_cast<uint32_t>(-1) = 0
//
// static_cast<int16_t>(32768u) = -32768
// clamp_cast<int16_t>(32768u) = 32767
template <typename Dst, typename Src>
constexpr typename std::enable_if<std::is_integral<Src>::value, Dst>::type
constexpr_clamp_cast(Src src) {
static_assert(
std::is_integral<Dst>::value && sizeof(Dst) <= sizeof(int64_t),
"constexpr_clamp_cast can only cast into integral type (up to 64bit)");
using L = std::numeric_limits<Dst>;
// clang-format off
return
// Check if Src and Dst have same signedness.
std::is_signed<Src>::value == std::is_signed<Dst>::value
? (
// Src and Dst have same signedness. If sizeof(Src) <= sizeof(Dst),
// we can safely convert Src to Dst without any loss of accuracy.
sizeof(Src) <= sizeof(Dst) ? Dst(src) :
// If Src is larger in size, we need to clamp it to valid range in Dst.
Dst(constexpr_clamp(src, Src(L::min()), Src(L::max()))))
// Src and Dst have different signedness.
// Check if it's signed -> unsigend cast.
: std::is_signed<Src>::value && std::is_unsigned<Dst>::value
? (
// If src < 0, the result should be 0.
src < 0 ? Dst(0) :
// Otherwise, src >= 0. If src can fit into Dst, we can safely cast it
// without loss of accuracy.
sizeof(Src) <= sizeof(Dst) ? Dst(src) :
// If Src is larger in size than Dst, we need to ensure the result is
// at most Dst MAX.
Dst(constexpr_min(src, Src(L::max()))))
// It's unsigned -> signed cast.
: (
// Since Src is unsigned, and Dst is signed, Src can fit into Dst only
// when sizeof(Src) < sizeof(Dst).
sizeof(Src) < sizeof(Dst) ? Dst(src) :
// If Src does not fit into Dst, we need to ensure the result is at most
// Dst MAX.
Dst(constexpr_min(src, Src(L::max()))));
// clang-format on
}
namespace detail {
// Upper/lower bound values that could be accurately represented in both
// integral and float point types.
constexpr double kClampCastLowerBoundDoubleToInt64F = -9223372036854774784.0;
constexpr double kClampCastUpperBoundDoubleToInt64F = 9223372036854774784.0;
constexpr double kClampCastUpperBoundDoubleToUInt64F = 18446744073709549568.0;
constexpr float kClampCastLowerBoundFloatToInt32F = -2147483520.0f;
constexpr float kClampCastUpperBoundFloatToInt32F = 2147483520.0f;
constexpr float kClampCastUpperBoundFloatToUInt32F = 4294967040.0f;
// This works the same as constexpr_clamp, but the comparision are done in Src
// to prevent any implicit promotions.
template <typename D, typename S>
constexpr D constexpr_clamp_cast_helper(S src, S sl, S su, D dl, D du) {
return src < sl ? dl : (src > su ? du : D(src));
}
} // namespace detail
template <typename Dst, typename Src>
constexpr typename std::enable_if<std::is_floating_point<Src>::value, Dst>::type
constexpr_clamp_cast(Src src) {
static_assert(
std::is_integral<Dst>::value && sizeof(Dst) <= sizeof(int64_t),
"constexpr_clamp_cast can only cast into integral type (up to 64bit)");
using L = std::numeric_limits<Dst>;
// clang-format off
return
// Special case: cast NaN into 0.
// Using a trick here to portably check for NaN: f != f only if f is NaN.
// see: https://stackoverflow.com/a/570694
(src != src) ? Dst(0) :
// using `sizeof(Src) > sizeof(Dst)` as a heuristic that Dst can be
// represented in Src without loss of accuracy.
// see: https://en.wikipedia.org/wiki/Floating-point_arithmetic
sizeof(Src) > sizeof(Dst) ?
detail::constexpr_clamp_cast_helper(
src, Src(L::min()), Src(L::max()), L::min(), L::max()) :
// sizeof(Src) < sizeof(Dst) only happens when doing cast of
// 32bit float -> u/int64_t.
// Losslessly promote float into double, change into double -> u/int64_t.
sizeof(Src) < sizeof(Dst) ? (
src >= 0.0
? constexpr_clamp_cast<Dst>(
constexpr_clamp_cast<std::uint64_t>(double(src)))
: constexpr_clamp_cast<Dst>(
constexpr_clamp_cast<std::int64_t>(double(src)))) :
// The following are for sizeof(Src) == sizeof(Dst).
std::is_same<Src, double>::value && std::is_same<Dst, int64_t>::value ?
detail::constexpr_clamp_cast_helper(
double(src),
detail::kClampCastLowerBoundDoubleToInt64F,
detail::kClampCastUpperBoundDoubleToInt64F,
L::min(),
L::max()) :
std::is_same<Src, double>::value && std::is_same<Dst, uint64_t>::value ?
detail::constexpr_clamp_cast_helper(
double(src),
0.0,
detail::kClampCastUpperBoundDoubleToUInt64F,
L::min(),
L::max()) :
std::is_same<Src, float>::value && std::is_same<Dst, int32_t>::value ?
detail::constexpr_clamp_cast_helper(
float(src),
detail::kClampCastLowerBoundFloatToInt32F,
detail::kClampCastUpperBoundFloatToInt32F,
L::min(),
L::max()) :
detail::constexpr_clamp_cast_helper(
float(src),
0.0f,
detail::kClampCastUpperBoundFloatToUInt32F,
L::min(),
L::max());
// clang-format on
}
} // namespace folly
......@@ -17,9 +17,10 @@
#include <folly/ConstexprMath.h>
#include <folly/portability/GTest.h>
#include <limits>
#include <type_traits>
namespace {
class ConstexprMathTest : public testing::Test {};
} // namespace
......@@ -300,3 +301,108 @@ TEST_F(ConstexprMathTest, constexpr_sub_overflow_clamped) {
folly::constexpr_sub_overflow_clamped(uint64_t(12), uint64_t(23));
EXPECT_EQ(uint64_t(0), v7);
}
template <class F, class... Args>
void for_each_argument(F&& f, Args&&... args) {
[](...) {}((f(std::forward<Args>(args)), 0)...);
}
// float -> integral clamp cast
template <typename Src, typename Dst>
static typename std::enable_if<std::is_floating_point<Src>::value, void>::type
run_constexpr_clamp_cast_test(Src, Dst) {
constexpr auto kQuietNaN = std::numeric_limits<Src>::quiet_NaN();
constexpr auto kSignalingNaN = std::numeric_limits<Src>::signaling_NaN();
constexpr auto kPositiveInf = std::numeric_limits<Src>::infinity();
constexpr auto kNegativeInf = -kPositiveInf;
constexpr auto kDstMax = std::numeric_limits<Dst>::max();
constexpr auto kDstMin = std::numeric_limits<Dst>::min();
// Check f != f trick for identifying NaN.
EXPECT_TRUE(kQuietNaN != kQuietNaN);
EXPECT_TRUE(kSignalingNaN != kSignalingNaN);
EXPECT_FALSE(kPositiveInf != kPositiveInf);
EXPECT_FALSE(kNegativeInf != kNegativeInf);
// NaN -> 0
EXPECT_EQ(0, folly::constexpr_clamp_cast<Dst>(kQuietNaN));
EXPECT_EQ(0, folly::constexpr_clamp_cast<Dst>(kSignalingNaN));
// Inf -> Max/Min
EXPECT_EQ(kDstMax, folly::constexpr_clamp_cast<Dst>(kPositiveInf));
EXPECT_EQ(kDstMin, folly::constexpr_clamp_cast<Dst>(kNegativeInf));
// barely out of range -> Max/Min
EXPECT_EQ(kDstMax, folly::constexpr_clamp_cast<Dst>(Src(kDstMax) * 1.001));
EXPECT_EQ(kDstMin, folly::constexpr_clamp_cast<Dst>(Src(kDstMin) * 1.001));
// value in range -> same value
EXPECT_EQ(Dst(0), folly::constexpr_clamp_cast<Dst>(Src(0)));
EXPECT_EQ(Dst(123), folly::constexpr_clamp_cast<Dst>(Src(123)));
}
// integral -> integral clamp cast
template <typename Src, typename Dst>
static typename std::enable_if<std::is_integral<Src>::value, void>::type
run_constexpr_clamp_cast_test(Src, Dst) {
constexpr auto kSrcMax = std::numeric_limits<Src>::max();
constexpr auto kSrcMin = std::numeric_limits<Src>::min();
constexpr auto kDstMax = std::numeric_limits<Dst>::max();
constexpr auto kDstMin = std::numeric_limits<Dst>::min();
// value in range -> same value
EXPECT_EQ(Dst(0), folly::constexpr_clamp_cast<Dst>(Src(0)));
EXPECT_EQ(Dst(123), folly::constexpr_clamp_cast<Dst>(Src(123)));
// int -> uint
if (std::is_signed<Src>::value && std::is_unsigned<Dst>::value) {
EXPECT_EQ(Dst(0), folly::constexpr_clamp_cast<Dst>(Src(-123)));
}
if (sizeof(Src) > sizeof(Dst)) {
// range clamping
EXPECT_EQ(kDstMax, folly::constexpr_clamp_cast<Dst>(kSrcMax));
// int -> int
if (std::is_signed<Src>::value && std::is_signed<Dst>::value) {
EXPECT_EQ(kDstMin, folly::constexpr_clamp_cast<Dst>(kSrcMin));
}
} else if (
std::is_unsigned<Src>::value && std::is_signed<Dst>::value &&
sizeof(Src) == sizeof(Dst)) {
// uint -> int, same size
EXPECT_EQ(kDstMax, folly::constexpr_clamp_cast<Dst>(kSrcMax));
}
}
TEST_F(ConstexprMathTest, constexpr_clamp_cast) {
for_each_argument(
[](auto dst) {
for_each_argument(
[&](auto src) { run_constexpr_clamp_cast_test(src, dst); },
// source types
float(),
double(),
(long double)(0),
int8_t(),
uint8_t(),
int16_t(),
uint16_t(),
int32_t(),
uint32_t(),
int64_t(),
uint64_t());
},
// dst types
int8_t(),
uint8_t(),
int16_t(),
uint16_t(),
int32_t(),
uint32_t(),
int64_t(),
uint64_t());
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment