GotW #06 Const-Correctness
著者:Herb Sutter
翻译:kingofark
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款06:正确使用const
难度:6 / 10
(总是尽可能的使用const,但也不要用得太过分而造成滥用。哪些地方应该用const,哪些地方不应该用?这里我们列举一些或明显或不明显的例子。)
[问题]
Const是编写“安全”代码的一个强有力的工具,还有利于编译器的优化处理。你应该尽可能的使用它……但是,“尽可能的”到底是什么意思?
请不要评价这个程序的好坏,也不要改变它的结构;这个程序是为了说明本条款的问题而故意精心设计的。你只要在适当的地方对其添加或者删除“const”(包括各种变体和相关的关键字)就可以了。(附加题:哪些地方的const用法造成了程序有无法预料的结果或者无法编译通过的错误?)
class Polygon { public: Polygon() : area_(-1) {} void AddPoint( const Point pt ) { InvalidateArea(); points_.push_back(pt); } Point GetPoint( const int i ) { return points_[i]; } int GetNumPoints() { return points_.size(); } double GetArea() { if( area_ < 0 ) // 如果还没有被计算和保存, CalcArea(); // 那么现在就开始。 return area_; } private: void InvalidateArea() { area_ = -1; } void CalcArea() { area_ = 0; vector<Point>::iterator i; for( i = points_.begin(); i != points_.end(); ++i ) area_ += /* some work */; } vector<Point> points_; double area_; }; Polygon operator+( Polygon& lhs, Polygon& rhs ) { Polygon ret = lhs; int last = rhs.GetNumPoints(); for( int i = 0; i < last; ++i ) // 连接 ret.AddPoint( rhs.GetPoint(i) ); return ret; } void f( const Polygon& poly ) { const_cast<Polygon&>(poly).AddPoint( Point(0,0) ); } void g( Polygon& const rPoly ) { rPoly.AddPoint( Point(1,1) ); } void h( Polygon* const pPoly ) { pPoly->AddPoint( Point(2,2) ); } int main() { Polygon poly; const Polygon cpoly; f(poly); f(cpoly); g(poly); h(&poly); }
[解答]
* class Polygon { public: Polygon() : area_(-1) {} void AddPoint( const Point pt ) { InvalidateArea(); points_.push_back(pt); }
1. 因为Point对象采用的是传值方式,所以把它声明为const几乎没有什么油水可捞,不会对性能有显著影响。
* Point GetPoint( const int i ) { return points_[i]; }
2. 同1,const值传递没有多大意义,反而容易引起人误解。
3. 这个成员函数应该定义成一个const成员函数,因为它不改变对象的状态。
4. (本要点是一个有争议的提法)如果该函数的返回类型不是一个内建(built-in)类型的话,通常应该将其返回类型也声明为const。这样做有利于该函数的调用者,因为它使得编译器能够在函数调用者企图修改临时量的时候产生一个错误信息,从而达到保护目的(例如, “Poly.GetPoint(i) = Point(2, 2);”,等等。诚然,如果真的想要修改临时量,那么首先就应该让GetPoint()返回引用(return-by-reference)而不是返回值(return-by-value)。然而在后面的叙述中我们又将看到,在用于operator+()中的const Polygon对象时,让GetPoint()返回const值或者const引用又是很有用的。)。
[作者记:Lakos(pg.618)对返回const值提出异议。他认为,对于内建(built-in)类型也返回const值不但没有什么实际意义的,反而还会影响模板的实体化过程(instantiation)。]
[学习指导]:在函数内对非内建(non-built-in)的类型采用值返回(return-by-value)的方法时,最好让函数返回一个const值。
* int GetNumPoints() { return points_.size(); }
5. 还是那句话,函数本身应该被声明为const。
(注意在这里不应该返回const int,因为int本身已经是一个右值类型;加上const以后反而会影响模板的实体化过程(instantiation),使代码变得容易让人糊涂,引人误解,甚至让人消化不良也有可能。)
* double GetArea() { if( area_ < 0 ) //如果还没有被计算和保存, CalcArea(); //那么现在就开始。 return area_; }
6. 尽管这个函数修改了对象的内部状态,但它还是应该被声明为const,因为被修改对象的可见(observable)状态没有发生变化(我们所做的是一些隐蔽的、不可告人的事情,但这是一个实现中的细节,即这个对象从逻辑上讲还是const)。这意味着,area_应该被声明成mutable。如果你的编译器目前还不支持mutable关键字的话,可以对area_采取const_cast操作以作为替代方案(最好还能在这里写一个注释,以便在可以使用mutable关键字的时候把这个cast操作替换掉。),但记住一定要让函数本身被声明为const。
* private: void InvalidateArea() { area_ = -1; }
7. 尽管这一观点也是备受争议,我还是坚持认为,即使仅仅只是出于一致性的考虑,也还是应该把这个函数声明为const。(当然我不得不承认,从语义学的角度上讲,这个函数只会被非const的函数调用,因为毕竟其目的只是想在对象的状态改变时让保存的area_值无效。)
* void CalcArea() { area_ = 0; vector<Point>::iterator i; for( i = points_.begin(); i != points_.end(); ++i ) area_ += /* some work */; }
8. 这个成员函数绝对应该是const才对。不管怎么说,它至少会被另外一个const成员函数即GetArea()调用。
9. 既然iterator不会改变points_集的状态,所以这里应该是一个const_iterator。
* vector<Point> points_; double area_; }; Polygon operator+( Polygon& lhs, Polygon& rhs ) {
10. 显然,这里应该通过const引用(reference)进行传递。
11. 还是那句话,返回值应该是const的。
* Polygon ret = lhs; int last = rhs.GetNumPoints();
12. 既然“last”也从不会被改变,那么也应该为“const int”型。
(这也是GetPoint()——不管它返回的是const值还是const引用——必须是一个const成员函数的原因。)
* return ret; } void f( const Polygon& poly ) { const_cast<Polygon&>(poly).AddPoint( Point(0,0) );
附加题的要点:如果被引用的对象被声明为const,那么这里的结果将是未定义的(如同下面要讲的f(cpoly)一样)。事实是,其参数并不真是const的,所以千万不要把它声明为const!
* } void g( Polygon& const rPoly ) { rPoly.AddPoint( Point(1,1) ); }
13. 这里的const毫无作用,因为无论如何一个引用(reference)是不可能被改变,使其指向另一个对象的。
* void h( Polygon* const pPoly ) { pPoly->AddPoint( Point(2,2) ); }
14. 这里的const也不起作用,但其原因与13中的不同:这次是因为你对指针使用传值方式,这与上面讲到的传递一个const int参数一样毫无意义。
(如果你在对附加题的解答中提到这里的函数会产生编译错误的话……不好意思,它们是合法的C++用法。也许你还考虑过把const放在&或者*的左边,但那只会使函数体本身不符合C++用法。)
* int main() { Polygon poly; const Polygon cpoly; f(poly);
这儿很好,没什么问题。
* f(cpoly);
在这里,当f()企图放弃const属性从而修改其参数的时候,会产生不确定的结果。
* g(poly);
这一语句没问题。
* h(&poly);
这一句也没问题。
}
好了,终于完了!现在得到了正确的代码版本(当然,只改正了有关const的错误,而不管其不良的编码风格):
class Polygon { public: Polygon() : area_(-1) {} void AddPoint( Point pt ) { InvalidateArea(); points_.push_back(pt); } const Point GetPoint( int i ) const { return points_[i]; } int GetNumPoints() const { return points_.size(); } double GetArea() const { if( area_ < 0 ) // 如果还没有进行计算和保存, CalcArea(); //那么现在就开始。 return area_; } private: void InvalidateArea() const { area_ = -1; } void CalcArea() const { area_ = 0; vector<Point>::const_iterator i; for( i = points_.begin(); i != points_.end(); ++i ) area_ += /* some work */; } vector<Point> points_; mutable double area_; }; const Polygon operator+( const Polygon& lhs, const Polygon& rhs ) { Polygon ret = lhs; const int last = rhs.GetNumPoints(); for( int i = 0; i < last; ++i ) // 连接 ret.AddPoint( rhs.GetPoint(i) ); return ret; } void f( Polygon& poly ) { poly.AddPoint( Point(0,0) ); } void g( Polygon& rPoly ) { rPoly.AddPoint( Point(1,1) ); } void h( Polygon* pPoly ) { pPoly->AddPoint( Point(2,2) ); } int main() { Polygon poly; f(poly); g(poly); h(&poly); }