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

后端 未结 1 1990
孤独总比滥情好
孤独总比滥情好 2021-01-16 04:22

Recently, I saw this question which asks how you can divide integers with ceil rounding (towards positive infinity). Unfortunately the answers either don\'t work fo

相关标签:
1条回答
  • 2021-01-16 04:44

    In C++, the / division operation rounds using truncate (towards zero) by default. We can adjust the result of division towards zero to implement other rounding modes. Note that when the division has no remainder, all rounding modes are equivalent because no rounding is necessary.

    With that in mind, we can implement the different rounding modes. But before we get started, we will need a helper template for the return types so that we don't use auto return types everywhere:

    #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 (towards +∞)

    Ceil rounding is identical to truncate rounding for negative quotients, but for positive quotients and nonzero remainders we round away from zero. This means that we increment the quotient for nonzero remainders.

    Thanks to if-constexpr, we can implement everything using only a single 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
        }
    }
    

    At first glance, the implementations for signed types seem expensive, because they use both an integer division and a modulo division. However, on modern architectures division typically sets a flag that indicates whether there was a remainder, so x % y != 0 is completely free in this case.

    You might also be wondering why we don't compute the quotient first and then check if the quotient is positive. This wouldn't work because we already lost precision during this division, so we can't perform this test afterwards. For example:

    -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 (towards -∞)

    Floor rounding is identical to truncate for positive quotients, but for negative quotients we round away from zero. This means that we decrement the quotient for nonzero remainders.

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

    The implementation is almost identical to that of div_ceil.

    Away From Zero

    Away from zero is the exact opposite of truncate. Basically, we need to increment or decrement depending on the sign of the quotient, but only if there is a remainder. This can be expressed as adding the sgn of the quotient onto the result:

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

    Using this helper function, we can fully implement up rounding:

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

    Unresolved Problems

    Unfortunately these functions won't work for all possible inputs, which is a problem that we can not solve. For example, dividing uint32_t{3 billion} / int32_t{1} results in int32_t(3 billion) which isn't representable using a 32-bit signed integer. We get an underflow in this case.

    Using larger return types would be an option for everything but 64-bit integers, where there isn't a larger alternative available. Hence, it is the responsibility of the user to ensure that when they pass an unsigned number into this function, it is equivalent to its signed representation.

    0 讨论(0)
提交回复
热议问题