1. 程式人生 > >Guru of the Week 條款06:正確使用const

Guru of the Week 條款06:正確使用const

 

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<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

難度: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_;
doublearea_;
};
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.1const值傳遞沒有多大意義,反而容易引起人誤解。

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_;
doublearea_;
};
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) {}
voidAddPoint( Point pt ){ InvalidateArea();
points_.push_back(pt); }
const Point GetPoint( int i ) const{ return points_[i]; }
intGetNumPoints() 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);
}