C++で四捨五入を実装する方法について

AI実装検定のご案内

C++で四捨五入を実装する場合は、標準ライブラリのstd::roundを使用する方法が基本です。

ただし、整数へ丸める場合、小数第N位まで残す場合、金額を扱う場合など、用途によって適切な実装方法は異なります。

また、doublefloatを使用する場合は、浮動小数点数特有の誤差にも注意が必要です。

目次

std::roundを使った基本的な四捨五入

std::roundの基本動作

std::roundは、指定した浮動小数点数を最も近い整数値へ丸める関数です。

利用するには、<cmath>をインクルードします。

#include <cmath>
#include <iostream>

int main()
{
    double value = 3.6;
    double result = std::round(value);

    std::cout << result << '\n';  // 4
}

基本的な動作は次のとおりです。

std::round(3.4);  // 3
std::round(3.5);  // 4
std::round(3.6);  // 4

小数部分が0.5未満の場合は小さい整数へ、小数部分が0.5以上の場合は大きい整数へ丸められます。

負の数の四捨五入

std::roundは、負の数にも対応しています。

std::round(-3.4);  // -3
std::round(-3.5);  // -4
std::round(-3.6);  // -4

小数部分がちょうど0.5の場合は、ゼロから遠ざかる方向へ丸められます。

そのため、-3.5-3ではなく-4になります。

元の値丸めた結果
2.42
2.53
-2.4-2
-2.5-3

現在の丸めモードには影響されない

std::roundは、実行環境に設定されている浮動小数点の丸めモードにかかわらず、中間値をゼロから遠ざける方向へ丸めます。

一般的な四捨五入を実装する場合は、基本的にstd::roundを使用すれば問題ありません。

四捨五入した結果を整数型で取得する方法

static_castで整数型へ変換する

std::roundの戻り値は整数型ではなく、浮動小数点型です。

整数型として取得したい場合は、static_castで変換できます。

#include <cmath>
#include <iostream>

int main()
{
    double value = 12.7;

    int result =
        static_cast<int>(std::round(value));

    std::cout << result << '\n';  // 13
}

std::lroundとstd::llroundを使う

整数型の結果を直接取得したい場合は、std::lroundstd::llroundを使用できます。

#include <cmath>
#include <iostream>

int main()
{
    double value = 12345.6;

    long result1 = std::lround(value);
    long long result2 = std::llround(value);

    std::cout << result1 << '\n';  // 12346
    std::cout << result2 << '\n';  // 12346
}

各関数の戻り値は次のとおりです。

関数戻り値の型
std::round浮動小数点型
std::lroundlong
std::llroundlong long

整数型の範囲外に注意する

丸めた結果が整数型の表現可能範囲を超える場合は、正常な値を取得できません。

double value = 1.0e100;
long long result = std::llround(value);

安全に処理する場合は、変換前に値の範囲を確認する必要があります。

#include <cmath>
#include <limits>
#include <stdexcept>

long long checkedRoundToLongLong(double value)
{
    if (!std::isfinite(value)) {
        throw std::invalid_argument(
            "value must be finite");
    }

    const double minValue =
        static_cast<double>(
            std::numeric_limits<long long>::min());

    const double maxValue =
        static_cast<double>(
            std::numeric_limits<long long>::max());

    if (value < minValue || value > maxValue) {
        throw std::out_of_range(
            "value is outside long long range");
    }

    return std::llround(value);
}

ただし、doubleではlong longの境界値を完全に正確に表現できない場合があります。

境界付近の値を厳密に扱う場合は、より慎重な設計が必要です。

小数第N位で四捨五入する方法

小数第2位まで残す場合

小数第2位まで残す場合は、値を100倍してからstd::roundで丸め、最後に100で割ります。

#include <cmath>
#include <iostream>

int main()
{
    double value = 12.3456;

    double result =
        std::round(value * 100.0) / 100.0;

    std::cout << result << '\n';  // 12.35
}

計算の流れは次のとおりです。

  1. 値を100倍する
  2. 丸めたい桁を整数部分へ移動する
  3. std::roundで丸める
  4. 100で割って元の桁へ戻す

小数第1位まで残す場合

小数第1位まで残す場合は、10倍してから丸めます。

double result =
    std::round(value * 10.0) / 10.0;

小数第3位まで残す場合

小数第3位まで残す場合は、1000倍してから丸めます。

double result =
    std::round(value * 1000.0) / 1000.0;

汎用関数として実装する

残したい小数点以下の桁数を指定できる関数は、次のように実装できます。

#include <cmath>

double roundToDigits(double value, int digits)
{
    double factor = std::pow(10.0, digits);

    return std::round(value * factor) / factor;
}

使用例は次のとおりです。

#include <iostream>

int main()
{
    double value = 123.456789;

    std::cout
        << roundToDigits(value, 0)
        << '\n';  // 123

    std::cout
        << roundToDigits(value, 1)
        << '\n';  // 123.5

    std::cout
        << roundToDigits(value, 2)
        << '\n';  // 123.46

    std::cout
        << roundToDigits(value, 3)
        << '\n';  // 123.457
}

十の位や百の位で四捨五入する方法

十の位で四捨五入する

整数部分を十の位で四捨五入する場合は、10で割ってから丸め、最後に10を掛けます。

#include <cmath>
#include <iostream>

int main()
{
    double value = 1234.0;

    double result =
        std::round(value / 10.0) * 10.0;

    std::cout << result << '\n';  // 1230
}

1235の場合は、1240になります。

double value = 1235.0;

double result =
    std::round(value / 10.0) * 10.0;

// 1240

百の位で四捨五入する

百の位で四捨五入する場合は、100で割ってから丸め、100を掛けます。

double result =
    std::round(value / 100.0) * 100.0;

小数部と整数部の両方に対応する

正のdigitsで小数点以下の桁、負のdigitsで整数部分の桁を指定する関数は、次のように実装できます。

#include <cmath>

double roundToPosition(double value, int digits)
{
    if (digits >= 0) {
        const double factor =
            std::pow(10.0, digits);

        return
            std::round(value * factor) / factor;
    }

    const double factor =
        std::pow(10.0, -digits);

    return
        std::round(value / factor) * factor;
}

使用例は次のとおりです。

#include <iostream>

int main()
{
    double value = 1234.567;

    std::cout
        << roundToPosition(value, 2)
        << '\n';  // 1234.57

    std::cout
        << roundToPosition(value, 0)
        << '\n';  // 1235

    std::cout
        << roundToPosition(value, -1)
        << '\n';  // 1230

    std::cout
        << roundToPosition(value, -2)
        << '\n';  // 1200
}

valueに0.5を加える方法が推奨されない理由

正の数では動作する場合がある

古いコードでは、次のような実装が使われることがあります。

int result =
    static_cast<int>(value + 0.5);

正の数だけを扱う場合は、一見正しく動作します。

double value = 3.6;

int result =
    static_cast<int>(value + 0.5);

// 4

負の数では正しく動作しない

負の数では、期待する四捨五入結果にならない場合があります。

double value = -3.6;

int result =
    static_cast<int>(value + 0.5);

計算結果は次のようになります。

-3.6 + 0.5 = -3.1

浮動小数点型から整数型へ変換すると、小数部分はゼロ方向へ取り除かれます。

そのため、結果は-3になります。

通常の四捨五入では-4が期待されるため、この方法は適切ではありません。

std::roundを使用する

正の数と負の数を正しく扱うには、std::roundを使用します。

int result =
    static_cast<int>(std::round(value));

ただし、変換後の整数型に値が収まるかどうかは、別途確認する必要があります。

round、floor、ceil、truncの違い

std::round

std::roundは、最も近い整数へ丸めます。

中間値はゼロから遠い方向へ丸められます。

std::round(3.5);   // 4
std::round(-3.5);  // -4

std::floor

std::floorは、負の無限大方向へ丸めます。

std::floor(3.8);   // 3
std::floor(-3.2);  // -4

std::ceil

std::ceilは、正の無限大方向へ丸めます。

std::ceil(3.2);    // 4
std::ceil(-3.8);   // -3

std::trunc

std::truncは、小数部分を取り除き、ゼロ方向へ丸めます。

std::trunc(3.8);   // 3
std::trunc(-3.8);  // -3

各関数の違いは次のとおりです。

関数3.7-3.7丸め方向
std::round4-4最も近い整数
std::floor3-4負の無限大方向
std::ceil4-3正の無限大方向
std::trunc3-3ゼロ方向

「切り捨て」という表現は、負の数を扱う場合に意味が曖昧になります。

そのため、処理内容を説明するときは、「負の無限大方向」や「ゼロ方向」など、丸める方向を明示するのが適切です。

表示だけ小数点以下の桁数を整える方法

std::fixedとstd::setprecisionを使う

計算結果そのものを変更せず、表示だけ小数点以下2桁にしたい場合は、std::fixedstd::setprecisionを使用します。

#include <iomanip>
#include <iostream>

int main()
{
    double value = 12.3456;

    std::cout
        << std::fixed
        << std::setprecision(2)
        << value
        << '\n';
}

出力結果は次のようになります。

12.35

値そのものは変更されない

std::setprecisionは、出力時の表示形式を変更する機能です。

変数に格納されている値そのものは変更されません。

double value = 12.3456;

std::cout
    << std::fixed
    << std::setprecision(2)
    << value;

// value自体は変更されない

後続の計算でも12.3456に相当する値を使用したい場合は、そのままで問題ありません。

後続の計算でも12.35に丸めた値を使いたい場合は、std::roundによる処理が必要です。

double rounded =
    std::round(value * 100.0) / 100.0;

std::fixedの有無で意味が変わる

std::fixedを指定した場合、std::setprecision(2)は小数点以下の桁数を表します。

std::cout
    << std::fixed
    << std::setprecision(2);

一方、std::fixedを指定しない場合、通常は有効桁数として扱われます。

std::cout
    << std::setprecision(2)
    << 123.456;

小数点以下2桁で表示したい場合は、必ずstd::fixedと組み合わせます。

浮動小数点数の誤差に注意する

10進小数を正確に表現できない場合がある

doublefloatは、多くの10進小数を内部で正確に表現できません。

これは、浮動小数点数が基本的に2進数で表現されるためです。

例えば、次のような10進小数は、内部では非常に近い別の値として保持される可能性があります。

double value = 2.675;

倍率を掛ける処理でも誤差が発生する

次の処理では、複数の箇所で誤差が発生する可能性があります。

std::round(value * 100.0) / 100.0;

誤差が発生する可能性がある箇所は次のとおりです。

  • valueの内部表現
  • value * 100.0の乗算
  • 丸め後に100で割る処理
  • 丸めた結果を再び浮動小数点数として保持する処理

そのため、数学的には同じように見える値でも、環境や計算の途中結果によって、期待した丸め結果と異なる場合があります。

2.675の結果は環境で確認する

次の式が必ず2.67または2.68になると断定することは適切ではありません。

std::round(2.675 * 100.0) / 100.0;

2.675の内部表現だけでなく、100倍するときにも浮動小数点の丸めが行われるためです。

実際の値を確認する場合は、高い精度で出力します。

#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>

int main()
{
    double value = 2.675;
    double scaled = value * 100.0;
    double rounded =
        std::round(scaled) / 100.0;

    std::cout
        << std::setprecision(
            std::numeric_limits<double>::max_digits10)
        << "value   = " << value << '\n'
        << "scaled  = " << scaled << '\n'
        << "rounded = " << rounded << '\n';
}

実際の結果は、使用しているコンパイラや処理系で確認するのが確実です。

金額計算で四捨五入する場合の注意点

金額は最小単位の整数で管理する

金額計算では、doubleをそのまま使用するより、最小単位を整数として保持する方法が適しています。

例えば、10.25ドルをセント単位で管理する場合は、次のようにします。

#include <cstdint>

std::int64_t amountInCents = 1025;

これにより、10.25ドルを1025セントとして正確に保持できます。

1円未満を扱う場合は単位を明示する

日本円は通常、現金決済で1円未満の補助単位を使用しません。

システム内部で1円未満を扱う場合は、管理単位を明示します。

#include <cstdint>

// 100分の1円単位で保持する
std::int64_t amountInHundredthsOfYen = 1025;

この場合、102510.25円に相当します。

整数管理でも丸め規則は必要

金額を整数で管理しても、税率や割引率を掛ける場面では丸めが必要です。

std::int64_t tax =
    amount
    * taxRateNumerator
    / taxRateDenominator;

このとき、どの丸め方法を使用するかを仕様として決める必要があります。

主な丸め方法は次のとおりです。

  • 切り捨て
  • 切り上げ
  • 四捨五入
  • 最近接偶数丸め

整数で管理する利点は、丸めが不要になることではありません。

丸め規則を明確に実装しやすくなり、浮動小数点誤差を避けやすくなることが利点です。

整数除算で四捨五入する方法

正の整数向けの基本式

正の整数同士を割り、最も近い整数へ丸める場合は、次の式が使われることがあります。

result =
    (numerator + denominator / 2)
    / denominator;

使用例は次のとおりです。

#include <iostream>

int main()
{
    int numerator = 8;
    int denominator = 3;

    int result =
        (numerator + denominator / 2)
        / denominator;

    std::cout << result << '\n';  // 3
}

この式は、主に次の条件を前提とします。

numerator >= 0
denominator > 0

負の数にはそのまま使えない

分子や分母に負の数が含まれる場合は、この式だけでは正しく処理できません。

符号の組み合わせと、中間値をどちらへ丸めるかを明示的に実装する必要があります。

加算時のオーバーフローに注意する

次の部分では、整数型の最大値を超える可能性があります。

numerator + denominator / 2

numeratorがすでに最大値に近い場合は、加算によって符号付き整数オーバーフローが発生する可能性があります。

汎用的な処理として使用する場合は、オーバーフロー対策が必要です。

符号付き整数の四捨五入で注意する境界値

INT64_MINの絶対値は表現できない

64ビット符号付き整数の範囲は、一般的に次のように非対称です。

最小値: -9223372036854775808
最大値:  9223372036854775807

そのため、最小値の絶対値は、同じstd::int64_t型では表現できません。

次のような処理は危険です。

std::int64_t absoluteValue =
    value < 0 ? -value : value;

valueINT64_MINの場合、-valueは表現可能範囲を超えます。

余りを2倍する処理にも注意する

次のような比較も、値が大きい場合はオーバーフローする可能性があります。

absoluteRemainder * 2

安全性を高めるには、2倍せずに分母の半分と比較する方法などを使用します。

INT64_MINを-1で割る場合

次の除算も結果をstd::int64_tで表現できません。

INT64_MIN / -1

数学的な結果がINT64_MAXより1大きくなるためです。

除算を行う前に、この特殊ケースを確認する必要があります。

安全性を考慮した整数除算の四捨五入

中間値をゼロから遠ざける実装

次の関数は、整数同士の割り算を最も近い整数へ丸め、中間値をゼロから遠い方向へ処理します。

#include <cstdint>
#include <limits>
#include <stdexcept>

std::uint64_t unsignedMagnitude(
    std::int64_t value)
{
    if (value >= 0) {
        return static_cast<std::uint64_t>(
            value);
    }

    return static_cast<std::uint64_t>(
        -(value + 1)) + 1;
}

std::int64_t divideRoundAwayFromZero(
    std::int64_t numerator,
    std::int64_t denominator)
{
    if (denominator == 0) {
        throw std::invalid_argument(
            "denominator must not be zero");
    }

    if (
        numerator
            == std::numeric_limits<
                std::int64_t>::min()
        && denominator == -1
    ) {
        throw std::overflow_error(
            "division result is out of range");
    }

    const bool negativeResult =
        (numerator < 0)
        != (denominator < 0);

    const std::uint64_t
        unsignedNumerator =
            unsignedMagnitude(numerator);

    const std::uint64_t
        unsignedDenominator =
            unsignedMagnitude(denominator);

    std::uint64_t quotient =
        unsignedNumerator
        / unsignedDenominator;

    const std::uint64_t remainder =
        unsignedNumerator
        % unsignedDenominator;

    const std::uint64_t half =
        unsignedDenominator / 2;

    const bool denominatorIsOdd =
        unsignedDenominator % 2 != 0;

    const bool roundUp =
        remainder > half
        || (
            !denominatorIsOdd
            && remainder == half
        );

    if (roundUp) {
        ++quotient;
    }

    if (!negativeResult) {
        const std::uint64_t maxValue =
            static_cast<std::uint64_t>(
                std::numeric_limits<
                    std::int64_t>::max());

        if (quotient > maxValue) {
            throw std::overflow_error(
                "rounded result is out of range");
        }

        return static_cast<std::int64_t>(
            quotient);
    }

    const std::uint64_t negativeLimit =
        static_cast<std::uint64_t>(
            std::numeric_limits<
                std::int64_t>::max()) + 1;

    if (quotient > negativeLimit) {
        throw std::overflow_error(
            "rounded result is out of range");
    }

    if (quotient == negativeLimit) {
        return std::numeric_limits<
            std::int64_t>::min();
    }

    return -static_cast<std::int64_t>(
        quotient);
}

使用例

#include <iostream>

int main()
{
    std::cout
        << divideRoundAwayFromZero(7, 2)
        << '\n';  // 4

    std::cout
        << divideRoundAwayFromZero(-7, 2)
        << '\n';  // -4

    std::cout
        << divideRoundAwayFromZero(7, -2)
        << '\n';  // -4

    std::cout
        << divideRoundAwayFromZero(-7, -2)
        << '\n';  // 4

    std::cout
        << divideRoundAwayFromZero(8, 3)
        << '\n';  // 3
}

この実装では、符号付き整数の境界値や、中間計算におけるオーバーフローをできるだけ避けています。

nearbyintとrintによる丸め

現在の丸めモードに従う

C++には、現在設定されている浮動小数点の丸め方向に従う関数もあります。

std::nearbyint(value);
std::rint(value);

これらはstd::roundとは異なり、実行環境の丸めモードによって結果が変わります。

最近接偶数丸めになる場合

実行環境の丸めモードが最近接偶数丸めの場合は、次のような結果になります。

2.5 → 2
3.5 → 4
4.5 → 4
5.5 → 6

中間値を最も近い偶数へ丸めるため、繰り返し計算における偏りを抑えやすい方法です。

常に偶数丸めになるわけではない

std::nearbyintstd::rintが、常に最近接偶数丸めになるわけではありません。

現在設定されている浮動小数点の丸め方向に従います。

一般的な四捨五入を行いたい場合は、std::roundを使用するほうが分かりやすいでしょう。

テンプレートで四捨五入関数を実装する方法

固定の桁数制限を設けない

floatdoublelong doubleでは、表現可能な値の範囲が異なります。

そのため、すべての型に対して同じ桁数制限を設定する方法は適切ではありません。

例えば、digits-308から308に制限しても、floatでは10^308を表現できません。

一方、long doubleは、処理系によってdoubleより広い範囲を扱える場合があります。

実際の計算結果を検査する

固定値で制限するのではなく、std::powや乗算結果を検査する方法が適切です。

#include <cmath>
#include <concepts>
#include <stdexcept>

template<std::floating_point T>
T roundToDigits(T value, int digits)
{
    if (!std::isfinite(value)) {
        throw std::invalid_argument(
            "value must be finite");
    }

    const T factor =
        std::pow(
            T{10},
            static_cast<T>(digits));

    if (
        !std::isfinite(factor)
        || factor == T{0}
    ) {
        throw std::out_of_range(
            "digits is out of supported range");
    }

    const T scaled =
        value * factor;

    if (!std::isfinite(scaled)) {
        throw std::overflow_error(
            "scaled value overflowed");
    }

    if (
        value != T{0}
        && scaled == T{0}
    ) {
        throw std::underflow_error(
            "scaled value underflowed");
    }

    return
        std::round(scaled) / factor;
}

この実装でも浮動小数点誤差は残りますが、型ごとの表現範囲に応じて異常を検出できます。

実用的な四捨五入関数

一般用途向けの実装例

一般的な数値を小数第N位で四捨五入する場合は、次のように実装できます。

#include <cmath>
#include <iomanip>
#include <iostream>
#include <stdexcept>

double roundToDigits(
    double value,
    int digits)
{
    if (!std::isfinite(value)) {
        throw std::invalid_argument(
            "value must be finite");
    }

    const double factor =
        std::pow(10.0, digits);

    if (
        !std::isfinite(factor)
        || factor == 0.0
    ) {
        throw std::out_of_range(
            "digits is out of supported range");
    }

    const double scaled =
        value * factor;

    if (!std::isfinite(scaled)) {
        throw std::overflow_error(
            "scaled value overflowed");
    }

    if (
        value != 0.0
        && scaled == 0.0
    ) {
        throw std::underflow_error(
            "scaled value underflowed");
    }

    const double rounded =
        std::round(scaled) / factor;

    if (!std::isfinite(rounded)) {
        throw std::overflow_error(
            "rounded result is not finite");
    }

    return rounded;
}

int main()
{
    try {
        const double value = 123.4567;

        const double result =
            roundToDigits(value, 2);

        std::cout
            << std::fixed
            << std::setprecision(2)
            << result
            << '\n';
    }
    catch (const std::exception& error) {
        std::cerr
            << "Error: "
            << error.what()
            << '\n';

        return 1;
    }

    return 0;
}

出力結果は次のようになります。

123.46

実装時の注意点

この関数を使用する場合は、次の点に注意が必要です。

  • doubleでは10進小数を完全に正確に表現できない
  • 丸めた結果も浮動小数点数として保持される
  • 非常に大きな桁数ではオーバーフローする可能性がある
  • 非常に小さな桁数ではアンダーフローする可能性がある
  • 末尾のゼロを表示するにはstd::fixedstd::setprecisionが必要
  • 金額や税計算では整数単位や10進数専用の型を検討する必要がある

C++で四捨五入を実装するときの選び方

最も近い整数へ丸める

double result = std::round(value);

整数型として取得する

long long result =
    std::llround(value);

小数第2位まで残す

double result =
    std::round(value * 100.0) / 100.0;

十の位で四捨五入する

double result =
    std::round(value / 10.0) * 10.0;

表示だけ小数点以下2桁にする

std::cout
    << std::fixed
    << std::setprecision(2)
    << value;

金額を正確に管理する

std::int64_t amountInSmallestUnit;

金額は、円、セント、100分の1円など、システムで決めた最小単位の整数として管理します。

まとめ

C++で一般的な四捨五入を実装する場合は、std::roundを使用する方法が基本です。

整数型の結果が必要な場合は、std::lroundstd::llroundも利用できます。

小数第N位で四捨五入する場合は、10の累乗を掛けてからstd::roundを使用し、同じ倍率で割る方法が一般的です。

ただし、doublefloatは2進浮動小数点数であるため、10進小数を常に正確に表現できるわけではありません。

一般的な表示や計算ではstd::roundで十分な場合が多いものの、金額、税金、請求、会計など、厳密な10進数処理が必要な場面では、整数単位での管理や10進数専用ライブラリの利用が適しています。

以上、C++で四捨五入を実装する方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次