编译时浮点初始化的替代 [英] Alternatives for compile-time floating-point initialization

查看:264
本文介绍了编译时浮点初始化的替代的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在进行一个基于模板元程序的浮点运算实现。表示编译时浮点值的模板如下:

  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



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



即使这可以工作,我想知道这个问题是否还有其他可能的方法。因此,在编译时通过tmp进行浮点类初始化的替代方法是什么?






设想的替代方案:



为了完整性,我将描述我已经实现的替代方法,它们如何工作,以及它的常量和优点。



一些背景



首先,我将描述



我的图书馆, Turbo元编程库基于三个原则:




  • 类型模板参数:完全通用的混合类型参数,值参数和模板模板参数确实很难(几乎不可能),因此此库仅使用类型参数。


  • 统一表情评估:一个的第一个需要在编程语言中工作是一种方式来评估表达式并采取它的价值。 Turbo提供 tml :: eval 元功能,它接受任何类型的表达式并返回(计算)其值。


  • 通过模板专业化定制的通用算法和元函数:每当我可以使用C ++ 11模板别名,以避免繁琐的 typename :: type 施工。我的约定是在嵌套的 impl 命名空间上定义实现模板(真正做工作的元函数)和C ++ 11模板别名到当前命名空间的结果。由于这种别名直接返回结果,所以它们不能在复杂表达式上求值(考虑元函数实例 add< X,Y> ,其中 / code>和 Y 是lambda的变量。如果 add 是结果的别名,如果我们需要直接使用表达式(元函数)而不是其结果,我的约定是在 func 嵌套命名空间。




以下是一些示例:

 使用bits = tml :: util :: sizeof_bits< int> ;; // bits是一个size_t整数常量,其中
//大小在int
的位上
//使用double_size = tml :: lambda< _1,tml :: mul< tml :: util :: func :: sizeof_bits< _1>返回
类型的位的大小的元函数。 ,tml :: Int 2>> ;;;

使用int_double_size = tml :: eval< double_size,int> // read asdouble_size(int)

tml 是库的主命名空间,浮点功能暴露在 tml :: floating 命名空间。



TL; DR




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


  • code> tml :: integral_constant (只是 std :: integral_constant 的别名)是用于传递值参数的事实值作为类型参数通过拳击。该库具有仅使用类型参数的约定(还有模板模板参数的包装器,参见 tml :: lazy tml :: bind )。




尝试1:从整数



这里我们定义一个元函数 integer 从整数一个点的值:

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

使用result = tml :: floating :: number< S,exp,(m :: mantissa<<(31-hsb :: value))> //注意数字是标准化的
};

它是直接取整数值,使用它作为尾数,



它的用法示例可以是:

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



优点:




  • 效率:无需额外的复杂计算即可获得等效浮点数。唯一相关的操作是调用 highest_set_bit


  • 效率)。还有没有精确度问题(至少不是小值)。




缺点:




  • 仅适用于整数值。



尝试2:



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

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

使用pi = decimal< 3,141592654> ;;

它是计算整数部分的值(只需调用 integer ,上一次尝试)和小数部分的值。

小数部分的值是在小数点开始之前调整的整数值的数量。换句话说:

 整数< fractional_part> 
fractional_value = ________________________________
10 ^ number_of_digits

只是两个值的总和:

  result = integer_part_value + fractional_value 

整数数字的位数为 log10(number)+ 1 。对于不需要递归的整数值,我最终得到了一个 log10 元函数:

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

所以它有O(1)复杂度)。



使用此元功能,上面的公式变为:

  //首先一些别名,使代码更方便:
使用integral_i = tml :: integral_constant< std :: int64_t,INTEGRAL> ;;
using integer_f = tml :: floating :: integer< INTEGRAL>
using fractional_f = tml :: floating :: integer< FRACTIONAL> ;;
using ten = tml :: floating :: integer< 10> ;;
using one = tml :: Int< 1>

使用fractional_value = tml :: eval< tml :: div< fractional_f,
tml :: pow< ten,
tml :: add< tml :: log10& ,
one
>
>
>
>

然后结果是:

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



优点




  • 允许实现 12.123 的非整数值。



缺点: / h3>


  • 效果: tml :: pow 是递归的,具有O(n)的复杂性。对于浮点值, tml :: div 实现为分子与分母的倒数的乘法。


  • 精确度问题:执行的顺序乘法计算功率可能导致累积的小精度​​问题。


  • 符号有限:没有办法指定在 13.0004 后面跟随零的数字,因为整数文本 0004 无效。




尝试3(3.1和3.2):小数科学记数法



我们使用十进制(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>
使用power = tml :: eval< tml :: pow< tml :: floating :: integer>,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> ;;

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

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

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



优点



    $ b
  • 使用众所周知的符号,不涉及语法技巧。



缺点




  • 非常大/期望自那以来科学表示法如何工作)。注意,浮点内部计算可能导致累积精度误差,与数字的长度(尾数)的长度成比例。是与上述尝试相同的精度错误(从使用 tml :: pow tml :: div
  • 您可能想要使用用户定义的文字。 根据cppreference.com,它


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


    (另请参见 http://en.cppreference.com/w/cpp/language/user_literal )。这样,您可以使表达式

      123.456_mysuffix 

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



    编辑:阅读你编辑的问题,并实现你正在谈论什么样的模板元程序,我只是想强调,字面量也可以作为参数包访问 char 模板参数。您可以将它集成到您​​的编译时框架中。


    I'm currently working on a template-meta-programming based implementation of floating-point arithmetic. The template which represent compile-time float values is as follows:

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

    Since initializing such values using hardcoded mantissas, exponents, etc, is a cumbersome and bug-prone process I have written a template for converting decimal values to floating-point ones:

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

    Where the first parameter represents the integral part and the second the fractional digits. I think this is a common and well known way.
    However this pattern suffers from some issues (How I enter negative less-than-one numbers?), where one of the most annoying for me is the fact that there is no way to enter zero digits just after the comma, i.e., numbers like 0.00032.

    I'm C++11 aware, and I was thinking about a user-defined-literal + decltype() approach (Even with a macro #define FLOAT(x) decltype(x_MY_LITERAL)) but I'm not sure that approach is possible in all contexts, I mean, if the literal + decltype is evaluable in the context of a template parameter.

    Even if that could work, I want to know if there are other possible approaches for this problem. So, what alternatives are there for floating-point-like initialization at compile-time via tmp?


    Attemped alternatives:

    Just for completeness shake, I will describe the alternatives I have implemented, how they work, and its consts and pros. The question itself remains open, to allow anybody to add more alternatives.

    Some background

    First I will describe the features I have used, just to make sure everybody understand the code.

    My library, The Turbo Metaprogramming Library, is based on three principles:

    • Type template parameters only: Being completely generic mixing type parameters, value parameters, and template-template parameters is really hard (Near impossible), so this library uses type parameters only. Whenever is necessary to use values or templates, the library provides wrappers to pass such parameters through boxing.

    • Uniform expression evaluation: One of the first needs when working in a programming language is a way to evaluate expressions and take its value. Turbo provides the tml::evalmetafunction, which takes any kind of expression and returns (evaluates) its value.

    • Generic algorithms and metafunctions customized via template specialization: Whenever I can I use C++11 template aliases to avoid the cumbersome typename ::type construction. My convention is to define implementation templates (The metafunctions which really do the work) on a nested impl namespace, and a C++11 template alias to the result on the current namespace. Since such aliases return the result directly, they are not evaluable on a complex expression (Consider a metafunction instantation add<X,Y>, where X and Y are variables of a lambda. If add was an alias to the result, that doesn't work because the evaluation has no sense. If we need the expression (metafunction) instead of its result directly, my convention was to put an alias to the metafunction on a func nested namespace .

    Here are some examples:

    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 is the main namespace of the library, and floating-point features are exposed on the tml::floating namespace.

    TL;DR

    • tml::eval takes any expression and evaluates it, returning its value. Its a C++11 template alias, so typename ::type is not needed.

    • tml::integral_constant (Just an alias of std::integral_constant) is the de-facto value wrapper for passing value parameters as type parameters through boxing. The library has the convention of using type-parameters only (There are wrappers for template-template parameters too, see tml::lazy and tml::bind).

    Attempt 1: From integer

    Here we define a metafunction integer which returns a floating-point value from an integer one:

    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
    };
    

    What it does is to take the integral value directly, use it as mantissa, and normalize the number explicitly computing the highest (most significant) set bit, shifting the mantissa acordingly.

    An example of its ussage could be:

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

    Advantages:

    • Efficiency: No extra complex computations are required to obtain the equivalent floating point number. The only relevant operation is the call to highest_set_bit.

    • The number is normalized by default (Regarding on efficiency too). Also there are no precision issues (At least not for small values).

    Disadvantages:

    • Only works with integral values.

    Attempt 2: Decimal initialization

    This alternative uses a pair of integral values to represent the integral and fractional parts of the number respectively:

    template<std::int64_t INTEGRAL , std::uint64_t FRACTIONAL>
    struct decimal{ ... };
    
    using pi = decimal<3,141592654>;
    

    What it does is to compute the value of the integral part (Just call to integer, the previous attempt) and the value of the fractional part.
    The value of the fractional part is the value of the integer adjusted until the radix point is at the beginning of the number. In other words:

                           integer<fractional_part>
    fractional_value = ________________________________
                              10^number_of_digits
    

    Then the value of the number is just the sum of both values:

    result = integer_part_value + fractional_value
    

    The number of digits of an integral number is log10(number) + 1. I have ended up with a log10 metafunction for integral values that doesn't require recursion:

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

    So it has O(1) complexity (Measuring template instantation depth, of course).

    With this metafunction, the formula above becomes:

    //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
                                                                 >
                                                        >
                                               >
                                      > 
    

    And then the result is:

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

    Advantages

    • Allows instancing non-integral values like 12.123.

    Disadvantages:

    • Performance: tml::pow is recursive, with a complexity of O(n). tml::div for floating-point values is implemented as a multiplication of the numerator by the reciprocal of the denominator. That reciprocal is computed by a Newton-Raphson approximation (Five iterations by default).

    • Precision issues: The sequential multiplications done to compute the power could lead to accumulative minor precision issues. The same for the Newton-Raphson approximation done to compute the division.

    • The notation is limited: There is no way to specify numbers with trailing zeros after the point, say 13.0004, since an integer literal 0004 is not valid.

    Attempt 3 (3.1 and 3.2): Decimal scientific notation

    Instead of writing the number using hardcoded digits, we use decimal (Power of 10) scientific notation to initialize floating-point numbers:

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

    To compute the number you only have to take the value of the significant, and multiply it by the corresponding power of 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>>;
    };
    

    There is an improvement for this attempt, which treats the given significant if it was normalized to one integer digit only. So a value 0.0034565432 could be written as (34565432 , -3) instead of (34565432 , -11).
    I call it 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
    

    Advantages

    • Leads with wide numbers, with heading zeros included, in a simple way.
    • Uses a well known notation, no syntactic tricks involved.

    Disadvantages

    • Poor precision with very large/small numbers (Well, thats expected since thats how scientific-notation works). Note the floating-point internal computations could lead to accumulative precision errors, proportional to the length (of the mantissa) and exponent of the number. Are the same precision errors of the attempts above (From the usage of tml::pow, tml::div, etc).

    解决方案

    You might want to use user-defined literals. According to cppreference.com, it

    Allows integer, floating-point, character, and string literals to produce objects of user-defined type by defining a user-defined suffix.

    (see also http://en.cppreference.com/w/cpp/language/user_literal). This way, you could make the expression

    123.456_mysuffix
    

    yield whatever type you want, if you define the literal operator for _mysuffix. With that operator, you can access the input 123.456 either as a (standard c++) floating point number or you can do the necessary conversion from the raw string as a const char* yourself.

    EDIT: After reading your edited question and realizing what kind of template meta-programming you were talking about, I just wanted to emphasize that the literal can also be accessed as a parameter pack of char template parameters. You might be able to integrate this into your compile time framework.

    这篇关于编译时浮点初始化的替代的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆