從一段代碼談起

看看下面這段代碼:

#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)下面的情況:

  1. 線程A調(diào)用createLock函數(shù),此時,由于成員m_Lock為NULL,那么它會繼續(xù)調(diào)用成員m_Lock的構(gòu)造函數(shù),構(gòu)造這把鎖,在構(gòu)造的過程中,線程B搶占了執(zhí)行權(quán);
  2. 由于線程B獲取了執(zhí)行權(quán),它也調(diào)用createLock函數(shù),此時,由于線程A還未構(gòu)造成功m_Lock,因此m_Lock的值還為NULL,那么線程B也會去構(gòu)造一遍成員m_Lock。
  3. 這樣,線程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í)的腳步,否則真的是不進則退。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容