问题描述
我目前正在进行一个基于模板元程序的浮点运算实现。表示编译时浮点值的模板如下:
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提供
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
的别名)是用于传递值参数的事实值作为类型参数通过拳击。该库具有仅使用类型参数的约定(还有模板模板参数的包装器,参见和)。
尝试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 ...
> ;;
}
所以它有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):小数科学记数法
效果: tml :: pow
是递归的,具有O(n)的复杂性。对于浮点值, tml :: div
实现为分子与分母的倒数的乘法。
精确度问题:执行的顺序乘法计算功率可能导致累积的小精度问题。
符号有限:没有办法指定在 13.0004
后面跟随零的数字,因为整数文本 0004
无效。
我们使用十进制(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,它
(另请参见)。这样,您可以使表达式
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::eval
metafunction, 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 nestedimpl
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 instantationadd<X,Y>
, whereX
andY
are variables of a lambda. Ifadd
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 afunc
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, sotypename ::type
is not needed.tml::integral_constant
(Just an alias ofstd::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, seetml::lazy
andtml::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 literal0004
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
(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.
这篇关于编译时浮点初始化的替代的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!