目录
第18章 表达式模板
在这一章里, 我们将介绍一种称为表达式模板(expressiontemplate) 的编程技术。 刚开始, 是为了支持一种数值数组的类而引入该技术的。 因此, 在这一章里, 我们把数值数组作为讨论表达式模板的着眼点。
对于一个数值数组类, 它需要为基于整个数组对象的数值操作提供支持。 例如, 我们可能需要对两个数组进行求和, 最后结果所含的每个元素是两个实参数组中对应元素值之和。 类似地, 我们也可以对整个数组进行放大(即我们后面所指的scalar) , 也就是说数组中的每个元素都乘以一个大于1的值。 通常而言, 我们期望可以像内建类型一样, 让数组也具有这样的放大(scalar) 运算符:
Array<double> x(1000), y(1000);
…
x=1.2*x + x*y;
谈到表达式模板, 我们自然就会想起前面的template metaprogramming。 之所以会有这样的联系, 一方面是由于: 表达式模板有时依赖于深层的嵌套模板实例化, 而这种实例化又和我们在template metaprogramming中遇到的递归实例化非常相似(见17.7节的例子) ; 另一方面则是由于: 最初开发这两种实例化技术都是为了支持高性能的数组操作, 而这又从另一个侧面说明了metaprogramming和表达式模板是息息相关的。 当然, 这两种技术还是互补的。 例如,metaprogramming 主要用于小的、 大小固定的数组, 而表达式模板则适用于能够在运行期确定大小、 中等大小的数组。
18.1 临时变量和分割循环
在深入了解表达式模板之前, 让我们先来看一种比较简单的 、 用于实现数值数组操作的模板实现。 其中基本的数组模板看起来如下所示(SArray的含义是simple array) :
template<typename T>
class SArray
{
public:
// 创建一个具有初始值大小的数组
explicit SArray(size_t s)
: storage(new T[s]), storage_size(s) {
init();
}
//拷贝构造函数
SArray(SArray<T> const& orig)
: storage(new T[orig.size()]), storage_size(orig.size()) {
copy(orig);
}
//析构函数: 释放内存空间
~SArray() {
delete[] storage;
}
// 赋值运算符
SArray<T>& operator= (SArray<T> const& orig)
{
if (&orig != this) {
copy(orig);
}
return *this;
}
//返回数组大小
size_t size() const
{
return storage_size;
}
//针对常数和变量的下标运算符
T operator[] (size_t idx) const
{
return storage[idx];
}
T& operator[] (size_t idx)
{
return storage[idx];
}
protected:
// 运用缺省构造函数来初始化值
void init()
{
for (size_t idx = 0; idx < size(); ++idx)
{
storage[idx] = T();
}
}
//拷贝另一个数组的值
void copy(SArray<T> const& orig)
{
assert(size() == orig.size());
for (size_t idx = 0; idx < size(); ++idx)
{
storage[idx] = orig.storage[idx];
}
}
private:
T* storage; // 元素的存储空间
size_t storage_size; // 元素的个数
};
//而数值运算符可以编码如下:
// exprtmpl/sarrayops1.hpp
// 对两个SArrays求和
template<typename T>
SArray<T> operator+ (SArray<T> const& a, SArray<T> const& b)
{
SArray<T> result(a.size());
for (size_t k = 0; k < a.size(); ++k) {
result[k] = a[k] + b[k];
}
return result;
}
//对两个SArray求积
template<typename T>
SArray<T> operator* (SArray<T> const& a, SArray<T> const& b){
SArray<T> result(a.size());
for (size_t k = 0; k < a.size(); ++k) {
result[k] = a[k] * b[k];
}
return result;
}
//让一个SArray 乘以一个放大倍数
template<typename T>
SArray<T> operator* (T const& s, SArray<T> const& a)
{
SArray<T> result(a.size());
for (size_t k = 0; k < a.size(); ++k) {
result[k] = s*a[k];
}
return result;
}
//对SArray和scalar求积
// 对scalar 和 SArray求和
// 对SArray 和 scalar 求和
…
我们还可以写出其他的一些版本, 也可以类似地添加其他的一些运算符。 但为了简单起见, 上面的这些运算符已经足够考察下面的例子表达式了:
int main()
{
SArray<double> x(1000), y(1000);…
x = 1.2*x + x*y;
}
显然, 上面的实现是非常低效的, 其原因主要是以下两方面:
1.每个运算符操作(除了赋值运算符) 至少需要生成了一个临时数组(也就是说, 在我们的例子中, 即使编译器不执行任何附加的临时拷贝操作, 也至少会生成3个大小为1 000的临时数组) 。
2.运算符程序的每次使用都要求对实参和结果数组进行额外的遍历(这就是说, 在我们的例子中, 即使只是生成了一个SArray对象, 大概也要读取6 000次double值, 写入4 000次double值) 。
让我们通过下面运用临时变量的表达式, 具体地分析上面的这些结论:
tmp1 = 1.2*x; // 循环1 000次子操作(即元素操作) , 再加上创建和删除tmp1
tmp2 = x*y // 循环1 000次子操作, 再加上创建和删除tmp2
tmp3 = tmp1+tmp2; // 循环1 000次子读操作、 1 000次写操作,再加上生成和删除tmp3
x = tmp3; // 1 000次读操作和1 000次写操作
对于元素个数少的数组而言, 除非能够分配非常快速的内存配置器, 否则创建多余临时对象的过程通常都会占用每个操作的大部分时间; 而对于元素个数很多的数组而言, 则是完全不允许生成临时对象的, 因为根本就没有足够的内存来容纳这些临时对象。
实际上, 每个数值数组程序库的实现都会面临这个问题, 因此通常鼓励我们多使用包含计算的赋值运算符(computed assignments, 诸如+=、 *=等) , 来代替前面纯粹的赋值运算符。 使用包含计算的赋值运算符的好处在于: 由于实参和结果都是由调用者提供, 因此将不需要创建任何临时对象。 例如, 我们可以这样添加SArray成员:
// SArray的自加运算符
template<class T>
SArray<T>& SArray<T>::operator+= (SArray<T> const& b)
{
for (size_t k = 0; k < size(); ++k) {
(*this)[k] += b[k];
}
return *this;
}
//SArray的自乘运算符
template<class T>
SArray<T>& SArray<T>::operator*= (SArray<T> const& b)
{
for (size_t k = 0; k < size(); ++k) {
(*this)[k] *= b[k];
}
return *this;
}
//针对放大倍数的自乘运算符
templat