1. lazy evaluationg实际上是"拖延战术":延缓运算直到运算结果被需要为止.如果运算结果一直不被需要,运算也就不被执行,从而提高了效率.所谓的运算结果不被执行,有时指只有部分运算结果被需要,那么采用拖延战术,便可避免另一部分不被需要的运算,从而提高效率,以下是lazy evaluation的四种用途.
2. Reference Counting(引用计数)
如果要自己实现一个string类,那么对于以下代码:
String s1="Hello"; String s2=s1;
最直接的是采用eager evalutation(急式评估):为s1做一个副本并放入s2内,尽管此时s2的内容和s1并没有不同.
采用lazy evaluation的思想,可以先让s2分享s1的值,这样就省去了"调用new"以及"复制任何东西"的高昂成本.唯一要做的是一些簿记工作,以记录共享同一内容的各个对象.对s2的任何读操作,只需要s1的值即可,然而,一旦需要对s2的值进行写操作,就不能再做任何拖延,必须为s2做一份真实副本并进行写操作.
这种"数据共享"的观念就是lazy evaluation:在真正需要之前,不为对象构造副本.在某些应用领域,可能永远也不需要提供那样一份副本,从而提高效率.
3. 区分读和写
承接于2的策略,如果对自定义的string类进行以下操作:
String s1="Hello World!"; String s2=s1; cout<<s2[0]; cin>>s2[1];
那么两次对operator []的调用operator[]的实际行为实际上是不同的,前一个读操作只需要返回对应的引用即可,后一个写操作就需要先对s1做一个副本并放入s2中,然后再返回引用,也就是说,视operator[]用于读操作还是写操作,需要在operator内做不同事情,而要判断operator[]用于读操作还是写操作几乎是不可能的是,但如果利用lazy evaluation和条款30所描述的proxy classes,便可以延缓决定"究竟是读还是写",知道确定其答案.
4. Lazy Fetching(缓式取出)
对于程序需要使用内含许多字段的大型对象,比如:
class LargeObject{ public: LargeObject(ObjectID id); const string&field1()const; int field2()const; double field3()const; const string& field4()const; const string& field5()const; ... }
那么对于从磁盘中回复一个LargeObject对象,如果要取出此对象的所有字段,数据库相关操作成本可能极高,尤其是如果这些数据需要从远程数据库跨越网络而来.但如果只需要该对象的某个或某几个字段,那么读取所有数据的操作其实是不必要的.
采用Lazy evaluation的思想,在产生一个LargeObject对象时,可以只产生该对象的"外壳",而不从磁盘读取任何数据.只有当该对象的某个字段被需要时,才从数据库中取回对应的数据,以下做法可以实现这种"demand-page"式的对象初始化行为:
class LargeObject{ public: LargeObject(ObjectID id); const string&field1()const; int field2()const; double field3()const; const string& field4()const; const string& field5()const; ... private: ObjectID oid; mutable string *field1Vaule;//注意使用了mutable修饰符 mutable int *field2Value; mutable double *field3Value; mutable string *field4Value; ... } LargeObject::LargeObject(Object id):oid(id),field1Value(0),field2Value(0),field3Value(3)...{} const string& LargeObject::field1()const{ if(field1Value==0){ read the data from field 1 from the database and make field1Vaule point to it; } return *field1Value; }
对象内的每个字段都是指针,指向必要的数据,NULL初始值表示该字段未被读入,需要先从数据库读入.将字段指针声明为mutable,可以保证字段指针可以在任何时候都能被更改以指向实际数据,即使是在const成员函数内也一样.有些编译器厂商可能不支持mutable的使用,在const成员函数内可以采用const_cast甚至C转型操作移除this的常量特性并构造一个冒牌this并进行相关操作:
const string& LargeObject::field1()const{ LargeObject* const fakeThis=const_cast<LargeObject*const>(this); if(field1Value==0){ fakeThis->field1Value=the proper data from the database; } return *field1Value; }
5. Lazy Expression Evaluation(表达式缓评估)
对于以下代码:
template<class T> class Matrix{...} Matrix<int> m1(1000,1000); Matrix<int> m2(1000,1000); ... Matrix<int>m3=m1+m2;
对于operator+,通常的做法是eager ecaluation:计算并返回m1和m2的和,这是一个大规模运算,并需要大量内存分配成本.
采用lazy evaluation的思想,可以先设一个数据结构于m3中,用于标记m3是m1和m2的总和,这个数据结构可能只由两个指针和一个enum组成,前者指向m1和m2,后者用来表示运算动作.假设在m3被使用之前,程序又执行以下动作:
Matrix m4(1000,1000); ... m3=m4*m1;
那么便可直接将m3定位为m4和m1的乘积,之前没有用到的矩阵加法操作实际上并没有进行.当然,对m1和m2加和却没有用到的情况比较夸张,但不排除维护过程中程序员更改代码使得m1+m2不被用到的请况出现.
当然,lazy evaluation在此处还有更大用法——只计算大型运算中需要的部分运算结果.
对于以下代码:
cout<<m3[4];
此时不能再使用拖延战术,但也只需要计算m3第四行的值,除此以外,不需要计算其他任何值.实际上,正是这种策略使得APL(20世纪60年代的一款如软件,允许用户以交谈方式使用软件执行矩阵运算)能够快速处理加法,减法,乘法甚至除法.
当然,有时lazy evaluation并不能起作用,比如如下操作:
cout<<m3;
或者:
m3=m1+m2;
m1=m4;
这时就要采取某些措施以确保对m1的改变不会影响m3的值,可以在对m1进行改变之前先对m3求解,也可以将m1的旧值复制一份,然后令m3依从该值等,其他可能会修改矩阵值的情况,也要采取类似措施.
此外,由于存储数值间的相依关系,必须维护一些数据结构一存取数值,相依关系等,此外还必须将赋值,复制,加法等操作符进行重载,因此lazy evaluation用于数值运算领域也有许多工作要做,但与节省的时间和空间相比可能是微不足道的.
6. "Lazy evaluation在许多领域中都可能有用途:可避免非必要的对象复制,可区别operator[]的读取和写操作,可避免非必要的数据库读取动作,可避免非必要的数值计算动作."但其提升效率的前提是(部分)计算可能可以被避免,否则,在计算绝对必要的情况下,使用lazy evaluation不仅不能提升效率,还需要付出为lazy evaluation而设计的额外的数据结构等代价.
7. "Lazy evaluation并非C++的专属技能.这项技术可以用任何一种程序语言完成,有数种语言——APL,某些Lisp版本,以及几乎所有的数据流(dataflow)语言——已接受这个观念成为语言的一个基础部分."不过由于C++对封装性质的支持使得将lazy evaluation加入某个类内而对客户隐藏具体实现成为可能.