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

标签 c++ templates c++11 floating-point template-meta-programming

我目前正在研究基于模板元编程的浮点算法实现。表示编译时浮点值的模板如下:

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

我知道C++ 11,我在考虑一种用户定义的文字+ decltype()方法(即使使用宏#define FLOAT(x) decltype(x_MY_LITERAL)),但我不确定该方法是否在所有情况下都可行,我的意思是,如果字面量+ decltype可在模板参数的上下文中求值。

即使可行,我也想知道是否还有其他解决此问题的方法。那么,在通过tmp进行编译时,像浮点一样的初始化有哪些替代方案?

尝试的替代方法:

仅出于完整性考虑,我将描述我已实现的替代方案,它们如何工作以及其const和优点。问题本身仍然是开放的,以允许任何人添加更多替代方案。

一些背景

首先,我将描述我所使用的功能,以确保每个人都了解代码。

我的库The Turbo Metaprogramming Library基于三个原则:
  • 仅类型模板参数:完全通用的混合类型参数,值参数和模板模板参数非常困难(几乎不可能),因此该库仅使用类型参数。每当需要使用值或模板时,该库都会提供包装器,以通过装箱传递此类参数。
  • 统一表达式评估:使用编程语言时的首要需求之一是评估表达式并获取其值(value)的方法。 Turbo提供了tml::eval元函数,该函数接受任何类型的表达式并返回(评估)其值。
  • 通过模板特化定制的通用算法和元函数:每当我可以使用C++ 11模板别名来避免繁琐的typename ::type构造时。我的约定是在嵌套的impl命名空间上定义实现模板(真正起作用的元函数),并在当前命名空间上为结果定义C++ 11模板别名。由于此类别名直接返回结果,因此无法在复杂表达式上求值(请考虑元函数实例化add<X,Y>,其中XY是lambda的变量。如果add是结果的别名,那将不起作用,因为求值没有意义,如果我们需要表达式(元函数)而不是直接返回结果,我的约定是在func嵌套 namespace 上为元函数添加别名。

    这里有些例子:
    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是该库的主要 namespace ,并且浮点功能在tml::floating namespace 中公开。

    TL; DR
  • tml::eval接受任何表达式并对其求值,并返回其值。它是C++ 11模板别名,因此不需要typename ::type
  • tml::integral_constant(只是std::integral_constant的别名)是实际值包装器,用于通过装箱将值参数作为类型参数传递。该库仅使用类型参数作为惯例(模板模板参数也有包装器,请参见 tml::lazy tml::bind )。

  • 尝试1:从整数开始

    在这里,我们定义一个元函数integer,该函数从整数1返回浮点值:
    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):十进制科学计数法

    代替使用硬编码数字写数字,我们使用十进制(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::powtml::div等的使用)。
  • 最佳答案

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

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



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

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

    编辑:阅读完您编辑过的问题并意识到您正在谈论的模板元编程类型之后,我只是想强调一下,也可以将文字作为char模板参数的参数包进行访问。您可能可以将其集成到编译时框架中。

    关于c++ - 编译时浮点初始化的替代方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25272844/

    相关文章:

    c++ - Random_shuffle 可以在没有 <algorithm> 库的情况下运行吗?

    c++ - 如何在 Windows 上使用和配置 clang-tidy?

    c++ - 这个可变参数模板代码有什么作用?

    c++ - CLion:无法解析 <member_variable_name>

    c++ - 不能在非 Boost 版本的 Asio 中使用 asio::placeholders::error

    c++ - 为什么 vector 初始化构造和复制?

    c++ - C++中运算符重载的这些方法的区别

    c++ - 是否可以将html和js嵌入到QwebKit中

    c++ - 对于 GCC 的 "sorry, unimplemented: cannot expand ‘NEXT ...’ 到固定长度参数列表“错误”,是否有一个很好的解决方法?

    c++ - 多次解包参数包