C++实现 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 来说还是非侵入性的,虽然实现上乍看起来可能不是很直观,除此也还有些小问题可以更完善些:

操作符重载那里很可能会影响别的类型,因此最好限制一下,只针对 vector 和 VecTmp 进行重载,这里可以用 SFINAE 来处理。

VecTmp 的 operator[] 函数中的 switch 可以优化掉,VecTmp 模板只需增加一个参数,然后对各种运算类型进行偏特化就可以了。

VecTmp 对保存的操作数是有要求的,只能是 vector 或者是 VecTmp<>,这里也应该用 SFINAE 强化一下限制,使得用错时出错信息好看些。

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

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

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

神奇的 boost::proto

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

(0)

相关推荐

  • 深入理解C++中的vector类的用法及特性

    //<vector> template < class T, class Alloc = allocator<T> > class vector; 向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence container).跟任意其它类型容器一样,它能够存放各种类型的对象.可以简单的认为,向量是一个能够存放任意类型的动态数组. vector类为内置数组提供了一种替代表示,与string类一样 vector 类是随标准 C++引入的标准库的一部分 ,为

  • 浅谈c++ vector和map的遍历和删除对象

    示例如下: // Aa.cpp : Defines the entry point for the console application. #include "stdafx.h" #include <vector> #include <map> #include <iostream> using namespace std; int main(int argc, char* argv[]) { printf("Hello World!\n

  • 详解C++编程中的vector类容器用法

    vector简介 vector是STL中最常见的容器,它是一种顺序容器,支持随机访问.vector是一块连续分配的内存,从数据安排的角度来讲,和数组极其相似,不同的地方就是:数组是静态分配空间,一旦分配了空间的大小,就不可再改变了:而vector是动态分配空间,随着元素的不断插入,它会按照自身的一套机制不断扩充自身的容量. vector的扩充机制:按照容器现在容量的一倍进行增长.vector容器分配的是一块连续的内存空间,每次容器的增长,并不是在原有连续的内存空间后再进行简单的叠加,而是重新申请

  • C++中vector和map的删除方法(推荐)

    1.连续内存序列容器(vector,string,deque) 序列容器的erase方法返回值是指向紧接在被删除元素之后的元素的有效迭代器,可以根据这个返回值来安全删除元素. vector<int> c; for(vector<int>::iterator it = c.begin(); it != c.end();) { if(need_delete()) it = c.erase(it); else ++it; } 2.关联容器(set,multiset,map,multima

  • 关于vector迭代器失效的几种情况总结

    在泛型编程还是STL的实际运用中,迭代器(iterator)无疑扮演者重要的角色.迭代器是一种类似于指针的对象(如可以内容提领,成员访问等),但他又不仅仅是一种普通的指针. 关于迭代器失效,我们可以看下面这个例子: #include<vector> #include<list> void PrintVector(const vector<int>& v) { vector<int>::const_iterator it = v.begin(); wh

  • C++从文本文件读取数据到vector中的方法

    前言 大家应该都只奥vector(向量)是 C++中的一种数据结构,确切的说是一个类.它相当于一个动态的数组,当程序员无法知道自己需要的数组的规模多大时,用其来解决问题可以达到最大节约空间的目的.这篇文章介绍的是C++从文本文件读取数据到vector中的方法,下面话不多说,直接来看示例代码吧. 如题,要将如下文本文件读进vector中 示例代码如下 #include <iostream> using namespace std; #include <cmath> #include

  • C++实现 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>&

  • Java中Vector与ArrayList的区别详解

    首先看这两类都实现List接口,而List接口一共有三个实现类,分别是ArrayList.Vector和LinkedList.List用于存放多个元素,能够维护元素的次序,并且允许元素的重复.3个具体实现类的相关区别如下:1.ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问.数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中.当从ArrayList的中间位置插入或者删除元素时,需要对数组

  • C#实现的简单整数四则运算计算器功能示例

    本文实例讲述了C#实现的简单整数四则运算计算器功能.分享给大家供大家参考,具体如下: 运行效果图如下: 具体代码如下: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace 计算器 { public pa

  • C++ 中Vector常用基本操作

    标准库vector类型是C++中使用较多的一种类模板,vector类型相当于一种动态的容器,在vector中主要有一些基本的操作,下面通过本文给大家介绍,具体内容如下所示: (1)头文件#include<vector>. (2)创建vector对象,vector<int> vec; (3)尾部插入数字:vec.push_back(a); (4)使用下标访问元素,cout<<vec[0]<<endl;记住下标是从0开始的. (5)使用迭代器访问元素. vect

  • C++中vector容器的用法

    在c++中,vector是一个十分有用的容器,下面对这个容器做一下总结. 1 基本操作 (1)头文件#include<vector>. (2)创建vector对象,vector<int> vec; (3)尾部插入数字:vec.push_back(a); (4)使用下标访问元素,cout<<vec[0]<<endl;记住下标是从0开始的. (5)使用迭代器访问元素. vector<int>::iterator it; for(it=vec.begi

  • 详解Java中的Vector

    Vector实现了AbstractList抽象类和List接口,和ArrayList一样是基于Array存储的 Vector 是线程安全的,在大多数方法上存在synchronized关键字 //Vector存放的元素,初始化默认长度为10 protected Object[] elementData; //元素个数 protected int elementCount; //每次扩容大小,默认为0 protected int capacityIncrement; //构造函数,无指定初始化大小和

  • Java 中的vector和list的区别和使用实例详解

    要了解vector,list,deque.我们先来了解一下STL. STL是Standard Template Library的简称,中文名是标准模板库.从根本上说,STL是一些容器和算法的集合.STL可分为容器(containers).迭代器(iterators).空间配置器(allocator).配接器(adapters).算法(algorithms).仿函数(functors)六个部分.指针被封装成迭代器,这里vector,list就是所谓的容器. 我们常常在实现链表,栈,队列或者数组时,

  • 迅速掌握Java容器中常用的ArrayList类与Vector类用法

    ArrayList类 List集合的实例化: List<String> l = new ArrayList<String>(); //使用ArrayList类实例化List集合 List<String> l2 = new LinkedList<String>(); //使用LinkedList类实例化List集合 ArrayList常用方法: add(int index, Object obj); addAll(int, Collection coll);

  • JAVA Vector源码解析和示例代码

    第1部分 Vector介绍Vector 是矢量队列,它是JDK1.0版本添加的类.继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口.Vector 继承了AbstractList,实现了List:所以,它是一个队列,支持相关的添加.删除.修改.遍历等功能.Vector 实现了RandmoAccess接口,即提供了随机访问功能.RandmoAccess是java中用来被List实现,为List提供快速访问功能的.在Vector中,我们即可以通过

  • 浅谈 java中ArrayList、Vector、LinkedList的区别联系

    以前面试的时候经常会碰到这样的问题.,叫你写一下ArrayList.LinkedList.Vector三者之间的区别与联系:原先一直搞不明白,不知道这三者之间到底有什么区别?哎,惭愧,基础太差啊,木有办法啊委屈 现在得去说说这三者之间的区别与联系了:这三者都是实现了List接口,都拥有List接口里面定义的方法,并且同时拥有Collection接口的方法: ArrayList:采用的是数组的方式进行存储数据的,查询和修改速度快,但是增加和删除速度慢:线程是不同步 LinkedList:采用的是链

随机推荐