副标题#e#
1 引言
我相信各人很相识,建设、复制和销毁姑且工具是C++编译器最爱的户内举动。不幸的是,这些行为会低落C++措施的机能。确实,姑且工具凡是被视为C++措施低效的第一因素[1]。
下面的代码是正确的:
vector < string > ReadFile();
vector < string > vec = ReadFile();
可能
string s1, s2, s3;
//...
s1 = s2 + s3;
可是,假如体贴效率,则需要限制雷同代码的利用。ReadFile()和operator+建设的姑且工具别离被复制然后再废弃。这是一种挥霍!
为了办理这个问题,需要一些不太优雅的约定。譬喻,可以凭据引用通报函数参数:
void ReadFile(vector < string > & dest);
vector < string > dest;
ReadFile(dest);
这相合时人讨厌。更糟的是,运算符没有这个选择,所以假如想高效的处理惩罚大工具,措施员必需限制建设姑且工具的运算符的利用:
string s1, s2, s3;
//...
s1 = s2;
s1 += s3;
这种难缠的手法凡是减缓了设计大措施的大团队的事情效率,这种强加的一连不绝的烦恼抹杀了编写代码的兴趣并且增加了代码数量。莫非从函数返回值,利用运算符通报姑且工具,这样做是错误的吗?
一个正式的基于语言的办理方案的提议已经递交给了尺度化委员会[2]。Usenet上早已激发了大接头,本文也因此在个中被重复接头过了。
本文展示了如何办理C++存在的不须要的复制问题的要领。没有百分之百让人满足地办理方案,可是一个清洁的水平是可以到达的。让我们一步一步的来建设一个强有力的框架,来辅佐我们从措施中消除不需要的姑且工具的复制。这个办理方案不是百分之百透明的,可是它消除了所有的不需要的复制,并且封装后足以提供一个靠得住的替代品,直到多年今后,一个清洁的、基于语言的尺度化的实现呈现。
#p#副标题#e#
2 姑且工具和“转移结构函数”(Move Constructor)
在和姑且工具斗争了一段时间之后,我们意识到在大大都环境下,完全消除姑且工具是不切实际的。大大都时候,要害是消除姑且工具的复制而不是姑且工具自己。下面具体的接头一下这个问题。
大大都具有昂贵的复制开销的数据布局将它们的数据以指针可能句柄的形式储存。典范的例子包罗,字符串(String)范例储存巨细(size)和字符指针(char*),矩阵(Matrix)范例储存一组整数维数和数据存储区指针(double*),文件(File)范例储存一个文件句柄(handle)。
如你所见,复制字符串、矩阵可能文件的开销不是来自于复制实际的数据成员,而是来自于指针可能句柄指向的数据的复制。
因此,对付消除复制的目标来说,检测姑且工具是一个好要领。残忍点说就是,既然一个工具死定了,我们完全可以趁着它还新鲜,把它用作器官捐募者。
顺便说一下什么是姑且工具?这里给出一个非正式的界说:
当且仅当分开一段上下文(context)时在工具上执行的仅有的操纵是析构函数时,一个工具被当作是姑且的。这里上下文大概是一个表达式,也大概是一个语句范畴,譬喻函数体。
C++尺度没有界说姑且工具,可是它假定姑且工具是匿名的,譬喻函数的返回值。凭据我们的更一般化的界说,在函数中界说的定名的栈分派的变量也是姑且的。稍后为了便于接头我们利用这个一般化的界说。
思量这个String类的实现(仅作为示例):
class String
{
char* data_;
size_t length_;
public:
~String()
{
delete[] data_;
}
String(const String& rhs)
: data_(new char[rhs.length_]), length_(rhs.length_)
{
std::copy(rhs.data_, rhs.data_ + length_, data_);
}
String& operator=(const String&);
//...
};
这里复制的本钱主要由data_的复制构成,也就是分派新的内存并复制。假如可以探测到rhs实际上是姑且的就好了。思量下面的C++伪代码:
class String
{
//...同前...
String(temporary String& rhs)
: data_(rhs.data_), length_(rhs.length_)
{
//复位源字符串使它可以被销毁
//因为姑且工具的析构函数仍然要执行
rhs.data_ =0;
}
//...
}
这个我们虚构的重载结构函数String(temporary String&)在建设一个String姑且工具(凭据前面的界说)时挪用。然后,这个结构函数执行了一个rhs工具转移的结构进程,只是简朴的复制指针而不是复制指针指向的内存块。最后,“转移结构函数”复位源指针rhs.data_(规复为空指针)。利用这个要领,当姑且工具被销毁时,delete[]会无害的应用在空指针上[译注:C++担保删除空指针是安详的]。
#p#分页标题#e#
一个重要的细节是“转移结构”后rhs.length_没有被清0。凭据教条主义的概念,这是不正确的,因为data_==0而length_!=0,所以字符串被粉碎了。可是,这里有一个很站得住脚的来由,因为rhs的状态没有须要是完整的,只要它可以被安详而正确的销毁就行了。这是因为会被应用在rhs上独一一个操纵就是析构函数,而不是其他的。所以只要rhs可以被安详的销毁,而不消去看是否像一个正当的字符串。
“转移结构函数”对付消除不需要的姑且工具复制是一个精采的办理方案。我们只有一个小问题,C++语言中没有temporary要害字。
还应该留意到姑且工具的探测不会辅佐所有的类。有时,所有的数据直接存储在容器中。思量:
class FixedMatrix
{
double data_[256][256];
public:
//...操纵...
};
对这样一个类,实际上复制本钱在于逐字节的复制sizeof(FixedMatrix)个字节,而探测姑且工具并没有辅佐[译注:因为数组不是指针,不能直接互换地点]。
3 已往的办理方案
不须要的复制是C++社区恒久存在的问题。有两个尽力偏向齐头并进,其一是从编码和库编写的角度,另一个是语言界说和编译器编写层面。
语言/编译器概念方面,有返回值优化(Return Value Optimization, RVO)。RVO被C++语言界说所答允[3][译注:可是不是强制性的,而是实现界说的]。根基上,编译器假定通过拷贝结构函数(Copy Constructor)复制返回值。
确切地说,基于这样的假定,因此编译器可以消除不须要的复制。譬喻,思量:
vector< String > ReadFile()
{
vector< String > result;
//...填充result...
return result;
}
vector< String > vec=ReadFile();
智慧的编译器可以将vec的地点作为一个埋没的参数通报给ReadFile而把result建设在谁人地点上。所以上面的源代码生成的代码看起来像这样:
void ReadFile(void* __dest)
{
//利用placement new在dest地点建设vector
vector< String >& result=
*new(__dest) vector< String >;
//...填充result...
}
//假设有符合的字节对齐
char __buf[sizeof(vector< String >)];
ReadFile(__buf);
vector< String >& vec=
*reinterpret_cast < vector< String >* >(__buf);
RVO有差异的气势气魄,但要旨是沟通的:编译器消除了一次拷贝结构函数的挪用,通过简朴的在最终目标地上结构函数返回值。
不幸的是,RVO的实现不像看上那样容易。思量ReadFile稍稍修改后的版本:
vector< String > ReadFile()
{
if (error) return vector< String >();
if (anotherError)
{
vector< String > dumb;
dumb.push_back("This file is in error.");
return dumb;
}
vector< String > result;
//...填充result...
return result;
}
******************************************************
Wang Tianxing校注:
这个例子并不是很有说服力。内里的三个工具的浸染域互不相交,因此照旧较量容易利用 RVO 的。难以运用RVO的是这种环境:
vector< String > ReadFile()
{
vector< String > dumb;
dumb.push_back( "This file is in error." );
vector< String > result;
// ... 填充 result ...
return error ? dumb : result;
}
******************************************************
此刻有不止一个局部变量需要被映射到最后的功效上,他们有好几个。有些是定名的(dumb/result),而另一些是无名的姑且工具。无需多说,面临这样的排场,大量优化器会投降而且听从守旧的和缺乏效率的要领。
纵然想写不导致夹杂RVO实现的“直线条”的代码,也会因为听到每个编译器可能编译器版本都有本身探测和应用RVO的法则而失望。一些RVO应用仅仅针对返回无名姑且工具的函数,这是最简朴的RVO形式。最巨大的RVO应用之一是函数返回值是一个定名的功效,叫做定名返回值优化(Named RVO或NRVO)。
#p#分页标题#e#
本质上,写措施时要指望可移植的RVO,就要依赖于你的代码的准确写法(在很难界说的“准确”意义下),依赖于月亮的圆缺,依赖于你的鞋的尺码。
可是,别忙,尚有许多种环境下RVO无法制止姑且工具的拷贝。编译器时常不能应用RVO,纵然它很想。思量稍稍改变后的 ReadFile() 的挪用:
vector vec;
vec=ReadFile();
这个改变看上去完全没有恶意,可是却导致了庞大的差别。此刻不再挪用拷贝结构函数而挪用赋值运算符(assignment operator),这是令一个差异的脱缰野马。除非编译器优化能力完全像是在利用邪术,此刻真的可以和RVO吻别了:vector<T>::operator=(const vector<T>&)期望一个vector的常量引用,所以ReadFile会返回一个姑且工具,绑定到一个常量引用,复制到vec,然后被废弃。不须要的姑且工具又来了!
在编码方面,一个恒久被推荐的技能是COW(按需复制,copy-on-write)[4],这是一个基于引用计数的能力。
COW有几个利益,个中之一是探测和消除了不须要的复制。譬喻,函数返回时,返回的工具的引用计数是1。然后复制的时候,引用计数增加到2。最后,销毁姑且工具的时候,引用计数回到1,引用指向的目标地仅仅是数据的所有者。实际上没有复制行动产生。
不幸的是,引用计数在多线程安详性方面有大量的缺陷,增加本身的开销和大量埋没的陷阱[4]。COW是如此之鸠拙,因此,固然它有许多利益,最近的STL实现都没有为std::string利用引用计数,尽量实际上std::string的接口有目标设计为支持引用计数!
已经开拓了几个实现“不行复制”工具的步伐,auto_ptr是最精辟的一个。auto_ptr是容易正确利用的,可是不幸的是,恰好也容易不正确的利用。本文的接头的办理要领扩充了界说auto_ptr中利用的技能。
4 Mojo
Mojo(连系工具转移,Move of Joint Objects)是一项编码技能,又是一个消除不须要的姑且工具复制的小框架。Mojo通过度辨姑且工具和正当的“非姑且”的工具而得以事情。
4.1 通报函数参数
Mojo激发了一个有趣的阐明,即函数参数通报约定的观测。Mojo之前的一般发起是:
[法则1]假如函数试图改变参数(也就是作为副浸染),则把参数作为很是量工具的指针可能引用通报。譬喻:void Transmogrify(Widget& toChange);
void Increment(int* pToBump);
double Cube(double value);
String& String::operator=(const String& rhs);
template< class T > vector< T >::push_back(const T&);
第三条法则试图制止意外的大工具的复制。然而,有时第三条法则强制不须要的复制举办而不是阻止它的产生。思量下面的Connect函数:
void Canonicalize(String& url);
void ResolveRedirections(String& url);
void Connect(const String& url)
{
String finalUrl=url;
Canonicalize(finalUrl);
ResolveRedirections(finalUrl);
//...利用finalUrl...
}
Connect函数得到一个常量引用的参数,并快速的建设一个副本。然后进一步处理惩罚副本。
这个函数展示了一个影响效率的常量引用的参数利用。Connect的函数声明体现了:“我不需要一个副本,一个常量引用就足够了”,而函数体实际上却建设了一个副本。所以如果此刻这样写:
String MakeUrl();
//...
Connect(MakeUrl());
可以预料MakeUrl()会返回一个姑且工具,他将被复制然后销毁,也就是令人害怕的不需要的复制模式。对一个优化复制的编译器来说,不得不作很是坚苦的事情,其一是会见Connect函数的界说(这对付疏散编译模块来说很坚苦),其二是理会Connect函数的界说并进一步领略它,其三是改变Connect函数的行为以使姑且工具和finalUrl融合。
如果此刻将Connect函数改写如下:
void Connect(String url) //留意按值通报
{
Canonicalize(url);
ResolveRedirections(url);
//... 利用 url ...
}
从Connect的挪用者的概念来看,绝对没有什么区别:固然改变了语法接口,可是语义接口仍然是沟通的。对编译器来说,语法的改变使所有事物都产生了改变。此刻编译器有更多的余地体贴url姑且工具了。譬喻,在上面提到的例子中:
Connect(MakeUrl());
#p#分页标题#e#
编译器不必然要真的智慧到将MakeUrl返回的姑且工具和Connect函数需要的常量融合。假如那么做,确实会越发坚苦。最终,MakeUrl的真正功效会被改变并且在Connect函数中利用。利用常量引用参数的版本会使编译器窒息,阻止它实行任何优化,而利用传值参数的版本和编译器顺畅的相助。
这个新版本的倒霉之处在于,此刻挪用Connect也许生成了更多的呆板码。思量:
String someUrl=...;
Connect(someUrl);
在这种环境下,第一个版本简朴的通报someUrl的引用[译注:从很是量到常量是尺度转型]。第二个版本会建设一个someUrl的副本,挪用Connect,然后销毁谁人副本。跟着挪用Connect的静态数量的增长,代码巨细的开销同时增长。另一方面,譬喻Connect(MakeUrl())这样的挪用会引入姑且工具,在第二个版本中又恰好生成更少的代码。在大都环境下,巨细差别仿佛不会导致问题发生[译注:在某些小内存应用中则是一个问题,譬喻嵌入式应用情况]。
所以我们给出了一套差异的推荐法则:
[法则1]假如函数内部老是建造参数的副本,按值通报。 [法则2]假如函数从来不复制参数,按常量引用通报。 [法则3]假如函数有时复制参数,并且体贴效率,则凭据Mojo协议。此刻只留下开拓Mojo协议了,不管它是什么。
主要的想法是重载同样的函数(譬喻Connect),目标是分辨姑且的和非姑且的值。后者也称为左值(lvalue),因为汗青原因,左值因为可以呈此刻赋值运算符的左边而得名。
此刻开始重载Connect,第一个想法是界说Connect(const String&)来捕获常量工具。然而这是错误的,因为这个声明“吞吃”了所有的String工具,不管是左值(lvalue)可能姑且工具[译注:前面提到过,很是量可以隐式转型为常量,这是尺度转型行动]。所以第一个好主意是不要声明接管常量引用的参数,因为它像一个黑洞一样,吞噬所有的工具。
第二个实验是界说Connect(String&)试图捕捉很是量的左值。这事情精采,出格是常量值和无名的姑且工具不能被这个重载版本接管,这是一个好的起点。此刻我们只剩下在常量工具和很是量姑且工具之间作出区分了。
为了到达这个目标,我们采纳了一种技能,界说两个替身范例[译注:原文是type sugar,嘿嘿,假如你愿意,可以叫他范例砂糖,假如你喜欢吃糖的话。]ConstantString和TemporaryString,而且界说了从String工具到这些工具转型运算符:
class String;
//常量String的替身范例
struct ConstantString
{
const String* obj_;
};
//姑且String的替身范例
struct TemporaryString : public ConstantString {};
class String
{
public:
//...结构函数,析构函数,运算符,等等......
operator ConstantString() const
{
ConstantString result;
result.obj_ = this;
return result;
}
operator TemporaryString()
{
TemporaryString result;
result.obj_ = this;
return result;
}
};
此刻界说下面三个重载版本:
//绑定很是量姑且工具
void Connect(TemporaryString);
//绑定所有的常量工具(左值和姑且工具)
void Connect(ConstantString);
//绑定很是量左值
void Connect(String& str)
{
//挪用另一个重载版本
Connect(ConstantString(str));
}
常量String工具被Connect(ConstantString)接收。没有其他绑定可以事情,另两个仅仅被很是量String工具挪用。
姑且工具不能挪用Connect(String&)。然而它们可以挪用Connect(TemporaryString)可能Connect(ConstantString),前者一定被选中而不产生歧义。原因是因为TemporaryString从ConstantString派生而来,一个应该留意的企图。
思量一下ConstantString和TemporaryString都是独立的范例。那么,当要求复制一个姑且工具时,编译器将同等的看待operator TemporaryY()/Y(TemporarY)可能operator ConstantY() const/Y(ConstantY)。
为什么是同等的?因为就选择成员函数来说,很是量到常量转型是“无摩擦的”。
因而,需要汇报编译器更多的选择第一个而不是第二个。那就是担任在这里的浸染。此刻编译器说:“好吧,我猜我要颠末ConstantString可能TemporaryString…,可是等等,派生类TemporaryString是更好的匹配!”
这里的法则是从重载候选中选择函数时,匹配的派生类被视作比匹配的基类更好。
#p#分页标题#e#
我对上述代码稍作修改,从std::string派生了String,并在此基本上凭据Mojo的方法修改,功效在gcc3.2编译器下简直如作者指出的行为一般无二。这条重载的决策法则很少在C++书籍中提到,Wang Tianxing从烟波浩淼的尺度文本中找出了这条法则:
13.3.3.2 Ranking implicit conversion sequences [over.rank]
4 [...]
-- If class B is derived directly or indirectly
from class A and class C is derived directly
or indirectly from B,
[...]
-- binding of an expression of type C to a
object of type B is better than binding
an expression of type C to a object
of object A,
上面这些尺度中的条款,是从隐式转型的转换品级中节选出来的,大抵的意思是说,假如C担任B,而B担任A,那么范例为C的表达式绑定到B的工具比到A的工具更好,这是上面论述的技能的尺度依据。另外,雷同的引用和指针的绑定也合用于此法则,这里省略了这些条款。
最后一个有趣的格式是,担任不需要必需是public的。存取法则和重载法则是不斗嘴的。
让我们看看Connect如何事情的例子:
String s1("http://moderncppdesign.com");
如你所见,我们到达了期望的主要方针:在姑且工具和所有其他工具之间制造了不同。这就是Mojo的要旨。
// 挪用Connect(String&)
Connect(s1);
// 挪用operator TemporaryString()
// 接下来挪用Connect(TemporaryString)
Conncet(String("http://moderncppdesign.com"));
const String s4("http://moderncppdesign.com");
// 挪用operator ConstantString() const
// 接下来挪用Connect(ConstantString)
Connect(s4);
尚有一些不太显眼的问题,大大都我们要一一办理。
首先是淘汰代码反复:Connect(String&)和Connect(ConstantString)根基上作沟通的工作。上面的代码通过第一个重载函数挪用第二个重载函数办理了这个问题。
让我们面临第二个问题,为每个需要mojo的范例写两个小类听上去不是很吸引人,所以让我们开始建造一些更具一般性的对象更便于利用。我们界说了一个mojo名字空间,并放入两个泛型的Constant和Temporary类:
namespace mojo
{
template < class T >
class constant
{
const T* data_;
public:
explicit constant(const T& obj) : data_(&obj)
{
}
const T& get() const
{
return *data_;
}
};
template < class T >
class temporary : private constant< T >
{
public:
explicit temporary(T& obj) : contant< T >( obj)
{
}
T& get() const
{
return const_cast< T& >(constant< T >::get());
}
};
}
让我们再界说一个基类mojo::enabled,它包罗了两个运算符:
template < class T > struct enabled //在mojo名字空间中
{
operator temporary< T >()
{
return temporary< T >(static_cast< T& >(*this));
}
operator constant< T >() const
{
return constant< T >(static_cast< const T& >(*this));
}
protected:
enabled() {} //只能被派生
~enabled() {} //只能被派生
};
利用这个“脚手架”,将一个类“mojo化”的任务可以想象会变得更简朴:
class String : public mojo::enabled< String >
{
//...结构函数,析构函数,运算符,等等...
public:
String(mojo::temporary< String > tmp)
{
String& rhs = tmp.get();
//...执行rhs到*this的析构性复制...
}
};
这就是通报函数参数的Mojo协议。
凡是,一切事情精采,你获得了一个好的设计品。不错,那些意外的环境都节制在一个很小的范畴内,这使他们更有代价。
用Mojo设计我们可以很容易检测到一个类是否支持Mojo。只需要简朴的写:
namespace mojo
{
template < class T >
struct traits
{
enum
{
enabled = Loki::SuperSubclassStrict< enabled< T >, T >::value
};
};
};
Loki提供了探测一个范例是否从另一个类派生的机制。[5]
#p#分页标题#e#
此刻可以发明一个任意的范例X是凭据Mojo协议设计的,只要通过mojo::traits<X>::enabled即可确定。这个检测机制对泛型编程是很重要的,很快我们就会看到它的浸染。
4.2 函数返回值优化
此刻我们可以正确的通报参数,让我们看看如何将Mojo扩展到函数返回值优化。这次的目标又是具有可移植性的效率改进,即100%的消除不需要的复制而不依赖于特定的返回值优化(RVO)实现。
让我们先看看凡是的发起怎么说。出于盛情,一些作者也推荐返回值的利用法则[7]:
[法则4]当函数返回用户界说的工具的值的时候,返回一个常量值。譬喻:const String operator+(const String& lhs,const String& rhs);
法则4的潜台词是利用户界说的运算符越发靠近于内建的运算符可以克制错误的表达式的成果,就仿佛想是if (s1+s2==s3)的时候笔误成了if (s1+s2=s3)。假如operator+返回一个常量值,这个特定的BUG将会在编译期间被检测到[译注:返回内建数据范例的值隐含地老是常量的,而用户界说范例则需要显式的用常量限定符指出]。然而,其他的作者[6]推荐不要返回常量值。
沉着的看,任何返回值都是短暂的,它是方才被建设就要很快消失的短命鬼。那么,为什么要强迫运算符的利用者得到一个常量值呢?从这个概念看,常量的姑且工具看上去就象是自相抵牾的,既是稳定的,又是姑且的。从实践的概念看,常量工具强迫复制。
此刻假定我们同意,假如效率是重要的,最好是制止返回值是常量,那么我们如何使编译器确信将函数的功效转移到目标地,而不是复制他呢?
当复制一个范例为T的工具时,拷贝结构函数被挪用。凭据下面的配置,我们恰好可以提供这样一个拷贝结构函数实现这个方针。
class String : public mojo :: enabled < string >
{
//...
public:
String( String& );
String( mojo :: temporary < String > );
String( mojo :: constant < String > );
};
这是一个很好的设计,除了一个小细节–它不能事情。
因为拷贝结构函数和其他的函数不完全沟通,出格是,对一个范例X来说,在需要X(const X&)的处所界说X(X&),下面的代码将无法事情:
void FunctionTakingX(const X&);
FunctionTakingX(X()); // 错误!不能发明X(const X&)
Wang Tianxing在gcc3.2, bcc5.5.1, icl7.0情况下测试功效表白都不会产生错误,并进而查阅了尺度,发明Andrei是正确的,假如必然说要有什么错误的话,他没有指出这是实现界说的。
8.5.3 References
5 [...]
— If the initializer expression is an rvalue, with T2 a class type,
and “cv1 T1” is reference-compatible with “cv2 T2,” the reference
is bound in one of the following ways (the choice is implementation-
defined):
— The reference is bound to the object represented by the rvalue
(see 3.10) or to a sub-object within that object.
— A temporary of type “cv1 T2” [sic] is created, and a
constructor is called to copy the entire rvalue object into the
temporary. The reference is bound to the temporary or to a
sub-object within the temporary.93)
The constructor that would be used to make the copy shall be
callable whether or not the copy is actually done.
93) Clearly, if the reference initialization being processed is one
for the first argument of a copy constructor call, an implementation
must eventually choose the first alternative (binding without
copying) to avoid infinite recursion.
我引用了这段尺度文本,有乐趣的读者可以自行研究它的寄义。
这严重的限制了X,所以我们被迫实现String(const String&)结构函数。此刻假如你答允我引用本文的话,在前面我曾经说过:“所以第一个好主意是不要声明一个函数接管常量引用,因为它像一个黑洞一样吞噬所有的工具。”
鱼与熊掌不行兼得,不是吗?
很清楚,拷贝结构函数需要出格的处理惩罚。这里的想法是建设一个新的范例fnresult,那就是为String工具提供一个“转移器(mover)”。下面是需要执行的步调:
前面返回范例为T的值的函数此刻将返回fnresult<T>。为了使这个变革对对换用者透明,fnresult必需可以被隐式的转型为T。
然后为fnresult成立转移语义:无论何时一个fnresult<T>工具被复制,内里包括的T被转移。
雷同运算符的常量性和姑且性,在mojo::enabled类中为fnresult提供一个转型运算符。
一个mojo化的类(如前例中的String)界说了一个结构函数String( mojo :: fnresult < String > )完成转移。
这个fnresult的界说看起来就像:
#p#分页标题#e#
namespace mojo
{
template < class T >
class fnresult : public T
{
public:
fnresult ( const fnresult& rhs )
: T ( temporary < T > ( const_cast < fnresult& > ( rhs ) ) )
{
}
explicit fnresult ( T& rhs ) : T ( temporary < T > ( rhs ) )
{
}
};
}
因为fnresult<T>从T担任而来,第一步值得留意,即fnresult<T>转型为T,然后第二个值得留意的就是复制fnresult<T>工具的时候,隐含着它的T子工具(subobject)强制转型为temporary<T>。
正如前面提到的,我们增加一个转型答允返回一个fnresult,最后的版本看起来是这样的:
template < class T > struct enabled
{
operator temporary < T > ( )
{
return temporary < T > ( static_cast < T& > ( *this ) );
}
operator constant < T > ( ) const
{
return constant < T > ( static_cast < const T& > ( *this ) );
}
operator fnresult < T > ( )
{
return fnresult < T > ( static_cast < T& > ( *this ) );
}
protected:
enabled ( ) { } // intended to be derived from
~enabled ( ) { } // intended to be derived from
};
最后是String的界说:
class String : public mojo :: enabled < String >
{
//...
public:
// COPY rhs
String ( const String& rhs );
// MOVE tmp.get() into *this
String ( mojo :: temporary < String > tmp );
// MOVE res into *this
String ( mojo :: fnresult < String > res );
};
此刻思量下面的函数:
mojo :: fnresult < String > MakeString()
{
String result;
//?..
return result;
}
//...
String dest(MakeString());
在MakeString的return语句和dest的界说之间的路径是:
result -> String :: operator fnresult < String > () -> fnresult < String > (const fnresult < String >& ) -> String :: String ( fnresult < String > )
利用RVO的编译器可以消除挪用链中fnresult<String>(const fnresult<String>&)的挪用。然而,更重要的是没有函数执行真正的复制,它们都被界说为功效的实际内容滑腻的转移到dest。也就是说没有涉及内存分派和复制。
此刻,正如所见,有两个,最多三个转移操纵。虽然,在必然条件和必然范例的环境下,一次复制比三次转移大概更好。尚有一个重要的区别,复制也许会失败(抛出异常),而转移永远不会失败。
5 扩展
好的,我们使Mojo事情了,并且对付单独的类相当好。此刻奈何将Mojo扩展到组合工具,它们也许包括大量其他的工具,并且他们中的一些已经是mojo化的。
这个任务就是将转移结构函数从类通报到成员。思量下面的例子,内嵌类String在类Widget中:
class Widget : public mojo::enabled < Widget >
{
String name_;
public:
Widget(mojo::temporary< Widget > src) // source is a temporary
: name_(mojo::as_temporary(src.get().name_))
{
Widget& rhs = src.get();
//... use rhs to perform a destructive copy ...
}
Widget(mojo::constant< Widget > src) // source is a const
: name_(src.get().name_) // 译注:这里原文name_(src.name_)显然有误
{
Widget& rhs = src;
//... use rhs to perform a destructive copy ...
}
};
在转移结构函数中的name_的初始化利用了一个重要的Mojo帮助函数:
namespace mojo
{
template < class T >
struct traits
{
enum { enabled =
Loki::SuperSubclassStrict< enabled< T >, T >::value };
typedef typename
Loki::Select< enabled,temporary< T >,T& >::Result temporary;
};
template < class T >
inline typename traits< T >::temporary as_temporary(T& src)
{
typedef typename traits< T >::temporary temp;
return temp(src);
}
}
as_temporary做的所有工作就是按照一个左值建设一个姑且工具。利用这个要领,类成员的转移结构函数被方针工具所挪用。
#p#分页标题#e#
假如String是mojo化的,Widget获得他的利益;假如不是,一个直接的复制被执行。换句话说,假如String是mojo::enabled<String>的一个派生类,那么as_temporary返回一个mojo::temporary<String>。不然,as_temproary(String& src)是一个简朴的函数,带一个String&的参数并返回同样的String&。
6 应用:auto_ptr的亲戚和mojo化的容器
思量一个mojo_ptr类,它通过使拷贝结构函数私有而克制它们:
class mojo_ptr : public mojo::enable< mojo_ptr >
{
mojo_ptr(const mojo_ptr&); // const sources are NOT accepted
public:
// source is a temporary
mojo_ptr(mojo::temporary< mojo_ptr > src)
{
mojo_ptr& rhs = src.get();
//... use rhs to perform a destructive copy ...
}
// source is a function's result
mojo_ptr(mojo::fnresult< mojo_ptr > src)
{
mojo_ptr& rhs = src.get();
//... use rhs to perform a destructive copy ...
}
//..
};
这个类有一个有趣的行为。你不能复制这个类的常量工具。你也不能复制这个类的左值。可是你可以复制这个类的姑且工具(利用转移语义),并且你可以显式的移动一个工具到别的的工具:
mojo_ptr ptr1;
mojo_ptr ptr2 = mojo::as_temporary(ptr1);
这自己并没有什么大不了的,假如 auto_ptr 里让 auto_ptr(auto_ptr&)私有,也可以做到这一点。有趣的处所不是mojo_ptr自己,而是如何利用as_temporary。你可以成立高效的容器,储存“经典”的范例、一般的mojo化的范例以及和mojo_ptr雷同的范例。所有这样的一个容器当他需要转移元素时,必需利用as_temporary。对付“经典”范例,as_temporary是一个什么都不做的等效函数,对付mojo_ptr,as_temporary是一个提供滑腻转移机制的函数书。move()以及uninitialized_move()的函数模板(拜见所附代码,译注:代码请到原版链接处寻找)也唾手可得。
利用尺度术语,mojo_ptr既不是可以复制的,也不是可以赋值的。然而,mojo_ptr可以看作是一种新范例,叫做“可转移的”。这是一个重要的新的分类,也许可以用于锁(lock)、文件(file)和其他的不行复制的句柄(handle)。
假如你曾经但愿一个拥有元素的雷同于 vector< auto_ptr<Widget> > 的容器,并且有安详、清楚的语义,此刻你获得了,并且尚有其他成果。别的,当包括一个拷贝昂贵的范例时,如vector< vector<string> >,mojo化的vector“更能适应元素个数增减的需要”。
7 结论
mojo是一种技能,也是一个紧凑的小框架,用于消除不须要的姑且工具的复制。mojo的事情方法是检测姑且工具而且通过函数重载哄骗他们而不是简朴的作为左值。这样做的功效是,得到姑且工具的函数执行一个粉碎性的复制,只要确信其他代码不再利用这个姑且工具即可。
假如客户代码凭据一套简朴的法则通报函数参数和返回值,可以应用mojo。
mojo界说了一个单独的机制来消除函数返回时的复制。
特另外机制和范例转换使mojo对付客户代码不是100%的透明,然而对付基于库的办理方案来说集成度是相当好的。说得好听一点,mojo将作为一个结实的替代品,直到一个更结实的、基于语言特性的被尺度化并实现。
8 叩谢
原文的叩谢略,译文获得了Wang Tianxing的热情辅佐,除了辅佐我审核了若干技能细节之外,还指出了不少打字错误,以及若干英语中的谚语。
9 参考文献
[1] Dov Bulka and David Mayhew. Efficient C++: Performance Programming Techniques, (Addison-Wesley, 1999). [2] Howard E. Hinnant, Peter Dimov, and Dave Abrahams. "A Proposal to Add Move Semantics Support to the C++ Language," ISO/IEC JTC1/SC22/WG21 — C++, document number N1377=02-0035, September 2002, <http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2002/n1377.htm>. [3] "Programming Languages — C++," International Standard ISO/IEC 14882, Section 12.2. [4] Herb Sutter. More Exceptional C++ (Addison-Wesley, 2002). [5] Andrei Alexandrescu. Modern C++ Design (Addison-Wesley, 2001). [6] John Lakos. Large-Scale C++ Software Design (Addison-Wesley, 1996), Section 9.1.9. [7] Herb Sutter. Exceptional C++ (Addison-Wesley, 2000).作者简介
Andrei Alexandrescu是一位华盛顿大学西雅图分校的博士生,广受赞誉的《Modern C++ Design》(中译本现代C++设计正在译制中)一书的作者。可以通过电子邮件andrei@metalanguage.com接洽。Andrei照旧一个C++课程的有招呼力的讲师。
译者的话
#p#分页标题#e#
作为第一次编译技能文章,并且选择的是C++中本身相比拟力生疏的主题,而且本文报告的内容是具有前瞻性的,而不是见诸于现有资料和文献的从头整理。因此在翻译进程中,有些细节译者本人也没有完全领略,因此不免呈现不少过错,接待各人来到newsfan的C++新闻组接头。