【发布时间】:2014-10-06 00:38:32
【问题描述】:
我目前正在研究基于模板元编程的浮点运算实现。表示编译时浮点值的模板如下:
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 进行类似浮点的初始化有哪些替代方案?
尝试过的替代方案:
为了完整起见,我将描述我已经实现的替代方案、它们的工作原理以及它的优点和优点。问题本身仍然是开放的,允许任何人添加更多替代方案。
一些背景
首先我将描述我使用的功能,以确保每个人都理解代码。
我的库 The Turbo Metaprogramming Library 基于三个原则:
仅类型模板参数:完全通用混合类型参数、值参数和模板模板参数真的很难(几乎不可能),所以这个库只使用类型参数。每当需要使用值或模板时,该库都会提供包装器以通过装箱传递此类参数。
统一表达式求值:使用编程语言工作时的首要需求之一是求值表达式并获取其值的方法。 Turbo 提供了
tml::evalmetafunction,它接受任何类型的表达式并返回(评估)其值。-
通过模板专业化定制的通用算法和元函数:只要我可以使用 C++11 模板别名来避免繁琐的
李>typename ::type构造。我的约定是在嵌套的impl命名空间上定义实现模板(真正完成工作的元函数),并在当前命名空间上为结果定义一个C++11 模板别名。由于此类别名直接返回结果,因此它们无法在复杂表达式上求值(考虑元函数实例化add<X,Y>,其中X和Y是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)"
tml 是库的主命名空间,浮点特性暴露在 tml::floating 命名空间上。
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实现为分子乘以分母的倒数。该倒数通过 Newton-Raphson 近似计算(默认为 5 次迭代)。精度问题:计算幂的顺序乘法可能会导致累积的小精度问题。计算除法的 Newton-Raphson 近似也是如此。
符号是有限的:无法指定在点后带有尾随零的数字,例如
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::pow、tml::div等的用法)。
【问题讨论】:
-
科学记数法不是更合适吗?也就是说,将第一个模板整数视为在第一个非零数字之后有一个小数点,而另一个参数是十进制指数?所以
decimal<123,4>对应1.23e4? -
@Ben 十进制科学记数法,谢谢!我在问这个问题时完全忘记了那个:(。这可能是另一种选择(如果你愿意,写一个答案),但我以自然(十进制)方式询问更多关于初始化的信息,比如
123.33。 -
标准库中存在类似的东西,但它不是用于“小数”,而是用于“有理数”,即
std::ratio。它确实std::ratio<Numerator, Denominator>。如果这不是一个选项,我认为decimal<123, 33>将是代表123.33的好方法。 -
@Rapptz:“我认为十进制 是表示 123.33 的好方法”——这个问题正是关于这种方法的问题,例如你将如何表示
123.033或 @ 987654381@? -
@MichaelBurr 确切地说,问题是关于浮点初始化的不同方法,以及它们的优缺点。我目前实现了 5 种不同的方法(整数初始化、十进制初始化
decimal<integral_part,decimals>、十进制科学记数法、标准化十进制科学记数法,最后通过用户定义的文字和解析元函数解析浮点文字),我的想法是包括所有这些都在问题的尝试的解决方案部分。
标签: c++ templates c++11 floating-point template-meta-programming