Альтернативы для инициализации с плавающей запятой

В настоящее время я работаю над реализацией арифметики с плавающей запятой, основанной на шаблонах. Шаблон, который представляет значения с плавающей запятой, выглядит следующим образом:

template<bool S , std::int16_t E , std::uint64_t M>
struct number{};

Поскольку инициализация таких значений с использованием жестко запрограммированных мантисса, экспонентов и т.д. является громоздким и подверженным ошибкам процессом, я написал шаблон для преобразования десятичных значений в плавающие:

template<std::int64_t INT , std::uint64_t DECS>
struct decimal{};

Если первый параметр представляет собой неотъемлемую часть, а второй - дробные цифры. Я думаю, что это общий и хорошо известный способ.
Однако эта модель страдает от некоторых проблем (как я ввожу отрицательные числа меньше одного?), Где один из самых раздражающих для меня - это тот факт, что невозможно вводить нулевые цифры сразу после запятой, т.е. Числа, подобные 0.00032.

Я знаю С++ 11, и я думал об пользовательском-литеральном + decltype() (даже с макросом #define FLOAT(x) decltype(x_MY_LITERAL)), но я не уверен, что этот подход возможен во всех контекстах, Я имею в виду, если литерал + decltype оценивается в контексте параметра шаблона.

Даже если это может сработать, я хочу знать, есть ли другие возможные подходы к этой проблеме. Итак, какие альтернативы существуют для инициализации с плавающей точкой в ​​момент компиляции через tmp?


Применяемые альтернативы:

Просто для полноты встряски, я опишу альтернативы, которые я реализовал, как они работают, и его константы и профи. Сам вопрос остается открытым, чтобы позволить кому-либо добавить больше альтернатив.

Некоторый фон

Сначала я опишу функции, которые я использовал, чтобы убедиться, что все понимают код.

Моя библиотека, Библиотека Turbo Metaprogramming, основана на трех принципах:

  • Только параметры шаблона шаблона. Полностью общие параметры типа микширования, параметры параметров и параметры шаблона шаблона действительно сложны (почти невозможно), поэтому эта библиотека использует только параметры типа. Всякий раз, когда это необходимо для использования значений или шаблонов, библиотека предоставляет обертки для передачи таких параметров через бокс.

  • Равномерная оценка выражения. Одна из первых потребностей при работе на языке программирования - это способ оценки выражений и принятия его значения. Turbo предоставляет metafunction tml::eval, который принимает любое выражение и возвращает (оценивает) его значение.

  • Общие алгоритмы и метафайлы, настроенные с помощью специализированной специализации: всякий раз, когда я могу использовать псевдонимы шаблонов С++ 11, чтобы избежать громоздкой конструкции typename ::type. Мое соглашение состоит в том, чтобы определить шаблоны реализации (метафайлы, которые действительно выполняют работу) в вложенном пространстве имен impl, и псевдоним шаблона С++ 11 для результата в текущем пространстве имен. Поскольку такие псевдонимы напрямую возвращают результат, они не могут быть оценены на сложном выражении (Рассмотрим метабактерную метку add<X,Y>, где X и Y являются переменными лямбда. Если add был псевдонимом результата, это не работает, потому что оценка не имеет смысла. Если нам нужно выражение (metafunction) вместо его результата напрямую, моя конвенция заключалась в том, чтобы поместить псевдоним в метафунг в пространстве имен func.

Вот несколько примеров:

using bits = tml::util::sizeof_bits<int>; //bits is a size_t integral constant with the 
                                          //size on bits of an int

//A metafunction which returns the size on bits of a type doubled
using double_size = tml::lambda<_1 , tml::mul<tml::util::func::sizeof_bits<_1>,tml::Int<2>> >;

using int_double_size = tml::eval<double_size,int>; //Read as "double_size(int)"

tml является основным пространством имен библиотеки, а функции с плавающей запятой отображаются в пространстве имен tml::floating.

TL; DR

  • tml::eval принимает любое выражение и оценивает его, возвращая его значение. Его псевдоним шаблона С++ 11, поэтому typename ::type не требуется.

  • tml::integral_constant (Просто псевдоним std::integral_constant) - это де-факто-значение обертки для передачи параметров значения в качестве параметров типа через бокс. В библиотеке есть соглашение только с использованием параметров типа (также есть обертки для параметров шаблона шаблона, см. tml::lazy и tml::bind).

Попытка 1: От целого

Здесь мы определяем metafunction integer, который возвращает значение с плавающей запятой из целого числа:

template<std::int64_t mantissa , sign_t S = (sign_t)(mantissa >= 0)>
struct integer
{
    using m   = tml::floating::number<S,0,static_cast<mantissa_t>((mantissa >= 0) ? mantissa : -mantissa)>;
    using hsb = tml::floating::highest_set_bit<m>;
    static constexpr const exponent_t exp = hsb::value - 31;

    using result = tml::floating::number<S,exp,(m::mantissa << (31 - hsb::value))>; //Note the number is normalized
};

То, что он делает, - это прямое интегральное значение, использовать его как мантисса и нормализовать число, явно вычисляющее самый старший (самый значительный) бит набора, соответствующим образом изменяя мантиссу.

Примером его использования может быть:

using ten = tml::floating::integer<10>;

Преимущества:

  • Эффективность: Для получения эквивалентного числа с плавающей запятой не требуются дополнительные сложные вычисления. Единственной релевантной операцией является вызов highest_set_bit.

  • По норме нормируется номер (относительно эффективности тоже). Также нет проблем с точностью (по крайней мере, не для небольших значений).

Недостатки:

  • Работает только со встроенными значениями.

Попытка 2: Десятичная инициализация

Эта альтернатива использует пару интегральных значений для представления интегральной и дробной частей числа соответственно:

template<std::int64_t INTEGRAL , std::uint64_t FRACTIONAL>
struct decimal{ ... };

using pi = decimal<3,141592654>;

То, что он делает, - вычислить значение интегральной части (просто вызвать integer, предыдущую попытку) и значение дробной части.
Значение дробной части - это значение целого числа, скорректированного до тех пор, пока точка основания не будет в начале номера. Другими словами:

                       integer<fractional_part>
fractional_value = ________________________________
                          10^number_of_digits

Тогда значение числа - это просто сумма обоих значений:

result = integer_part_value + fractional_value

Число цифр целочисленного числа log10(number) + 1. Я закончил с метафайлом log10 для целочисленных значений, которые не требуют рекурсии:

template<typename N>
struct log10
{
    using result = tml::Int<(0  <= N::value && N::value < 10)  ? 0 :
                            (10 <= N::value && N::value < 100) ? 1 :
                            ...
                           >;
} 

Таким образом, у него есть сложность O (1) (конечно, глубина ввода шаблона измерения).

При этом метафоре формула выше:

//First some aliases, to make the code more handy:
using integral_i   = tml::integral_constant<std::int64_t,INTEGRAL>;
using integral_f   = tml::floating::integer<INTEGRAL>;
using fractional_f = tml::floating::integer<FRACTIONAL>;
using ten          = tml::floating::integer<10>;
using one          = tml::Int<1>;

using fractional_value = tml::eval<tml::div<fractional_f , 
                                            tml::pow<ten,
                                                     tml::add<tml::log10<integral_i>,
                                                              one
                                                             >
                                                    >
                                           >
                                  > 

И тогда результат:

 using result = tml::eval<tml::add<integral_f,fractional_value>>;

Преимущества

  • Позволяет создавать нецелые значения, такие как 12.123.

Недостатки:

  • Производительность: tml::pow является рекурсивной со сложностью O (n). tml::div для значений с плавающей запятой реализуется как умножение числителя на обратное знаменателю. Этот взаимный результат вычисляется с помощью аппроксимации Ньютона-Рафсона (по умолчанию пять итераций).

  • Прецизионные проблемы. Последовательные умножения, выполняемые для вычисления мощности, могут привести к накопительным проблемам с низкой точностью. То же самое для аппроксимации Ньютона-Рафсона, выполненного для вычисления деления.

  • Обозначение ограничено. Невозможно указать числа с конечными нулями после точки, скажем 13.0004, поскольку целочисленный литерал 0004 недействителен.

Попытка 3 (3.1 и 3.2): десятичная научная нотация

Вместо того, чтобы записывать номер с использованием цифр с жестким кодом, мы используем десятичную (Power of 10) научную нотацию для инициализации чисел с плавающей запятой:

using pi = tml::floating::decimal_sci<3141592654,-9>; //3141592654 x 10^-9

Чтобы вычислить номер, вам нужно только принять значение значимого и умножить его на соответствующую мощность 10:

template<std::int64_t S , std::int64_t E>
struct decimal_sci
{
    using significant = tml::floating::integer<S>;
    using power       = tml::eval<tml::pow<tml::floating::integer<10>,tml::Int<E>>>;

    using result = tml::eval<tml::mul<significant,power>>;
};

Существует улучшение для этой попытки, которая обрабатывает данное значение, если оно было нормализовано только на одну целую цифру. Поэтому значение 0.0034565432 может быть записано как (34565432 , -3) вместо (34565432 , -11).
Я называю это tml::floating::decimal_scinorm:

template<std::int64_t S , std::int64_t E = 0>
struct decimal_scinorm
{
    using significant_i = tml::integral_constant<std::int64_t,S>;
    using exponent_i    = tml::integral_constant<std::int64_t,E>;

    using adjust  = tml::eval<tml::log10<significant_i>>;
    using new_exp = tml::eval<tml::sub<exponent_i,adjust>>;

    using result = typename decimal_sci<S,new_exp::value>::result;
};

using pi = tml::floating::decimal_scinorm<3141592654>; //3.141592654
using i  = tml::floating::decimal_scinorm<999999,-4>;  //0.000999999

Преимущества

  • Проводятся с широкими номерами, включая нулевые заголовки, простым способом.
  • Использует известную нотацию, никаких синтаксических трюков.

Недостатки

  • Плохая точность с очень большими/небольшими числами (ну, что и ожидалось, так как работает научная нотация). Обратите внимание, что внутренние вычисления с плавающей запятой могут приводить к ошибкам накопления точности, пропорциональным длине (мантиссы) и показателю числа. Являются теми же ошибками точности вышеприведенных попыток (из-за использования tml::pow, tml::div и т.д.).

Ответ 1

Возможно, вы захотите использовать пользовательские литералы. Согласно cppreference.com, он

Позволяет целые, плавающие, символьные и строковые литералы создавать объекты пользовательского типа, определяя определяемые пользователем суффикс.

(см. также http://en.cppreference.com/w/cpp/language/user_literal). Таким образом, вы можете сделать выражение

123.456_mysuffix

введите любой тип, который вы хотите, если вы определяете литеральный оператор для _mysuffix. С помощью этого оператора вы можете получить доступ к входу 123.456 либо в виде числа с плавающей запятой (стандартного С++), либо вы можете сделать необходимое преобразование из исходной строки как const char * самостоятельно.

EDIT: прочитав отредактированный вопрос и осознав, о каком метафоном шаблонов вы говорили, я просто хотел подчеркнуть, что литерал также может быть доступен как пакет параметров параметров шаблона char. Возможно, вы сможете интегрировать это в свою инфраструктуру времени компиляции.