看看下面這段代碼:
#include "RW_Thread_Mutex.h"
/// @class RWLock
/// @brief 讀寫鎖
class RWLock
{
public:
/// 構(gòu)造函數(shù)
RWLock()
{
m_Lock = NULL;
}
/// 析構(gòu)函數(shù)
virtual ~RWLock()
{
if (m_Lock)
delete m_Lock;
m_Lock = NULL;
}
public:
/// 創(chuàng)建鎖,確保該函數(shù)在單線程環(huán)境中被調(diào)用
void createLock()
{
if (NULL == m_Lock)
m_Lock = new ACE_RW_Thread_Mutex;
}
/// 讀上鎖
void lockRead()
{
if (NULL != m_Lock)
m_Lock->acquire_read();
}
/// 寫上鎖
void lockWrite()
{
if (NULL != m_Lock)
m_Lock->acquire_write();
}
/// 解鎖
void unlock()
{
if (NULL != m_Lock)
m_Lock->release();
}
private:
/// ACE讀寫鎖
ACE_RW_Thread_Mutex *m_Lock;
};
這是工作上合作單位給的一段代碼,來自一位從業(yè)近10年的C++程序員。功能很簡單,就是對讀寫鎖的一個封裝,但以我看來,這段代碼暴露了諸多問題:
人為拆分了構(gòu)造的過程
第一次使用這個類,相信很多人都會跟我一樣:
// 構(gòu)造一個讀寫鎖
RWLock rwlock;
// 加讀鎖
rwlock.lockRead();
// 解鎖
rwlock.unlock();
畢竟是很常用的一個功能,但如果這樣去使用,那么就可能在運行時出現(xiàn)亂七八糟的問題。
在類的定義中,有一個方法,叫做createLock,看它的注釋,真是讓人不明所以:
創(chuàng)建鎖,確保該函數(shù)在單線程環(huán)境中被調(diào)用
再看看它的實現(xiàn),這才恍然大悟。原來僅僅調(diào)用類的構(gòu)造函數(shù),還不能真正的構(gòu)造出這個對象,必須再調(diào)用一次createLock函數(shù),才能完成構(gòu)造過程。
在createLock函數(shù)中,作者倒很貼心地給我們添加了“非空判斷”:
if (NULL == m_Lock)
m_Lock = new ACE_RW_Thread_Mutex;
這樣,無論調(diào)用多少次createLock函數(shù),都只會構(gòu)造一次鎖,是一種“懶加載”,但實際情況真正如此嗎?
假設(shè)兩個線程A和B同時需要搶占這把鎖,那么可能會出現(xiàn)下面的情況:
- 線程A調(diào)用createLock函數(shù),此時,由于成員m_Lock為NULL,那么它會繼續(xù)調(diào)用成員m_Lock的構(gòu)造函數(shù),構(gòu)造這把鎖,在構(gòu)造的過程中,線程B搶占了執(zhí)行權(quán);
- 由于線程B獲取了執(zhí)行權(quán),它也調(diào)用createLock函數(shù),此時,由于線程A還未構(gòu)造成功m_Lock,因此m_Lock的值還為NULL,那么線程B也會去構(gòu)造一遍成員m_Lock。
- 這樣,線程A和線程B都去構(gòu)造了一遍m_Lock,并且后完成構(gòu)造的成員會覆蓋掉先一步構(gòu)造的的成員,造成了內(nèi)存泄漏。
更嚴(yán)重的,假設(shè)此時線程A重新獲取了執(zhí)行權(quán),那么它將會完成成員m_Lock的構(gòu)造,如果這時候它調(diào)用了加鎖的方法,而線程B又將成員覆蓋掉,那么線程A的鎖將永遠(yuǎn)無法得到釋放,因為,創(chuàng)建的鎖已經(jīng)被泄漏掉了。與之相比,內(nèi)存泄漏倒不是什么大問題了。
好像這個問題發(fā)生的概率很低,但千萬不要認(rèn)為低概率的問題不會發(fā)生。個人認(rèn)為,實際情況中,小概率的事件一定會發(fā)生,極小概率的事件很有可能會發(fā)生。這些問題一旦被觸發(fā),那么一定要花費非常多的時間去排查錯誤,真是得不償失。
這個方法的注釋倒是明確表示請“確保該函數(shù)在單線程環(huán)境中被調(diào)用”,但是別忘了,使用讀寫鎖,那幾乎是在多線程的環(huán)境下,所謂“單線程”下調(diào)用的場景,可以說并不存在。
其實,完全可以避免這個問題。只需要移除這個莫名其妙的createLock函數(shù),并將構(gòu)造的過程在構(gòu)造函數(shù)中實現(xiàn)就行了。構(gòu)造函數(shù)就是應(yīng)該完成對象的構(gòu)造,否則這個函數(shù)也不應(yīng)該叫構(gòu)造函數(shù)了。把構(gòu)造的過程人為地拆分成兩個步驟,不僅僅會讓使用者摸不著頭腦,而且可能會造成十分嚴(yán)重的后果。
還是那句話,不要過早地去優(yōu)化代碼。
在不應(yīng)該繼承的場景下允許繼承
這段代碼還有一個神奇的地方,它的析構(gòu)函數(shù)是一個虛函數(shù)。
我們知道,在C++中,如果不把析構(gòu)函數(shù)聲明為虛函數(shù),那么可能會造成內(nèi)存泄漏的危險。所以,當(dāng)聲明一個可以被繼承的類時,咱們一般會聲明一個虛析構(gòu)函數(shù),哪怕是一個空實現(xiàn)。反之,如果我們不想讓自己地類被繼承,那么析構(gòu)函數(shù)就應(yīng)該是非虛擬的。
在C++ 11之前,我們可以將構(gòu)造函數(shù)設(shè)置為私有,去強制阻止該類的繼承,但也會造成很大的變扭,因為無法使用構(gòu)造函數(shù)去構(gòu)造對象了。所以一些比較舊的代碼,會使用“將析構(gòu)函數(shù)設(shè)置為非虛函數(shù)”的方式,人為地去限制繼承。當(dāng)使用者看到帶有非虛析構(gòu)函數(shù)的類時,就應(yīng)該考慮考慮這些類被創(chuàng)建時的意圖,是否類的編寫者不希望自己的類被繼承。STL中的vector和map等都沒有提供非虛擬的析構(gòu)函數(shù),你會去繼承vector或map嗎?我想不會吧。
久而久之,似乎大家約定俗成,帶有非虛析構(gòu)函數(shù)的類是不應(yīng)該被繼承的。
回頭看一看這個類,它是一個讀寫鎖的實現(xiàn),而非接口,雖然它的命名很有疑惑性?;蛟S你希望使用別的方式去實現(xiàn)一個讀寫鎖,那么你應(yīng)該將這個類的函數(shù)提取出一個接口,繼承那個接口,而非這個類。想想看,你使用一個類去繼承另一個類時,需要重寫該類幾乎所有的函數(shù),那么這個繼承還有什么意義呢?前提時你寫得不是一個代理類,但是代理類也應(yīng)該繼承接口,而非實現(xiàn)類。
接口是一種約定的對象使用方式,是使用的說明書,而實現(xiàn)類則代表了使用某一種具體的方法,去完成這一接口的使用方式。使用一種實現(xiàn)方式,去繼承使用另一種實現(xiàn)方式的實現(xiàn)類,本身在邏輯上就難以說通。
在翻看別的代碼時,我驚訝地發(fā)現(xiàn)了為什么將這個類的析構(gòu)函數(shù)設(shè)置為虛擬函數(shù)的原因,看看另一個需要使用讀寫鎖的類:
/// 內(nèi)存表
public class MemTable : public RWLock
{
// ......
template<class T>
void get(size_t row, size_t col, T &val) const
{
lockRead();
// ...
unlock();
}
template<class T>
void set(size_t row, size_t col, const T &val)
{
lockWrite();
// ...
unlock();
}
// ......
};
原來設(shè)置虛析構(gòu)函數(shù)的目的,真的是為了繼承啊!
我們知道,繼承代表了一種“is a”的關(guān)系,即子類(或接口)是基類(或接口)的一種。好比鳥是一種動物,那么鳥就應(yīng)該繼承自動物。當(dāng)這里的對象是一種讀寫鎖嗎?顯然不是。只是這個對象在實際使用中,需要用到讀寫鎖而已。
明明是一種“has a”的包含關(guān)系,偏偏要使用“is a”的繼承關(guān)系,這種“繼承依賴癥”可以說是很差勁了。這種代碼,出自從業(yè)近十年的C++程序員,可能會對新手造成非常大的不良影響。
總結(jié)
經(jīng)過修改后的代碼成了這樣:
#include "RW_Thread_Mutex.h"
/// @class RWLock
/// @brief 讀寫鎖
class RWLock final
{
public:
/// 構(gòu)造函數(shù)
RWLock()
{
m_Lock = new ACE_RW_Thread_Mutex;
}
/// 析構(gòu)函數(shù)
~RWLock()
{
delete m_Lock;
}
public:
/// 讀上鎖
void lockRead()
{
m_Lock->acquire_read();
}
/// 寫上鎖
void lockWrite()
{
m_Lock->acquire_write();
}
/// 解鎖
void unlock()
{
m_Lock->release();
}
private:
/// ACE讀寫鎖
ACE_RW_Thread_Mutex *m_Lock;
};
添加了一個final關(guān)鍵字,強制該類不允許繼承。
其實我想說的,不僅僅是這幾個粗淺的問題而已。絕大多數(shù)老程序員的水平都很高,但也有很多號稱經(jīng)驗豐富的老程序員,其實并不是特別厲害,他們在長期的編碼過程中,驕傲自負(fù),早已不知學(xué)習(xí)為何物,凡事不講究個刨根問底,代碼往往可以運行就滿足了,問題越積越深。新手也不要盲目地相信年限經(jīng)驗,多思考代碼為何這樣寫,有沒有隱藏的問題,這樣才能真正地提高。
所以,永遠(yuǎn)不要停止學(xué)習(xí)的腳步,否則真的是不進則退。