C++中函数返回大型对象是效率的大敌。但是正如 EffectiveC++ 条款23 [ EC 23 ] 指出的, 有时候你一定得按值返回对象。譬如,你有一个稀疏矩阵类,它包含:
class Matrix{
std::vector< Elem > _data;
size_t _row, _col;
};
现在他的矩阵乘法函数就必须返回一个新的 SparseMatrix:
const Matrix operator* ( ) ( const Matrix& rhs ) {
Matrix mat( _row, rhs._col ); // MAT_1
//...
return mat; // MAT2 = MAT1
}
mat_l = mat_a * mat_b; // mat_l = MAT2
数数 return 处执行了多少次拷贝/赋值么? 最悲惨的情况是 —— 我们竟然需要两次无意义的拷贝!
MAT_1 用于完成运算,并在返回时构造 MAT_2 ,并马上析构。 MAT_2 赋值给mat_l,同样马上析构。 其中,这个可能包含数百上千元素的 vector 成员将被逐字拷贝两次(可能还包括大量的动态内存分配等等)。这的确是无谓的开销阿,而如果设计允许的话,我们完全可以避免他!
看看下面这个函数声明,它把需要返回的对象以引用out 传入, 这就完全避免了 return 拷贝行为:
void mul( const Matrix& lhs, const Matrix& rhs, Matrix& out );
按值返回的恶劣开销真的不可避免么? 幸运的是,C++ 编译器能够做一个小优化,它有可能直接在返回位置构造我们的 MAT1, 函数内部的临时对象就消除了一个 —— 这称为 RVO( Return Value Optimization 对按值返回的优化 ) 。
下面是一个很容易进行RVO的例子:
A f1(...){
//....
return A( args );
}
在VC和 gcc上,上面的代码都进行了返回值优化: A将被直接在栈上返回位置处构建(而不是建立一个A再拷贝到返回处)。这是每个C++程序员都应该知道的优化办法( EC 条款20),但是很多情况下我们必须先构造A,计算结束才返回:
A f2(...){
A a;
//...
return a;
}
这种方式也可以RVO的,但是技术很复杂。f2调用之时,栈顶上放着实参,而如果要直接在返回处构造 a ,则很可能需要把原有内容先搬走( 不过同样可能,编译器把所有对象位置错开)。经测试对于这样的例子, VC7.1 在任何优化级别下都不会进行 RVO; 而 GCC 3.4.2 在开启优化的情况下则进行了 RVO。我不明白VC为什么不这么优化,它已经把所有对象错开了,也许是某处还有其他相关选项?
最后,当可能返回不同具名对象时,大多数编译器不会去预测到底哪个最可能返回。 比如 GCC 3.4.2将放弃对下面的代码进行 RVO的尝试:
A f3(...){
A a;
A b;
//...
if( condition ) return a;
else return b;
}
当然,上面那个例子属于少数。选择适当的编译器,我们将有能力消除return 处的不必要拷贝。 然而还有一处拷贝。函数必须返回一个临时 Matrix,而函数外的 mat_l 一定得拷贝它。不过,既然临时对象的内容马上就不要了,我们为什么不把他们偷过来呢?
看看这篇文章 C++0x展望[语言核心进化],这种Move语义已经列入 C++0x 的讨论列表。我们将可能获得能力判断一个对象是否临时对象,然后决定是否偷内容 —— 但是—— 也许你已经想到了:其实我们现在就可以提供额外的,可以手动调用的 Move 语义函数。事实上C++标准库已经这么做了: stl 中的 swap 就是为完成这种语义而设计的。
使用stl 的时候,如果按值返回一个容器,我们应该返回 non-const 容器,而且用swap 而不是 assign 保存他的内容:
std::vector< int > find_all_match( ... );
vet = find_all_match( ... ); // 不好,无意义的拷贝,慢
find_all_match().swap( vet ); // 推荐,高效节能
类似的,我们可以为 Matrix 添加一个non-const 成员方法 moveTo ,用于将其内容搬到另一个Matrix中。假如 operator * 按值返回 non-const Matrix, 我们可以这么做:
( mat1 * mat2 ). moveTo( mat3 );
拷贝构造函数实现起来则有些困难。我们可以调用临时对象的成员函数,却不能把他提交给一般引用 —— 语言只允许我们把临时对象绑定到 const 引用上。
所以,这种情况下需要把部分子成员声明为 mutable(记住,你必须从临时对象中实实在在的抠掉数据,不然他们会被析构的)。 换一种做法,我们也可以const_cast this,对不少人来说两者都相当不优雅。每个人都有自己的程序哲学; 对于我,为了易用和高效我相当欢迎诡异的做法:
class Matrix{
mutable std::vector< Elem > _data;
size_t _row, _col;
public:
// Move ctor, moves rhs to Matrix. .
Matrix( const Matrix& rhs, bool ){
_data.swap( rhs._data );
_row = rhs._row;
_col = rhs._col;
}
// moveTo rhs
void moveTo( Matrix& rhs ){
if( rhs == * this) return;
rhs._data.clear(); // clear _data first
rhs._data.swap( _data );
rhs._row = _row;
rhs._col = _col;
}
//...
};
由于底层使用了支持 move 语义的 stl 组件,上面的功能实现起来相当简单。我们swap(而不是拷贝)vector 到目标中,复制其他简单数据,over。 我们可以这样使用他们:
Matrix a( b, true ); // 调用 Move ctor, b的内容搬到a中,而b 的内容变得无效。
a.moveTo( b ); // a的内容搬到 b中, a的内容变得无效。
Matrix a ( b*c, true ); // 把临时结果用于构造 a ,内容移动到这个a中。
(b*c).moveTo( a ); // 把临时结果移动到 a 中
发表于 @ 2006年04月07日 14:06:00 | 评论( loading... ) | 举报| 收藏