首页 > 解决方案 > 根据线性顺序使用最接近的值将数字裁剪到另一种类型的数值限制

问题描述

使用 C++,我想将一个数字剪辑到另一个可能小于源类型的算术类型的数值限制。约束是,结果值应该是整数集的线性顺序中最接近源值的值。

例子:

用例:图书馆的使用者只对提供的数字范围的一小部分感兴趣。为了安全地处理超出目标范围的数字,转换需要匹配上述约束(因此不能选择强制转换)。

实现(需要 C++17)

这就是我想出的:

#include <limits>

template <typename T>
constexpr T clip(T v, T lo, T hi)
{
    return (v < lo) ? lo : (hi < v) ? hi : v;
}

template <typename TargetType, typename SourceType>
constexpr TargetType clipToTargetTypeNumericLimits(SourceType v)
{
    // This condition is necessary to not break integer -> floating point conversion
    // (the `static_cast<SourceType>(lo/hi)` below will give unwanted results),
    // while at the same time still clipping floating point type -> shorter floating point type.
    if constexpr (std::is_floating_point<TargetType>::value
        && !(
            std::is_floating_point<SourceType>::value
            && (std::numeric_limits<SourceType>::max_exponent
                >= std::numeric_limits<TargetType>::max_exponent)
            )
        )
    {
        return static_cast<TargetType>(v);
    }
    else
    {
        constexpr auto lo = std::numeric_limits<TargetType>::min();
        constexpr auto hi = std::numeric_limits<TargetType>::max();
        constexpr auto lo_sourceType = static_cast<SourceType>(lo);
        constexpr auto hi_sourceType = static_cast<SourceType>(hi);
        return static_cast<TargetType>(clip<SourceType>(v, lo_sourceType, hi_sourceType));
        // ... cannot use std::clamp, since it might assert when (lo_sourceType > hi_sourceType)
        // return static_cast<TargetType>(std::clamp(v, lo_sourceType, hi_sourceType));
    }
}

问题

  1. 是否有在 boost 或其他库中已经可用的实现(我看了,但找不到)?
  2. 而不是if constexpr条件,它可能更简洁,只是禁止为整数源类型浮点目标类型调用函数。这也可能使其与 C++11/14 兼容。如何std::enable_if用于检查源类型和目标类型?
  3. C++20 概念将使编译错误更具可读性。这个函数在概念上会是什么样子?(是否应该将if constexpr条件删除并替换为类似于之前问题的概念?)
  4. 您是否看到可能的意外行为?

谢谢!


测试

#include <cassert>
#include <iostream>

template <typename SourceType, SourceType s,
          typename TargetType, TargetType expectedResult>
void test_clipToTargetTypeNumericLimits()
{
    constexpr auto result = clipToTargetTypeNumericLimits<TargetType>(s);
    std::cout << s << " -> " << +result << std::endl;
    static_assert(expectedResult == result);
}

template <typename SourceType, typename TargetType, typename S, typename T>
void test_clipToTargetTypeNumericLimits(S f_s, T f_expectedResult)
{
    constexpr auto s = f_s();
    constexpr auto expectedResult = f_expectedResult();
    constexpr auto result = clipToTargetTypeNumericLimits<TargetType>(s);
    std::cout << s << " -> " << +result << std::endl;
    static_assert(expectedResult == result);
}

int main()
{
    std::cout << "\n--- negative signed integer -> unsigned ---" << std::endl;
    test_clipToTargetTypeNumericLimits<
        int32_t,    -42,                // source value
        uint32_t,     0>();             // expected result

    std::cout << "\n--- integer -> shorter integer type ---" << std::endl;
    test_clipToTargetTypeNumericLimits<
        uint32_t,   UINT32_MAX,         // source value
        uint16_t,   UINT16_MAX>();      // expected result
    test_clipToTargetTypeNumericLimits<
        uint32_t,   UINT32_MAX,         // source value
        char,       INT8_MAX>();        // expected result
    test_clipToTargetTypeNumericLimits<
        int64_t,    INT32_MIN + 42,     // source value
        int32_t,    INT32_MIN + 42>();  // expected result

    std::cout << "\n--- floating point -> integer ---" << std::endl;
    test_clipToTargetTypeNumericLimits<
        float,
        unsigned>(
            [](){ return -42.7f; },         // source value
            [](){ return   0; });           // expected result
    test_clipToTargetTypeNumericLimits<
        double,
        uint8_t>(
            [](){ return 1024.5; },         // source value
            [](){ return UINT8_MAX; });     // expected result

    std::cout << "\n--- floating point -> shorter floating point type ---" << std::endl;
    test_clipToTargetTypeNumericLimits<
        double,
        float>(
            [](){ return std::numeric_limits<double>::min(); }, // source value
            [](){ return std::numeric_limits<float>::min(); }); // expected result

    test_clipToTargetTypeNumericLimits<
        long double,
        float>(
            [](){ return std::numeric_limits<long double>::max(); }, // source value
            [](){ return std::numeric_limits<float>::max(); });      // expected result

    std::cout << "\n--- integer -> floating point ---" << std::endl;
    test_clipToTargetTypeNumericLimits<
        uint64_t,
        float>(
            [](){ return UINT64_MAX; },     // source value
            [](){ return UINT64_MAX; });    // expected result

    std::cout << "\n--- to bool ---" << std::endl;
    constexpr auto b_f = clipToTargetTypeNumericLimits<bool>(0);
    std::cout << 0 << " -> " << std::boolalpha << b_f << std::endl;
    static_assert(0 == b_f);
    constexpr auto ldbl_max = std::numeric_limits<long double>::max();
    constexpr auto b_t = clipToTargetTypeNumericLimits<bool>(ldbl_max);
    std::cout << ldbl_max << " -> " << std::boolalpha << b_t << std::endl;
    static_assert(1 == b_t);

    std::cout << "\n--- evaluation at runtime ---" << std::endl;
    const auto duration_ticks = std::chrono::system_clock::now().time_since_epoch().count();
    const auto i8_at_runtime = clipToTargetTypeNumericLimits<int8_t>(duration_ticks);
    std::cout << duration_ticks << " -> " << +i8_at_runtime << std::endl;
    assert(INT8_MAX == i8_at_runtime);
}

可能的输出:

--- negative signed integer -> unsigned ---
-42 -> 0

--- integer -> shorter integer type ---
4294967295 -> 65535
4294967295 -> 127
-2147483606 -> -2147483606

--- floating point -> integer ---
-42.7 -> 0
1024.5 -> 255

--- floating point -> shorter floating point type ---
2.22507e-308 -> 1.17549e-38
1.18973e+4932 -> 3.40282e+38

--- integer -> floating point ---
18446744073709551615 -> 1.84467e+19

--- to bool ---
0 -> false
1.18973e+4932 -> true

--- evaluation at runtime ---
1585315690266730 -> 127

标签: c++

解决方案


我不太明白那个复杂的 constexpr 条件 - 注释似乎与任何现有代码都不匹配。

我也没有很仔细地检查逻辑。

无论如何,我决定解决由于使用 constexpr-if 而需要 C++17 的问题,而不是试图弄清楚你要做什么。

有很多方法可以做到这一点......我只会展示三种可能性......

在每一个中,我只是让你的条件和你写的一样。

第一个使用 enable if,但如果您先创建元函数,则更容易阅读。

namespace detail {
template <typename TargetT, typename SourceT>
struct IsFloatConversion
: std::bool_constant<
    std::is_floating_point<TargetT>::value
        && !(std::is_floating_point<SourceT>::value
                && (std::numeric_limits<SourceT>::max_exponent
                    >= std::numeric_limits<TargetT>::max_exponent))>
{
};
}
template <typename TargetType, typename SourceType>
constexpr
std::enable_if_t<detail::IsFloatConversion<TargetType, SourceType>::value,
        TargetType>
clipToTargetTypeNumericLimits(SourceType v)
{
    return static_cast<TargetType>(v);
}
template <typename TargetType, typename SourceType>
constexpr
std::enable_if_t<not detail::IsFloatConversion<TargetType, SourceType>::value,
        TargetType>
clipToTargetTypeNumericLimits(SourceType v)
{
    constexpr auto lo = std::numeric_limits<TargetType>::min();
    constexpr auto hi = std::numeric_limits<TargetType>::max();
    constexpr auto lo_sourceType = static_cast<SourceType>(lo);
    constexpr auto hi_sourceType = static_cast<SourceType>(hi);
    return static_cast<TargetType>(
        clip<SourceType>(v, lo_sourceType, hi_sourceType));
}

第二种方式只是标签调度。

namespace detail {
template <typename TargetType, typename SourceType>
constexpr TargetType
clipToTargetTypeNumericLimitsImpl(SourceType v, std::true_type)
{
    return static_cast<TargetType>(v);
}
template <typename TargetType, typename SourceType>
constexpr TargetType
clipToTargetTypeNumericLimitsImpl(SourceType v, std::false_type)
{
    constexpr auto lo = std::numeric_limits<TargetType>::min();
    constexpr auto hi = std::numeric_limits<TargetType>::max();
    constexpr auto lo_sourceType = static_cast<SourceType>(lo);
    constexpr auto hi_sourceType = static_cast<SourceType>(hi);
    return static_cast<TargetType>(
        clip<SourceType>(v, lo_sourceType, hi_sourceType));
}
}
template <typename TargetType, typename SourceType>
constexpr TargetType clipToTargetTypeNumericLimits(SourceType v)
{
    constexpr bool dofloat = std::is_floating_point<TargetType>::value
        && !(
            std::is_floating_point<SourceType>::value
            && (std::numeric_limits<SourceType>::max_exponent
                >= std::numeric_limits<TargetType>::max_exponent)
            );
    return detail::clipToTargetTypeNumericLimitsImpl<TargetType>(v,
        std::integral_constant<bool, dofloat>{});
}

第三个类似,但使用专业化。

namespace detail {
template <bool>
struct Clip
{
    template <typename TargetType, typename SourceType>
    static constexpr TargetType _ (SourceType v)
    {
        return static_cast<TargetType>(v);
    }
};
template <>
struct Clip<false>
{
    template <typename TargetType, typename SourceType>
    static constexpr TargetType _ (SourceType v)
    {
        constexpr auto lo = std::numeric_limits<TargetType>::min();
        constexpr auto hi = std::numeric_limits<TargetType>::max();
        constexpr auto lo_sourceType = static_cast<SourceType>(lo);
        constexpr auto hi_sourceType = static_cast<SourceType>(hi);
        return static_cast<TargetType>(
            clip<SourceType>(v, lo_sourceType, hi_sourceType));
    }
};
}
template <typename TargetType, typename SourceType>
constexpr TargetType clipToTargetTypeNumericLimits(SourceType v)
{
    constexpr bool dofloat = std::is_floating_point<TargetType>::value
        && !(
            std::is_floating_point<SourceType>::value
            && (std::numeric_limits<SourceType>::max_exponent
                >= std::numeric_limits<TargetType>::max_exponent)
            );
    return detail::Clip<dofloat>::template _<TargetType>(v);
}

还有其他方法,但这应该足以让您选择您喜欢的东西 - 或者构建您更喜欢的东西。


推荐阅读