В C++ есть несколько способов округлить дробное число (float, double) до целого. Сегодня я кратко опишу эти способы и покажу, чем они отличаются друг от друга.
Обзор
Ниже приводится список рассматриваемых способов с их кратким описанием.
- Явное приведение (
static_cast). Просто отбрасывает дробную часть. std::ceil. Ищет наименьшее целое число, которое больше исходного значения [1].std::floor. Ищет наибольшее целое число, которое меньше исходного значения [2].std::round. Округляет по тем правилам, которым нас учили в школе. При этом 0.5 становится единицей, а -0.5 — минус единицей [3].std::trunc. Ищет ближайшее целое с наименьшим значением по модулю [4].
Функции std::round и std::trunc появились в C++11. Функции std::ceil и std::floor унаследованы от языка C.
Существуют еще функции std::nearbyint (унаследована от C99) [5] и std::rint (появилась в C++11) [6]. Результат их работы зависит от текущего режима округления (current rounding mode). Их мы рассмотрим позже.
Все функции возвращают число (float, double или long double в зависимости от конкретной перегрузки) с дробной частью равной нулю.
Для всех функций есть их аналоги с f или l на конце (например, std::ceilf и std::ceill, или std::floorf и std::floorl, и т.д.). Функции, чьи имена оканчиваются на f (std::ceilf, std::floorf, std::roundf и т.д.), принимают и возвращают значение типа float. Функции, имена которых оканчиваются на l (std::ceill, std::floorl, std::roundl и т.д.), принимают и возвращают значение типа long double.
Начиная с C++23 все функции кроме std::nearbyint и std::rint имеют спецификатор constexpr.
Если функции в качестве входного аргумента получают ±∞, ±0 или ±NaN, полученное значение возвращается в качестве результата как есть (unmodified в оригинале)
Ни у одной из функций нет спецификатора noexcept. При этом исключения, которые может бросать та или иная функция не приводятся. Вообще обработка ошибок для этих функций организована так же, как и для других математических функций. Она описывается на странице [7]. Я не буду рассматривать её подробно, но нам придется с ней столкнуться ниже при обсуждении различий между функциями std::nearbyint и std::rint.
Пример
Чтобы наглядно продемонстрировать работу этих способов округления я набросал небольшую программу. Ниже приводится её полный исходный текст.
#include <array>
#include <cmath>
#include <iostream>
#include <string_view>
constexpr std::array<float, 6> numbers
{-0.8, -0.5, -0.3, 0.3, 0.5, 0.8};
template <typename F>
void print(std::string_view prefix, F transformer)
{
std::cout << prefix;
for(auto length = prefix.size(); length < 8; ++length)
std::cout << ' ';
for(auto number : numbers)
std::cout << transformer(number) << '\t';
std::cout << '\n';
}
int main()
{
print("" , [](auto f) { return f; });
print("ceil" , [](auto f) { return std::ceil(f); });
print("floor" , [](auto f) { return std::floor(f); });
print("int()" , [](auto f) { return static_cast<int>(f); });
print("round" , [](auto f) { return std::round(f); });
print("trunc" , [](auto f) { return std::trunc(f); });
return 0;
}
Основную работу выполняет шаблонная функция print (согласен, название не очень удачное). Она принимает строковый префикс, по которому мы понимаем какой способ округления использовался, и функциональный объект, который собственно и выполняет округление. Для простоты я не стал заморачиваться с лишними символами табуляции на концах строк, но по-хорошему их следовало бы убрать.
Ниже приводится результат работы этой программы.
-0.8 -0.5 -0.3 0.3 0.5 0.8 ceil -0 -0 -0 1 1 1 floor -1 -1 -1 0 0 0 int() 0 0 0 0 0 0 round -1 -1 -0 0 1 1 trunc -0 -0 -0 0 0 0
Приведение к целочисленному типу
Часто при выполнении округления дробного числа мы хотим получить значение целочисленного типа. Но если мы напишем так:
float f = 1.25; auto i = std::round(f);
Переменная i будет иметь тип float. Можно написать так:
float f = 1.25; int i = std::round(f);
Этот приём работает. Более того многие компиляторы (проверял gcc и clang) к моему удивлению даже не выдают никакого предупреждения. Но есть и другой способ.
Функции std::round и std::rint имеют специальные версии, начинающиеся с префиксов l (std::lround, std::lrint и т.д.) и ll (std::llround, std::llrint и т.д.). Функции, имена которых начинаются с префикса l, возвращают значение типа long. А функции, чьи имена начинаются с ll, возвращают значение типа long long. Таким образом для примера ниже
float f = 1.25; auto l = std::lround(f); auto ll = std::llround(f);
Переменная l будет иметь тип long, а переменная ll — long long.
Важно отметить, что в случае ошибки (например, аргумент функции является «не числом», или результат округления не умещается в требуемый целочисленный тип) функции с префиксами l и ll возвращают значение, зависящее от реализации (implementation-defined value). Оператор static_cast в этом случае приводит к неопределенному поведению. На странице [8] приводится обсуждение этой проблемы.
Префиксно-суффиксное разнообразие
Возможно Вы уже запутались со всеми этими префиксами и суффиксами. Чтобы помочь Вам приведу основные прототипы функций с их префиксами и суффиксами.
Все рассматриваемые выше функции можно разделить на две группы. В первую группу входят функции: std::ceil, std::floor, std::trunc и std::nearbyint. Они не имеют префиксов l и ll. Ниже приводятся прототипы этих функций на примере std::ceil.
float ceil(float); double ceil(double); long double ceil(long double); float ceilf(float); long double ceill(long double);
Во вторую группу входят функции std::round и std::rint. Ниже приводятся их прототипы на примере std::round.
float round(float); double round(double); long double round(long double); float roundf(float); long double roundl(long double); long lround(float); long lround(double); long lround(long double); long lroundf(float); long lroundl(long double); long long llround(float); long long llround(double); long long llround(long double); long long llroundf(float); long long llroundl(long double);
Режимы округления
Режимы округления описаны на странице [9]. Они были добавлены в C++11 и управляют поведением функций std::nearbyint и std::rint. Ниже даётся краткое описание этих режимов на примере функции std::rint [6]:
- FE_DOWNWARD —
std::rintведет себя так же, как иstd::floor. - FE_UPWARD —
std::rintведет себя так же, как иstd::ceil. - FE_TOWARDZERO —
std::rintведет себя так же, как иstd::trunc. - FE_TONEAREST — единственное отличие
std::rintотstd::roundв том, что случаи с 0.5 округляются к ближайшему четному, а не «от нуля» (так называемое «банковское округление» или «округление банкира»).
Интересно, что это не константы как можно было бы ожидать, а… макросы. Но для них дается уточнение [9]:
Each of these macro constants expands to a nonnegative integer constant expression, which can be used with std::fesetround and std::fegetround to indicate one of the supported floating-point rounding modes
Или в моём переводе:
Каждый из этих макросов раскрывается в неотрицательное целочисленное константное выражение, которое может быть использовано с функциями std::fesetround и std::fegetround для указания одного из поддерживаемых режимов округления чисел с плавающей точкой.
Согласно стандарту могут существовать и дополнительные режимы округления [9].
Функции std::fesetround и std::fegetround были добавлены в C++11 и описаны на странице [10]. Первая устанавливает режим округления, а вторая — возвращает текущий режим округления. Обе они воспринимают режим округления как значение типа int.
Изменение режима округления функцией std::fesetround влияет только на текущий поток [22]. Установленный режим округления наследуется от текущего потока тому потоку, который он только что создал.
Флаг -frounding-math
Если Вы используете функцию std::fesetround в компиляторе gcc, то при вызове std::rint Вы можете получить неправильный результат [11]. Дело в том, что по умолчанию компилятор считает, что программа не меняет режим округления и применяет некоторые оптимизации. Для того чтобы отключить их, при компиляции нужно указать флаг -frounding-math. Вот что говорится в его описании [12]:
This option should be specified for programs that change the FP rounding mode dynamically, or that may be executed with a non-default rounding mode.
Или в моём переводе:
Этот флаг должен быть указан для программ, которые динамически меняют режим округления, или которые должны исполняться с режимом округления отличным от того, что используется по умолчанию.
#pragma STDC FENV_ACCESS ON
На странице [9] говорится:
As with any floating-point environment functionality, rounding is only guaranteed if #pragma STDC FENV_ACCESS ON is set.
Или в моём переводе:
Как и в случае с функциональностью окружения чисел с плавающей запятой, округление гарантируется только если задано #pragma STDC FENV_ACCESS ON
И здесь возникает противоречие, потому что согласно стандарту компилятор не обязан поддерживать прагмы [13]. Так, используемый мной компилятор gcc (13.3.0) выдает предупреждение:
main.cpp:7: warning: ignoring ‘#pragma STDC FENV_ACCESS’ [-Wunknown-pragmas]
7 | #pragma STDC FENV_ACCESS ON
Компилятор clang (18.1.3) в моих тестах с функциями округлениями правильно работает вне зависимости от того задана прагма или нет. Но и при её наличии не выдает никаких предупреждений.
Компилятор от компании Microsoft так же правильно работает и без прагмы, а при её наличии выдает предупреждение:
warning C4068: неизвестная прагма "STDC"
Должен сделать важное замечание. Под «правильно работает» я понимаю правильно работает на моих тестах и на моём аппаратном обеспечении. Я не гарантирую, что эта правильность сохранится на других входных данных и на другом аппаратном обеспечении.
Пример
Чтобы наглядно продемонстрировать работу с режимами округления я набросал небольшой пример. Ниже приводится полный исходный текст демонстрационной программы.
#include <array>
#include <cfenv>
#include <cmath>
#include <iostream>
#include <string_view>
// ????
//#pragma STDC FENV_ACCESS ON
constexpr std::array<float, 6> numbers
{-0.8, -0.5, -0.3, 0.3, 0.5, 0.8};
std::string_view round_mode_to_string(int mode)
{
switch(mode)
{
case FE_DOWNWARD : return "FE_DOWNWARD";
case FE_TONEAREST : return "FE_TONEAREST";
case FE_TOWARDZERO : return "FE_TOWARDZERO";
case FE_UPWARD : return "FE_UPWARD";
}
return "Unknown";
}
template <typename F>
void print(std::string_view prefix, F transformer)
{
std::cout << prefix;
for(auto length = prefix.size(); length < 16; ++length)
std::cout << ' ';
for(auto number : numbers)
std::cout << transformer(number) << '\t';
std::cout << '\n';
}
int main()
{
std::cout << "Current rounding mode is "
<< round_mode_to_string(std::fegetround()) << "\n\n";
print("", [](auto f) { return f; });
for(auto mode :
{FE_DOWNWARD, FE_TONEAREST, FE_TOWARDZERO, FE_UPWARD})
{
std::fesetround(mode);
print(round_mode_to_string(mode),
[](auto f) { return std::rint(f); });
}
return 0;
}
Ниже приводится вывод этой программы на моём домашнем компьютере:
Current rounding mode is FE_TONEAREST
-0.8 -0.5 -0.3 0.3 0.5 0.8
FE_DOWNWARD -1 -1 -1 0 0 0
FE_TONEAREST -1 -0 -0 0 0 1
FE_TOWARDZERO -0 -0 -0 0 0 0
FE_UPWARD -0 -0 -0 1 1 1
Для большей наглядности дополнительно продублирую здесь вывод первого примера из нашего сегодняшнего поста.
-0.8 -0.5 -0.3 0.3 0.5 0.8 ceil -0 -0 -0 1 1 1 floor -1 -1 -1 0 0 0 int() 0 0 0 0 0 0 round -1 -1 -0 0 1 1 trunc -0 -0 -0 0 0 0
Отличие std::rint от std::nearbyint
Функции std::rint и std::nearbyint делают по сути одно и тоже: округляют число в соответствии с текущим режимом округления. Поэтому неудивительно, что возникает вопрос о том, чем они отличаются друг от друга [14]. На страницах [5,6] говорится:
The only difference between std::rint and std::nearbyint is that std::nearbyint never raises FE_INEXACT.
Или в моём переводе:
Единственное различие между функциями std::rint и std::nearbyint в том, что std::nearbyint никогда не взводит FE_INEXACT.
FE_INEXACT — это флаг, обозначающий некоторую ситуацию при выполнении математической функции. Он описан на странице [15] следующим образом:
rounding was necessary to store the result of an earlier floating-point operation
Или в моём переводе:
для сохранения результата предыдущей операции с числом с плавающей точкой потребовалось округление
Для проверки состояния флага может использоваться функция std::fetestexcept [16], а для его сброса std::feclearexcept [17].
Пример
Для демонстрации различия между функциями std::rint и std::nearbyint я набросал небольшой пример. В нём также демонстрируется использование функций std::fetestexcept и std::feclearexcept. Ниже приводится полный исходный текст примера.
#include <cfenv>
#include <cmath>
#include <iostream>
#include <string_view>
// ????
//#pragma STDC FENV_ACCESS ON
template <typename F>
void print(std::string_view prefix, F transformer)
{
std::cout << prefix;
for(auto length = prefix.size(); length < 12; ++length)
std::cout << ' ';
for(auto n : {2.0, 2.3})
std::cout << transformer(n) << '\t';
std::cout << '\n';
}
template<typename R>
auto make_transformer(R rounder)
{
return [=](auto n) -> std::string_view
{
std::feclearexcept(FE_INEXACT);
rounder(n);
if(std::fetestexcept(FE_INEXACT))
return "R";
else
return "NR";
};
}
int main()
{
print("", [](auto f) { return f; });
print("rint",
make_transformer(
[](auto f) { return std::rint(f); } ));
print("nearbyint",
make_transformer(
[](auto f) { return std::nearbyint(f); }));
return 0;
}
В этом примере для чисел 2.0 и 2.3 вызываются наши сравниваемые функции std::rint и std::nearbyint. После каждого такого вызова проверяется состояние флага FE_INEXACT. Если он взведён выводится "R", иначе — "NR".
Ниже приводится результат работы этой программы.
2 2.3 rint NR R nearbyint NR NR
Видно, что функция std::rint взвела этот флаг для числа 2.3, для которого произошло округление. Функция std::nearbyint в свою очередь не взвела его ни для какого числа.
Перечисление std::float_round_style
Шаблонная структура std::numeric_limits [18] содержит статическое константное поле round_style [19], содержащее стиль округления (rounding style), применяемый к типу, по которому инстанцирована структура. Начиная с C++11 оно имеет спецификатор constexpr.
Стиль округления представляет собой перечисление типа std::float_round_style [20]. Ниже приводится краткое описание возможных значений.
std::round_indeterminate— стиль не определен;std::round_toward_zero— по направлению к нулю;std::round_to_nearest— по направлению к ближайшему представимому (nearest representable) значению;std::round_toward_infinity— по направлению к положительной бесконечности;std::round_toward_neg_infinity— по направлению к отрицательной бесконечности.
В описании константного статического поля round_style [19] говорится:
These values are constants, and do not reflect the changes to the rounding made by std::fesetround.
Или в моём переводе:
Эти значения являются константами и не отражают изменений режима округления, вносимых функцией std::fesetround.
Таким образом, это поле представляет собой просто справочную информацию о режиме округления применяемому к тому или иному типу по умолчанию.
FLT_ROUNDS
Существует еще макрос FLT_ROUNDS [21], который содержит текущий режим округления, установленный функцией std::fesetround. Он раскрывается в одно из следующих значений:
- -1 — режим не задан;
- 0 — аналогично
FE_TOWARDZERO; - 1 — аналогично
FE_TONEAREST; - 2 — аналогично
FE_UPWARD; - 3 — аналогично
FE_DOWNWARD.
В зависимости от реализации могут возвращаться и другие значения для дополнительных режимов округления.
Заключение
Когда я только начинал работу над этим постом я думал, что это будет короткая статья с описанием способов округления и примером их использования. Но всё оказалось несколько сложнее.
Тем не менее я надеюсь, что мне удалось осветить основные моменты вопроса округления дробных чисел в С++. Если вдруг я что-то упустил — пишите в комментариях, или в форме обратной связи (https://norseev.ru/contacts/).
Ссылки
- Функция
std::ceil: https://en.cppreference.com/w/cpp/numeric/math/ceil.html - Функция
std::floor: https://en.cppreference.com/w/cpp/numeric/math/floor.html - Функция
std::round: https://en.cppreference.com/w/cpp/numeric/math/round.html - Функция
std::trunc: https://en.cppreference.com/w/cpp/numeric/math/trunc.html - Функция
std::nearbyint: https://en.cppreference.com/w/cpp/numeric/math/nearbyint.html - Функция
std::rint: https://en.cppreference.com/w/cpp/numeric/math/rint.html - Обработка ошибок математических функций: https://en.cppreference.com/w/cpp/numeric/math/math_errhandling.html
- Обсуждение того, что приведение
floatкintможет приводить к неопределенному поведению: https://www.reddit.com/r/cpp/comments/rsldwl/converting_float_to_int_can_be_undefined_behavior/ - Режимы округления: https://en.cppreference.com/w/cpp/numeric/fenv/FE_round.html
- Функции
std::fegetroundиstd::fesetround: https://en.cppreference.com/w/cpp/numeric/fenv/feround.html - Вопрос о неправильной работе функции
std::rint: https://stackoverflow.com/questions/78190410/why-does-gcc-o1-affects-stdrint - Некоторые флаги gcc включая
-frounding-mode: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html - Вопрос о согласованности требования
#pragma STDC FENV_ACCESSсо стандартом: https://stackoverflow.com/questions/79395182/pragma-stdc-fenv-access - Вопрос об отличиях функции
std::rintотstd::nearbyint: https://stackoverflow.com/questions/67515507/difference-between-rint-and-nearbyint - Математические исключения (на самом деле флаги): https://en.cppreference.com/w/cpp/numeric/fenv/FE_exceptions.html
- Функция
std::fetestexcept: https://en.cppreference.com/w/cpp/numeric/fenv/fetestexcept.html - Функция
std::feclearexcept: https://en.cppreference.com/w/cpp/numeric/fenv/feclearexcept.html - Шаблонная структура
std::numeric_limits: https://en.cppreference.com/w/cpp/types/numeric_limits.html - Статическое поле
std::numeric_limits<T>::round_style: https://en.cppreference.com/w/cpp/types/numeric_limits/round_style.html - Перечисление
std::float_round_style: https://en.cppreference.com/w/cpp/types/numeric_limits/float_round_style.html - Макрос
FLT_ROUNDS: https://en.cppreference.com/w/cpp/types/climits/FLT_ROUNDS.html - Вопрос об области действия функции fesetround (справедливо и для C++): https://stackoverflow.com/questions/79485550/what-is-the-scope-of-fesetround
