【问题标题】:Divide integers with floor, ceil and outwards rounding modes in C++在 C++ 中用 floor、ceil 和向外舍入模式除整数
【发布时间】:2020-08-16 11:47:31
【问题描述】:

最近,我看到this question 询问如何用 ceil 舍入(朝向正无穷大)除整数。不幸的是,答案要么不适用于有符号整数,要么存在下溢和上溢问题。

例如,accepted answer 有这个解决方案:

q = 1 + ((x - 1) / y);

x 为零时,~0 存在下溢,结果不正确。

如何为有符号和无符号整数正确实现 ceil 舍入以及如何实现其他舍入模式,如 floor(朝向负无穷大)和 outwards(远离零)?

【问题讨论】:

  • @Hi-IloveSO 除了浮点数不能以完全精度表示所有整数,而且与仅使用整数相比,这种转换需要很多时间。 See for yourself。浮点版本甚至还不包括ceil 实现,which isn't simple。如果它很简单,它就会被内联。
  • double 在大多数情况下应该能够覆盖int 的整个范围。浮点数很简单,只需添加或减去0.5 并将其转换回整数。时间可以忽略不计。
  • @Hi-IloveSO 它可能能够覆盖int 的范围,但肯定不能覆盖uint64_t 的范围。再一次,浮点运算需要更长的时间。加法至少需要 3 个循环,除法需要 30-60 个循环。在这个时候,您可以多次使用整数进行舍入。顺便说一下,0.5 的增加对 round 有用,而不是对 floorceilup i>.
  • @Hi-IloveSO “只需加或减 0.5 并将其转换回整数” - 这不适用于所有数字。参见例如stackoverflow.com/q/35143242/5910058
  • @Hi-IloveSO 你为什么称它为过度工程?编译输出很短,每个函数只有大约 20 行代码,是唯一有效的解决方案。

标签: c++ math integer rounding division


【解决方案1】:

在 C++ 中,/ 除法运算默认使用 truncate(接近零)进行舍入。我们可以将除法的结果调整为零以实现其他舍入模式。 请注意,当除法没有余数时,所有舍入模式都是等效的,因为不需要舍入。

考虑到这一点,我们可以实现不同的舍入模式。 但在开始之前,我们需要一个返回类型的帮助模板,这样我们就不会到处使用auto 返回类型:

#include <type_traits>

/**
 * Similar to std::common_type_t<A, B>, but if A or B are signed, the result will also be signed.
 *
 * This differs from the regular type promotion rules, where signed types are promoted to unsigned types.
 */
template <typename A, typename B>
using common_signed_t =
    std::conditional_t<std::is_unsigned_v<A> && std::is_unsigned_v<B>,
                       std::common_type_t<A, B>,
                       std::common_type_t<std::make_signed_t<A>, std::make_signed_t<B>>>;

Ceil(朝向 +∞)

Ceil 舍入与 truncate 舍入负商相同,但对于正商和非零余数,我们从零舍入。这意味着我们增加非零余数的商。

感谢if-constexpr,我们可以只使用一个函数来实现一切:

template <typename Dividend, typename Divisor>
constexpr common_signed_t<Dividend, Divisor> div_ceil(Dividend x, Divisor y)
{
    if constexpr (std::is_unsigned_v<Dividend> && std::is_unsigned_v<Divisor>) {
        // quotient is always positive
        return x / y + (x % y != 0);  // uint / uint
    }
    else if constexpr (std::is_signed_v<Dividend> && std::is_unsigned_v<Divisor>) {
        auto sy = static_cast<std::make_signed_t<Divisor>>(y);
        bool quotientPositive = x >= 0;
        return x / sy + (x % sy != 0 && quotientPositive);  // int / uint
    }
    else if constexpr (std::is_unsigned_v<Dividend> && std::is_signed_v<Divisor>) {
        auto sx = static_cast<std::make_signed_t<Dividend>>(x);
        bool quotientPositive = y >= 0;
        return sx / y + (sx % y != 0 && quotientPositive);  // uint / int
    }
    else {
        bool quotientPositive = (y >= 0) == (x >= 0);
        return x / y + (x % y != 0 && quotientPositive);  // int / int
    }
}

乍一看,有符号类型的实现似乎很昂贵,因为它们同时使用整数除法和模除法。但是,在现代架构中,除法通常会设置一个标志来指示是否存在余数,因此 x % y != 0 在这种情况下是完全免费的。

您可能还想知道为什么我们不先计算商,然后检查商是否为正。这是行不通的,因为我们在这个划分过程中已经失去了精度,所以我们不能在之后进行这个测试。例如:

-1 / 2 = -0.5
// C++ already rounds towards zero
-0.5 -> 0
// Now we think that the quotient is positive, even though it is negative.
// So we mistakenly round up again:
0 -> 1

地板(朝向 -∞)

对于正商,

Floor 舍入与 truncate 相同,但对于负商,我们从零舍入。这意味着我们减少非零余数的商。

template <typename Dividend, typename Divisor>
constexpr common_signed_t<Dividend, Divisor> div_floor(Dividend x, Divisor y)
{
    if constexpr (std::is_unsigned_v<Dividend> && std::is_unsigned_v<Divisor>) {
        // quotient is never negative
        return x / y;  // uint / uint
    }
    else if constexpr (std::is_signed_v<Dividend> && std::is_unsigned_v<Divisor>) {
        auto sy = static_cast<std::make_signed_t<Divisor>>(y);
        bool quotientNegative = x < 0;
        return x / sy - (x % sy != 0 && quotientNegative);  // int / uint
    }
    else if constexpr (std::is_unsigned_v<Dividend> && std::is_signed_v<Divisor>) {
        auto sx = static_cast<std::make_signed_t<Dividend>>(x);
        bool quotientNegative = y < 0;
        return sx / y - (sx % y != 0 && quotientNegative);  // uint / int
    }
    else {
        bool quotientNegative = (y < 0) != (x < 0);
        return x / y - (x % y != 0 && quotientNegative);  // int / int
    }
}

实现与div_ceil几乎完全相同。

远离零

远离零截断正好相反。基本上,我们需要根据商的符号来增加或减少,但前提是有余数。这可以表示为将商的sgn 添加到结果中:

template <typename Int>
constexpr signed char sgn(Int n)
{
    return (n > Int{0}) - (n < Int{0});
};

使用这个辅助函数,我们可以完全实现up四舍五入:

template <typename Dividend, typename Divisor>
constexpr common_signed_t<Dividend, Divisor> div_up(Dividend x, Divisor y)
{
    if constexpr (std::is_unsigned_v<Dividend> && std::is_unsigned_v<Divisor>) {
        // sgn is always 1
        return x / y + (x % y != 0);  // uint / uint
    }
    else if constexpr (std::is_signed_v<Dividend> && std::is_unsigned_v<Divisor>) {
        auto sy = static_cast<std::make_signed_t<Divisor>>(y);
        signed char quotientSgn = sgn(x);
        return x / sy + (x % sy != 0) * quotientSgn;  // int / uint
    }
    else if constexpr (std::is_unsigned_v<Dividend> && std::is_signed_v<Divisor>) {
        auto sx = static_cast<std::make_signed_t<Dividend>>(x);
        signed char quotientSgn = sgn(y);
        return sx / y + (sx % y != 0) * quotientSgn;  // uint / int
    }
    else {
        signed char quotientSgn = sgn(x) * sgn(y);
        return x / y + (x % y != 0) * quotientSgn;  // int / int
    }
}

未解决的问题

很遗憾,这些函数不适用于所有可能的输入,这是我们无法解决的问题。 例如,将uint32_t{3 billion} / int32_t{1} 相除得到int32_t(3 billion),它不能使用32 位有符号整数表示。 在这种情况下,我们得到了一个下溢。

对于除 64 位整数之外的所有内容,都可以选择使用更大的返回类型,因为没有更大的替代方案可用。 因此,用户有责任确保当他们将无符号数传递给此函数时,它等同于其有符号表示。

【讨论】:

  • div_up() 是一个令人困惑的名字,它暗示“朝向+无穷大”;也许div_out()div_away_from_0()。 2. 你能在所有这些中的某个地方制作一个标题吗?例如pastebin、github gist 等?
【解决方案2】:

如有必要,我会简化并使用同构参数类型,并让用户对异构输入进行显式类型转换。这样,可能的下溢和上溢就会移到这些函数之外。当然,正常的 UB 案例也适用,例如。除以零和 std::numeric_limits::min() 除以 -1 为有符号 T。

#include <type_traits>

//Division round up, aka take the ceiling, aka round toward positive infinity, eg. -1.5 -> -1, 1.5 -> 2
template<typename T>
    requires std::is_integral_v<T>
constexpr T divRndUp(T a, T b) noexcept
{
    if constexpr (std::is_unsigned_v<T>)
        return a / b + (a % b != 0);
    else
        return a / b + (a % b != 0 && ((a < 0) == (b < 0)));
}

//Division round down, aka take the floor, aka round toward negative infinity, eg. -1.5 -> -2, 1.5 -> 1
template<typename T>
    requires std::is_integral_v<T>
constexpr T divRndDwn(T a, T b) noexcept
{
    if constexpr (std::is_unsigned_v<T>)
        return a / b;
    else
        return a / b - (a % b != 0 && ((a < 0) != (b < 0)));
}

//Division round out, aka round out away from zero, aka round toward infinity, eg. -1.5 -> -2, 1.5 -> 2
template<typename T>
    requires std::is_integral_v<T>
constexpr T divRndOut(T a, T b) noexcept
{
    if constexpr (std::is_unsigned_v<T>)
        return a / b + (a % b != 0);
    else
        return a / b + (a % b != 0 && ((a < 0) == (b < 0))) - (a % b != 0 && ((a < 0) != (b < 0)));
}

//Division round in, aka truncate, aka round in toward zero, aka round away from infinity, eg. -1.5 -> -1, 1.5 -> 1
template<typename T>
    requires std::is_integral_v<T>
constexpr T divRndIn(T a, T b) noexcept
{
    return a / b;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-02-19
    • 2019-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-23
    • 2019-02-24
    相关资源
    最近更新 更多