实现 vector 的四则运算

这里假设 vector 的运算定义为对操作数 vector 中相同位置的元素进行运算,最后得到一个新的 vector。具体来说就是,假如 vector<int> d1{1, 2, 3}, d2{4, 5, 6};则, v1 + v2 等于 {5, 7, 9}。实现这样的运算看起来并不是很难,一个非常直观的做法如下所示:

vector<int> operator+(const vector<int>& v1, const vector<int>& v2) {
// 假设 v1.size() == v2.size()
vector<int> r; r.reserve(v1.size());
for (auto i = 0; i < v1.size(); ++i) {
r.push_back(v1[i] + v2[i]);
}
return r;
} // 同理,需要重载其它运算符

我们针对 vector 重载了每种运算符,这样一来,vector 的运算就与一般简单类型无异,实现也很直白明了,但显然这个直白的做法有一个严重的问题:效率不高。效率不高的原因在于整个运算过程中,每一步的运算都产生了中间结果,而中间结果是个 vector,因此每次都要分配内存,如果参与运算的 vector 比较大,然后运算又比较长的话,效率会比较低,有没有更好的做法?

既然每次运算产生中间结果会导致效率问题,那能不能优化掉中间结果?回过头来看,这种 vector 的加减乘除与普通四则运算并无太大差异,在编译原理中,对这类表达式进行求值通常可以通过先把表达式转为一棵树,然后通过遍历这棵树来得到最后的结果,结果的计算是一次性完成的,并不需要保存中间状态,比如对于表达式:v1 + v2 * v3,我们通常可以先将其转化为如下样子的树:

因此求值就变成一次简单的中序遍历,那么我们的 vector 运算是否也可以这样做呢?

表达式模板

要把中间结果去掉,关键是要推迟对表达式的求值,但 c++ 语言成面上不支持 lazy evaluation,因此需要想办法把表达式的这些中间步骤以及状态,用一个轻量的对象保存起来,具体来说,就是需要能够将表达式的中间步骤的操作数以及操作类型封装起来,以便在需要时能动态的执行这些运算得到结果,为此需要定义类似如下这样一个类:

enum OpType {
OT_ADD,
OT_SUB,
OT_MUL,
OT_DIV,
}; class VecTmp {
int type_;
const vector<int>& op1_;
const vector<int>& op2_; public:
VecTmp(int type, const vector<int>& op1, const vector<int>& op2)
: type_(type), op1_(op1), op2_(op2) {} int operator[](const int i) const {
switch(type_) {
case OT_ADD: return op1_[i] + op2_[i];
case OT_SUB: return op1_[i] - op2_[i];
case OT_MUL: return op1_[i] * op2_[i];
case OT_DIV: return op1_[i] / op2_[i];
default: throw "bad type";
}
}
};

有了这个类,我们就可以把一个简单的运算表达式的结果封装到一个对象里面去了,当然,我们得先将加法操作符(以及其它操作符)重载一下:

VecTmp operator+(const vector<int>& op1, const vector<int>& op2) {
return VecTmp(OT_ADD, op1, op2);
}

这样一来,对于 v1 + v2,我们就得到了一个非常轻量的 VecTmp 对象,而该对象可以很轻松地转化为 v1 + v2 的结果(遍历一遍 VecTmp 中保存的操作数)。但上面的做法还不能处理 v1 + v2 * v3 这样的套嵌的复杂表达式:v2 * v3 得到一个 VecTmp,那 v1 + VecTmp 怎么搞呢?

同理,我们还是得把 v1 + VecTmp 放到一个轻量的对象里,因此最好我们的 VecTmp 中保存的操作数也能是 VecTmp 类型的,有点递归的味道。。。用模板就可以了,于是得到如下代码:

#include <vector>
#include <iostream> using namespace std; enum OpType {
OT_ADD,
OT_SUB,
OT_MUL,
OT_DIV,
}; template<class T1, class T2>
class VecSum {
OpType type_;
const T1& op1_;
const T2& op2_;
public:
VecSum(int type, const T1& op1, const T2& op2): type_(type), op1_(op1), op2_(op2) {} int operator[](const int i) const {
switch(type_) {
case OT_ADD: return op1_[i] + op2_[i];
case OT_SUB: return op1_[i] - op2_[i];
case OT_MUL: return op1_[i] * op2_[i];
case OT_DIV: return op1_[i] / op2_[i];
default: throw "bad type";
}
}
}; template<class T1, class T2>
VecSum<T1, T2> operator+(const T1& t1, const T2& t2) {
return VecSum<T1, T2>(OT_ADD, t1, t2);
} template<class T1, class T2>
VecSum<T1, T2> operator*(const T1& t1, const T2& t2) {
return VecSum<T1, T2>(OT_MUL, t1, t2);
} int main() {
std::vector<int> v1{1, 2, 3}, v2{4, 5, 6}, v3{7, 8, 9};
auto r = v1 + v2 * v3;
for (auto i = 0; i < r.size(); ++i) {
std::cout << r[i] << " ";
}
}

上面的代码漂亮地解决了前面提到的效率问题,扩展性也很好(能够方便地增加对其它运算类型的支持),而且对 vector 来说还是非侵入性的,当然了,实现上乍看起来可能就不是很直观了,除此也还有些小问题可以更完善:

  1. 操作符重载那里很可能会影响别的类型,因此最好限制一下,只针对 vector<int>VecTmp<> 进行重载,这里可以用 SFINAE 来处理。
  2. VecTmp<> 的 operator[] 函数中的 switch 可以优化掉,VecTmp<> 模板只需增加一个参数,然后对各种运算类型进行偏特化就可以了。
  3. VecTmp<> 对操作数的类型是有隐性要求的,只能是 vector<int> 或者是 VecTmp<>,这里也应该用 SFINAE 强化一下限制,使得用错时出错信息可以好看些。

现在我们来重头再看看这一小段奇怪的代码,显然关键在于 VecTmp<> 这个模板,我们可以发现,它的接口其实很简单直白,但它的类型却可以是那么地复杂,比如说对于 v1 + v2 * v3 这个表达式,它的结果的类型是这样的: VecTmp<vector<int>, VecTmp<vector<int>, vector<int>>>,可以想象如果表达式再复杂些,它的类型也会跟着更复杂,如果你看仔细点,是不是还发现这东西和哪里很像?像一棵树,一棵类型的树:

这棵树看起来是不是还很眼熟,每个叶子结点都是 vector<int>,而每个内部结点则是由 VecTmp<> 实例化的:这是一棵类型的树,在编译时就确定了。这种通过表达式在编译时得到的复杂类型有一个学名叫: Expression template。在 c++ 中每一个表达式必产生一个结果,而结果必然有类型,类型是编译时的东西,结果却是运行时的。像这种运算表达式,它的最终类型是由表达式中每一步运算所产生的结果所对应的类型组合起来所决定的,类型确定的过程其实和表达式的识别是一致的。

而 VecTmp 对象在逻辑上其实也是一棵树,它的成员变量 op1_, op2_ 分别是左右儿子结点,树的内部结点代表一个运算,一个动作,左右儿子为其操作数,叶子结点则代表直接数(或 terminal),一遍中序遍历下来,得到的就是整个表达式的值。

神奇的 boost::proto

expression template 是个好东西(就正如 expression SFINAE 一样),它能帮助你根据给定的表达式,在编译时建立非常复杂好玩的类型(从而实现很多高级玩意,主要是函数式,EDSL 等)。但显然如果什么东西都需要自己从头开始写,这个技术用起来还是很麻烦痛苦的,好在模板元编程实在是个太好玩的东西,已经有很多人做了很多先驱性的工作,看看 boost proto 吧,在 c++ 的世界里再打开一扇通往奇怪世界的大门。

一种高效的 vector 四则运算处理方法的更多相关文章

  1. Java 开发者不容错过的 12 种高效工具

    Java 开发者常常都会想办法如何更快地编写 Java 代码,让编程变得更加轻松.目前,市面上涌现出越来越多的高效编程工具.所以,以下总结了一系列工具列表,其中包含了大多数开发人员已经使用.正在使用或 ...

  2. Java集合系列(二):ArrayList、LinkedList、Vector的使用方法及区别

    本篇博客主要讲解List接口的三个实现类ArrayList.LinkedList.Vector的使用方法以及三者之间的区别. 1. ArrayList使用 ArrayList是List接口最常用的实现 ...

  3. Linux下一种高效多定时器实现

    Linux下一种高效多定时器实现 作者:LouisozZ 日期:2018.08.29 运行环境说明 由于在 Linux 系统下一个进程只能设置一个时钟定时器,所以当应用需要有多个定时器来共同管理程序运 ...

  4. 两种高效的事件处理模式(Proactor和Reactor)

    典型的多线程服务器的线程模型 1. 每个请求创建一个线程,使用阻塞式 I/O 操作 这是最简单的线程模型,1个线程处理1个连接的全部生命周期.该模型的优点在于:这个模型足够简单,它可以实现复杂的业务场 ...

  5. C++的vector的使用方法

    vector c++的vector的使用方法,创建,初始化,插入,删除等. #include "ex_vector.h" #include <iostream> #in ...

  6. linux几种快速清空文件内容的方法

    linux几种快速清空文件内容的方法 几种快速清空文件内容的方法: $ : > filename #其中的 : 是一个占位符, 不产生任何输出. $ > filename $ echo & ...

  7. 【Android】一种提高Android应用进程存活率新方法

    [Android]一种提高Android应用进程存活率新方法 SkySeraph Jun. 19st 2016 Email:skyseraph00@163.com 更多精彩请直接访问SkySeraph ...

  8. [转]SQL三种获取自增长的ID方法

     最新更新请访问: http://denghejun.github.io   SQL SERVER中的三种获得自增长ID的方法  这个功能比较常用,所以记下来以防自己忘掉. SCOPE_IDENTIT ...

  9. CSharpGL(40)一种极其简单的半透明渲染方法

    CSharpGL(40)一种极其简单的半透明渲染方法 开始 这里介绍一个实现半透明渲染效果的方法.此方法极其简单,不拖累渲染速度,但是不能适用所有的情况. 如下图所示,可以让包围盒显示为半透明效果. ...

随机推荐

  1. [jQuery学习系列二 ]2-JQuery学习二-数组操作

    前言 上一篇内容 已经对于Jquery 有了一些认识, 包括Jquery的选择器和DOM对象, 那么这一篇继续来看下Jquery中很实用的Jquery对于数组的操作. Jquery中对数组的操作大致有 ...

  2. atitit.压缩算法 ZLib ,gzip ,zip 最佳实践 java .net php

    atitit.压缩算法 ZLib ,gzip ,zip   最佳实践  java .net php 1. 压缩算法的归类::: 纯算法,带归档算法 1 2. zlib(适合字符串压缩) 1 3. gz ...

  3. Android模拟器配置

    // 检查连接设备,为了保证运行正常,只能留一个连接设备 adb devices

  4. spring源码 — 二、从容器中获取Bean

    getBean 上一节中说明了容器的初始化,也就是把Bean的定义GenericBeanDefinition放到了容器中,但是并没有初始化这些Bean.那么Bean什么时候会初始化呢? 在程序第一个主 ...

  5. 无锁编程以及CAS

    无锁编程 / lock-free / 非阻塞同步 无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Sy ...

  6. Unity 5.4 测试版本新特性---因吹丝停

    原文:http://blogs.unity3d.com/2016/03/15/enhanced-visuals-better-performance-and-more-the-unity-5-4-pu ...

  7. SFTP+OpenSSH+ChrootDirectory设置

    账户设置 SFTP的账户直接使用Linux操作系统账户,我们可以用useradd命令来创建账户. 首先建立3个要管理的目录:   1 2 3 mkdir /home/sftp/homepage mkd ...

  8. CCNA实验1.port-security

    一, 二,MAC地址绑定 3550-1#conf t3550-1(config)#int f0/13550-1(config-if)#switchport mode access /指定端口模式.35 ...

  9. HTTP 错误 500.21 - Internal Server Error 处理程序“ExtensionlessUrlHandler-Integrated-4.0”在其模块列表中有一个错误模块“ManagedPipelineHandler”

    导致这个错误出现的原因是因为.net Framework4.0没有注册 解决方法:打开运行命令行,运行下面的命令: C:\WINDOWS\Microsoft.NET\Framework\v4.0.30 ...

  10. js判断密码强度

    html代码: <form name="form1" action=""> 密码:<input type="password&quo ...