Effective C++讀書筆記之三:儘可能使用const

Item 03:Use const whenever possible

如果關鍵字const出現在星號左邊,表示被指物是常量;如果出現在星號右邊,表示指針自身是常量;如果出現在星號兩邊,表示被植物和指針兩者都是常量。

STL迭代器以指針爲根據塑造出來,所以迭代器的作用就像個T*指針。聲明迭代器爲const就像聲明指針爲const一樣,如:
std::vector<int>vec;
const std::vector<int>::iterator iter = vec.begin();//iter的作用就像一個T*const
std::vector<int>::const iterator cIter = vec.begin();//cIter的作用就像一個const T*

令函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性。
舉個例子,考慮有理數operator*的聲明式:
class Rational{......};
const Rational operator* (const Rational & lhs,const Rational & rhs);

如果不返回一個const對象客戶就可能實現這樣的暴行:
Rational a,b,c;
...
(a*b)=c; //在a*b的結果上調用operator =

很多程序員可能在無意中這麼做,只因爲單純的打字錯誤:

if(a*b=c)...//其實只是想做一個比較動作!

一個“良好的用戶自定義類型”的特徵是它們避免無端地與內置類型不兼容,因此允許對兩值乘積做賦值動作也就沒什麼意思了。將operator*的回傳值聲明爲const可以預防那個“沒意思的賦值動作”。


const 成員函數

將const實施於成員函數的目的,是爲了確認該成員函數可以作用於const對象。

許多人漠視一件事實:兩個成員函數如果只是常量性不同,可以被重載,考慮如下一段代碼:
class TextBlock
{
public:
	...
	const char & operator[](std::size_t position) const//operator[]for const 對象
	{
		return text[position];
	} 
	char & operator[](std::size_t position)//operator[]for non-const對象 
	{
		return text[position];
	}
private:
	std::string text;
}
TextBlock的operator[]可以被這麼用:

TextBlock tb("Hello");
std::cout<<tb[0];//調用non-const TextBlock::operator[]

const TextBlock ctb("World");//調用const TextBlock::operator[]
std::cout<<ctb[0];

附帶一提,真實程序中const對象大多用於passed by pointer to constpassed by reference to const的比較結果。上述ctb的例子太過造作,下面這個比較真實:
void print (const TextBlock &ctb)
{
	std::cout<<ctb[0];
	... 
}
只要重載operator[]並對不同的版本給予不同的處理返回類型,就可以令const和non-const獲得不同的處理。

請注意,non const operator[]的返回值類型是一個reference to char,不是char。否則下面的句子就無法通過編譯:

tb[0]='x';

這是因爲,如果函數的返回值是個內置類型,那麼改動函數返回值從來就不合法。縱使合法,C++以by value返回對象這一事實意味着被改動的其實是tb.text[0]的一個副本,不是其自身,那不是你想要的行爲。

成員函數爲const意味着什麼?這有兩個流行的概念:bitwise constness 和 logical constness

bitwise constness陣營的人相信,成員函數只有在不更改對象之任何成員變量時纔可以說是const。然而不幸的是很多成員函數雖然不十分具備const性質卻能通過bitwise測試。具體地說,一個更改了“指針所指物"的成員函數雖然不能算是const,但如果只有指針(而非其所指物)隸屬於對象,那麼稱此函數爲bitwise constness 不會引發編譯器異議。如下:

class CTextBlock
{
public:
	...
	char& operator[](std:size_t position) const//bitwise constness聲明,但其實並不妥當
	{
		return pText[position];
	} 
private:
	char* pText;	
};

這個class不恰當地將其operator[]聲明爲const成員函數,而函數卻返回一個reference指向對象的內部值。由於operator[]的實現並不更改pText,於是編譯器很開心地認爲它是bitwise constness,但是看看它會發生什麼事:

const CTextBlock cctb("Hello");
char* pc=&cctb[0];//調用const operator[]取得一個指針,指向cctb的數據
                 //我認爲這裏應該是char*pc =cctb[0],歡迎大家一起討論

*pc='J';//cctb 現在有了"Jello"這樣的內容 

你創建一個常量對象並賦予初值,而且只對它調用const成員函數。但你終究還是改變了它的值。

這就是所謂的logical constness。這一派的擁護者主張,一個const成員函數可以修改它所處理對象內的某些bit,但只有在客戶端偵測不出的情況下才得如此。例如你的CTextBlock class可能高速緩存文本區塊的長度以便應付詢問:

class CTextBlock
{
public:
	...
	std::size_t length() const;
private:
	char *pText;
	std::size_t textLength; //最近一次計算的文本區塊長度
	bool lengthIsValid;   //目前的長度是否有效
};
std::size_t CTextBlock::length() const
{
	if(!lengthValid)
	{
		textLength = std::strlen(pText);
		lengthIsValid = true; //錯誤!在const成員函數內不能賦值給textLength和lengthIsValid 
	}
	return textLength;
}

length修改了textLength和lengthIsValid,這兩筆數據被修改對const CTextBlock來說可以接受,但是編譯器不同意,它堅持bitwise constness,怎麼辦呢?解決方法很簡單:利用C++寫一個與const相關的擺動場:mutable。mutable釋放掉non-static成員變量的bitwise constness約束:

class CTextBlock
{
public:
	...
	std::size_t length() const;
private:
	char *pText;
	mutable std::size_t textLength; 
	mutable bool lengthIsValid;   //這些成員變量可能總是會被修改,即使是在const成員函數內 
};
std::size_t CTextBlock::length() const
{
	if(!lengthValid)
	{
		textLength = std::strlen(pText);//現在,可以這樣 
		lengthIsValid = true; //也可以這樣 
	}
	return textLength;
}

在const和non-const成員函數中避免重複

對於”bitwise-constness 非我所欲”的問題,mutable是個解決辦法,但它不能解決所有的const相關難題。假設TextBlock內的operator[]不單只是返回一個reference指向某字符,也執行邊界檢驗、志記訪問信息、甚至可能進行數據完善性檢驗。如下:

class TextBlock
{
public:
	...
	const char& operator[](std::size_t position) const
	{
		...   //邊界檢驗
		...   //志記數據訪問
		...   //檢驗數據完整性
		return text[position]; 
	}
	char& operator[](std::size_t position)
	{
		...   //邊界檢驗
		...   //志記數據訪問
		...   //檢驗數據完整性
		return text[position]; 
	}
private:
	std::string text;
};

你能說出其中發生的代碼重複以及伴隨的編譯時間、維護、代碼膨脹等令人頭疼的問題嗎?你真正應該做的是實現operator[]的機能一次並實現它兩次。這促使我們將常量性移除。如下:

class TextBlock
{
public:
	...
	const char& operator[](std::size_t position) const
	{
		...   //邊界檢驗
		...   //志記數據訪問
		...   //檢驗數據完整性
		return text[position]; 
	}
	char& operator[](std::size_t opsition)
	{
		return
			 const_cast<char&>(//將op[]返回值的const移除 
			 	static_cast<const TextBlock&>(*this)//爲*this加上const 
				 	[position]);//調用const op[] 
		
	}
};

這份代碼有兩個轉型動作。我們打算讓non-const operator []調用其const兄弟,但non-const operator[]內部若只是單純調用operator[],會遞歸調用自己。爲了避免無窮遞歸,我們必須明確指出調用的是const operator[]。因此這裏將*this從其原始類型TextBlock&轉換爲const TextBlock&,這使接下來調用operator[]時得意調用const版本。第二次則是從const operator []的返回值中移除const。

至於反向做法——令const版本調用non-const版本以避免重複——那並不是你應該做的事。因爲const成員函數承諾絕不改變其對象的邏輯狀態而non-const成員函數卻沒有這般承諾。如果在const函數內調用non-const函數,就是冒了這樣的風險:你曾經承諾不改動的那個對象唄改動了。

請記住:
1.將某些東西聲明爲const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
2.編譯器強制實施bitwise constness,但你編寫程序是應該使用“概念上的常量性”。
3.當const和non-const成員函數有着實質等價的實現時,令non-const版本調用const版本可避免代碼重複。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章