簡體   English   中英

在 C++ 中用 floor、ceil 和向外舍入模式除整數

[英]Divide integers with floor, ceil and outwards rounding modes in C++

最近,我看到了這個問題,它詢問如何用ceil舍入(朝向正無窮大)來划分整數。 不幸的是,答案要么不適用於有符號整數,要么存在下溢和溢出問題。

例如,接受的答案有這個解決方案:

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

x為零時,下溢到~0 ,結果不正確。

如何為有符號和無符號整數正確實現ceil舍入,以及如何實現其他舍入模式,如floor (朝向負無窮大)和向外(遠離零)?

在 C++ 中, /除法運算默認使用截斷(向零)舍入。 我們可以將除法的結果調整為零以實現其他舍入模式。 請注意,當除法沒有余數時,所有舍入模式都是等效的,因為不需要舍入。

考慮到這一點,我們可以實現不同的舍入模式。 但在我們開始之前,我們需要一個返回類型的幫助模板,這樣我們就不會到處使用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舍入與負商的截斷舍入相同,但對於正商和非零余數,我們從零舍入。 這意味着我們增加非零余數的商。

多虧了if-constexpr ,我們可以只使用一個 function 來實現一切:

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
    }
}

乍一看,簽名類型的實現似乎很昂貴,因為它們同時使用 integer 除法和模除法。 但是,在現代架構中,除法通常會設置一個標志,指示是否存在余數,因此在這種情況下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

地板(朝向 -∞)

對於正商,下限舍入與截斷相同,但對於負商,我們從零舍入。 這意味着我們減少非零余數的商。

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相同。

遠離零

遠離零truncate完全相反。 基本上,我們需要根據商的符號來增加或減少,但前提是有余數。 這可以表示為將商的sgn添加到結果中:

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

使用這個助手 function,我們可以完全實現向上舍入:

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 位有符號 integer 表示。 在這種情況下,我們得到一個下溢。

使用更大的返回類型將是除了 64 位整數之外的所有選項,因為沒有更大的替代方案可用。 因此,用戶有責任確保當他們將一個無符號數傳遞給這個 function 時,它等同於它的有符號表示。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM