编译时浮点初始化的替代方法

Man*_*726 19 c++ floating-point templates template-meta-programming c++11

我目前正致力于基于模板元编程的浮点运算实现.表示编译时浮点值的模板如下:

template<bool S , std::int16_t E , std::uint64_t M>
struct number{};
Run Code Online (Sandbox Code Playgroud)

由于使用硬编码的尾数,指数等初始化这些值是一个麻烦且容易出错的过程,我编写了一个模板,用于将十进制值转换为浮点值:

template<std::int64_t INT , std::uint64_t DECS>
struct decimal{};
Run Code Online (Sandbox Code Playgroud)

其中第一个参数表示积分部分,第二个参数表示小数位数.我认为这是一种常见且众所周知的方式.
然而,这种模式会遇到一些问题(如何输入负数小于一的数字?),其中最令我讨厌的事实之一就是没有办法在逗号之后输入零数字,即数字之类的0.00032.

我是C++ 11意识到的,我正在思考一个用户定义的文字+ decltype()方法(即使有一个宏#define FLOAT(x) decltype(x_MY_LITERAL)),但我不确定这种方法在所有情境中是否可行,我的意思是,如果文字+ decltype可在模板参数的上下文中进行评估.

即使这可行,我想知道是否有其他可能的方法来解决这个问题.那么,在编译时通过tmp进行浮点式初始化有哪些替代方案呢?


尝试替代方案:

仅仅为了完整性,我将描述我已经实施的替代方案,它们如何工作,以及它的功能和优点.问题本身仍然是开放的,允许任何人添加更多的替代品.

一些背景

首先,我将描述我使用过的功能,以确保每个人都能理解代码.

我的图书馆,Turbo Metaprogramming Library,基于三个原则:

  • 仅键入模板参数:完全通用的混合类型参数,值参数和模板模板参数非常难(几乎不可能),因此此库仅使用类型参数.无论何时需要使用值或模板,库都提供包装器以通过装箱传递这些参数.

  • 统一表达式评估:在编程语言中工作时的首要需求之一是评估表达式并获取其价值的方法.Turbo提供tml::eval元函数,它接受任何类型的表达式并返回(计算)其值.

  • 通过模板专业化定制的通用算法和元函数:每当我可以使用C++ 11模板别名来避免繁琐的typename ::type构造.我的惯例是在嵌套命名空间上定义实现模板(真正完成工作的元函数)impl,并在当前命名空间上定义结果的C++ 11模板别名.由于这些别名直接返回结果,因此它们无法在复杂表达式上进行求值(考虑元函数瞬时add<X,Y>,其中XY是lambda的变量.如果add是结果的别名,则不起作用,因为评估没有意义.如果我们需要表达式(元函数)而不是直接的结果,我的惯例是在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)"
Run Code Online (Sandbox Code Playgroud)

tml是库的主命名空间,浮点特征在tml::floating命名空间中公开.

TL; DR

  • tml::eval接受任何表达式并对其求值,返回其值.它是一个C++ 11模板别名,因此typename ::type不需要.

  • tml::integral_constant(只是别名std::integral_constant)是事实上的值包装器,用于通过装箱将值参数作为类型参数传递.该库具有仅使用类型参数的约定(模板模板参数也有包装,请参阅tml::lazytml::bind).

尝试1:来自整数

这里我们定义一个元函数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
};
Run Code Online (Sandbox Code Playgroud)

它的作用是直接取整数值,将其用作尾数,并将显式计算最高(最重要)设定位的数字标准化,相应地改变尾数.

它的一个例子可能是:

using ten = tml::floating::integer<10>;
Run Code Online (Sandbox Code Playgroud)

好处:

  • 效率:无需额外的复杂计算即可获得等效的浮点数.唯一相关的操作是打电话给highest_set_bit.

  • 默认情况下,该数字是标准化的(关于效率也是如此).此外,没有精确问题(至少不是小值).

缺点:

  • 仅适用于整数值.

尝试2:十进制初始化

此替代方案使用一对整数值分别表示数字的整数部分和小数部分:

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

using pi = decimal<3,141592654>;
Run Code Online (Sandbox Code Playgroud)

它的作用是计算积分部分的值(只需调用integer,前一次尝试)和小数部分的值.
小数部分的值是调整的整数值,直到小数点位于数字的开头.换一种说法:

                       integer<fractional_part>
fractional_value = ________________________________
                          10^number_of_digits
Run Code Online (Sandbox Code Playgroud)

那么数字的值就是两个值的总和:

result = integer_part_value + fractional_value
Run Code Online (Sandbox Code Playgroud)

整数的位数是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 :
                            ...
                           >;
} 
Run Code Online (Sandbox Code Playgroud)

因此它具有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
                                                             >
                                                    >
                                           >
                                  > 
Run Code Online (Sandbox Code Playgroud)

然后结果是:

 using result = tml::eval<tml::add<integral_f,fractional_value>>;
Run Code Online (Sandbox Code Playgroud)

好处

  • 允许实例化非整数值12.123.

缺点:

  • 性能: tml::pow是递归的,复杂度为O(n).tml::div对于浮点值,实现为分子乘以分母的倒数.该倒数通过Newton-Raphson近似计算(默认为五次迭代).

  • 精度问题:为计算功率而进行的连续乘法可能会导致累积的次要精度问题.对于计算除法的Newton-Raphson近似也是如此.

  • 符号是有限的:例如13.0004,由于整数文字0004无效,因此无法在该点之后指定尾随零的数字.

尝试3(3.1和3.2):十进制科学记数法

我们使用十进制(幂10)科学记数法来初始化浮点数,而不是使用硬编码数字来编写数字:

using pi = tml::floating::decimal_sci<3141592654,-9>; //3141592654 x 10^-9
Run Code Online (Sandbox Code Playgroud)

要计算数字,您只需要取有效值的值,然后乘以相应的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>>;
};
Run Code Online (Sandbox Code Playgroud)

这种尝试有一种改进,如果仅将其归一化为一个整数,则对待给定的有效值.因此,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
Run Code Online (Sandbox Code Playgroud)

好处

  • 带有宽数字的引线,包括标题零,以简单的方式.
  • 使用一个众所周知的符号,没有涉及的句法技巧.

缺点

  • 精度差,数量非常大/小(嗯,这就是科学符号的工作原理).注意,浮点内部计算可能导致累积精度误差,与尾数(尾数)和数字的指数成比例.是尝试的相同精度误差以上(来自的使用tml::pow,tml::div等等).

Ogu*_*guk 8

您可能希望使用用户定义的文字.根据cppreference.com,它

允许整数,浮点,字符和字符串文字通过定义用户定义的后缀来生成用户定义类型的对象.

(另见http://en.cppreference.com/w/cpp/language/user_literal).这样,您就可以制作表达式

123.456_mysuffix
Run Code Online (Sandbox Code Playgroud)

如果为_mysuffix定义文字运算符,则生成所需的任何类型.使用该运算符,您可以作为(标准c ++)浮点数访问输入123.456,或者您可以自己从原始字符串作为const char*进行必要的转换.

编辑:在阅读您编辑的问题并实现您正在讨论的模板元编程之后,我只想强调文字也可以作为char模板参数的参数包进行访问.您可以将它集成到编译时框架中.