Стандартная функция sign (signum, sgn) в C++: существует ли она и как её реализовать?
Краткий ответ: в стандартной библиотеке C++ нет функции sign, но есть элегантные решения
Вы правы — в C++98, C++11, C++17 и даже C++20 нет встроенной функции sign() или sgn(). Однако начиная с C++11 появились инструменты, которые позволяют реализовать её правильно и эффективно. Рассмотрим несколько подходов.
1. Классический подход с std::signbit (C++11+)
Стандартная функция std::signbit из определяет знак числа, включая корректную обработку отрицательного нуля (-0.0) и NaN. Комбинируя её с проверкой на ноль, получаем идеальную реализацию:
#include <cmath> // для std::signbit, std::isnan
#include <type_traits>
template <typename T>
int sgn(T val) {
static_assert(std::is_floating_point<T>::value ||
std::is_integral<T>::value,
"sgn() requires numeric type");
if (std::isnan(val)) return 0; // или можно вернуть val, зависит от задачи
if (val == T(0)) return 0;
return std::signbit(val) ? -1 : 1;
}
2. Современный C++20: std::strong_order и std::compare_three_way
В C++20 появился трёхсторонний оператор сравнения (spaceship operator) и функции сравнения с гарантией строгого порядка. Можно использовать std::strong_order из <compare>:
#include <compare>
#include <cmath>
template <std::floating_point T>
int sgn(T val) {
if (std::isnan(val)) return 0;
auto cmp = std::strong_order(val, T(0));
if (cmp == std::strong_ordering::less) return -1;
if (cmp == std::strong_ordering::greater) return 1;
return 0;
}
3. Для целых чисел — оптимизированная версия
Для целых типов можно использовать трюк с приведением к знаковому типу:
template <typename T>
int sgn_int(T val) {
static_assert(std::is_integral<T>::value && std::is_signed<T>::value,
"sgn_int() requires signed integer type");
return (T(0) < val) - (val < T(0));
}
Почему это лучший подход?
- Корректность: правильно обрабатывает -0.0, NaN, бесконечности.
- Производительность: компилятор оптимизирует код до одной-двух инструкций.
- Безопасность типов: шаблоны с static_assert не дадут передать строку или указатель.
Замечание о NaN
В математике sign(NaN) не определён. В примерах выше я возвращаю 0, но вы можете изменить поведение: вернуть NaN, выбросить исключение или использовать std::optional<int>. Выбор зависит от контекста.
Итог: используйте шаблонную функцию с std::signbit для чисел с плавающей точкой и трюк с вычитанием для целых. Это стандартный идиоматичный подход в C++.
Альтернатива: использование boost::math::sign()
Если вы не против внешних зависимостей, библиотека Boost предоставляет готовую функцию boost::math::sign() в заголовочном файле <boost/math/special_functions/sign.hpp>. Она уже решает все проблемы с NaN, signed zero и типами.
#include <boost/math/special_functions/sign.hpp>
#include <iostream>
int main() {
double x = -5.7;
int s = boost::math::sign(x); // -1
std::cout << s << std::endl;
double nan_val = std::numeric_limits<double>::quiet_NaN();
std::cout << boost::math::sign(nan_val); // 0 (по умолчанию)
return 0;
}
Плюсы: проверенное решение, поддержка всех пограничных случаев, документация.
Минусы: нужно подключать Boost, что увеличивает размер проекта и время сборки. Для небольших проектов проще написать свою реализацию из основного ответа.
Также обратите внимание: в Boost есть boost::math::copysign() для копирования знака одного числа на другое — может быть полезно в численных алгоритмах.