淺談C++物理設計:最佳實踐

如何保證自知足

爲了驗證頭文件設計的自知足原則,實現文件的第一條語句必然是包含其對應的頭文件。html

反例:程序員

// cppunit/TestCase.cpp
#include "cppunit/core/TestResult.h"
#include "cppunit/core/Functor.h"
// 錯誤:沒有放在第一行,沒法校驗其自知足性
#include "cppunit/core/TestCase.h"

namespace
{
    struct TestCaseMethodFunctor : Functor
    {
        typedef void (TestCase::*Method)();
    
        TestCaseMethodFunctor(TestCase &target, Method method)
           : target(target), method(method)
        {}
    
        bool operator()() const
        {
            target.*method();
            return true;
        }
    
    private:
        TestCase ⌖
        Method method;
    };
}

void TestCase::run(TestResult &result)
{
    result.startTest(*this);
  
    if (result.protect(TestCaseMethodFunctor(*this, &TestCase::setUp)))
    {
        result.protect(TestCaseMethodFunctor(*this, &TestCase::runTest)); 
    }

    result.protect(TestCaseMethodFunctor(*this, &TestCase::tearDown));

    result.endTest(*this);
}

...

正例:算法

// cppunit/TestCase.cpp
#include "cppunit/core/TestCase.h"
#include "cppunit/core/TestResult.h"
#include "cppunit/core/Functor.h"

namespace
{
    struct TestCaseMethodFunctor : Functor
    {
        typedef void (TestCase::*Method)();
    
        TestCaseMethodFunctor(TestCase &target, Method method)
           : target(target), method(method)
        {}
    
        bool operator()() const
        {
            target.*method();
            return true;
        }
    
    private:
        TestCase ⌖
        Method method;
    };
}

void TestCase::run(TestResult &result)
{
    result.startTest(*this);
  
    if (result.protect(TestCaseMethodFunctor(*this, &TestCase::setUp))
    {
        result.protect(TestCaseMethodFunctor(*this, &TestCase::runTest)); 
    }

    result.protect(TestCaseMethodFunctor(*this, &TestCase::tearDown));

    result.endTest(*this);
}

...

overrideprivate

全部override的函數(除overridevirtual析構函數以外)都應該是private的,以保證按接口編程的良好設計原則。編程

反例:api

// html-parser/filter/AndFilter.h
#ifndef EOIPWORPIO_06123124_NMVBNSDHJF_497392
#define EOIPWORPIO_06123124_NMVBNSDHJF_497392

#include "html-parser/filter/NodeFilter.h"
#include <list>

struct AndFilter : NodeFilter
{
     void add(NodeFilter*);

     // 設計缺陷:本應該private
     OVERRIDE(bool accept(const Node&) const);

private:
     std::list<NodeFilter*> filters;
};

#endif

正例:數據結構

// html-parser/filter/AndFilter.h
#ifndef EOIPWORPIO_06123124_NMVBNSDHJF_497392
#define EOIPWORPIO_06123124_NMVBNSDHJF_497392

#include "html-parser/filter/NodeFilter.h"
#include <list>

struct AndFilter : NodeFilter
{
     void add(NodeFilter*);

private:
     OVERRIDE(bool accept(const Node&) const);

private:
     std::list<NodeFilter*> filters;
};

#endif

inline

避免頭文件中inline

頭文件中避免定義inline函數,除非性能報告指出此函數是性能的關鍵瓶頸。dom

C++語言將聲明和實現進行分離,程序員爲此不得不在頭文件和實現文件中重複地對函數進行聲明。這是C/C++天生給咱們的設計帶來的重複。這是一件痛苦的事情,驅使部分程序員直接將函數實現爲inlineide

inline函數的代碼做爲一種不穩定的內部實現細節,被放置在頭文件裏,其變動所致使的大面積的從新編譯是個大機率事件,爲改善微乎其微的函數調用性能與其相比將得不償失。函數

除非有相關profiling性能測試報告,代表這部分關鍵的熱點代碼須要被放回頭文件中。性能

但須要注意在特殊的狀況,能夠將實現inline在頭文件中,由於爲它們建立實現文件過於累贅和麻煩。

  • virtual析構函數

  • 空的virtual函數實現

  • C++11default函數

鼓勵實現文件中inline

對於在編譯單元內部定義的類而言,由於它的客戶數量是肯定的,就是它自己。另外,因爲它原本就定義在源代碼文件中,所以並無增長任何「物理耦合」。因此,對於這樣的類,咱們大能夠將其全部函數都實現爲inline的,就像寫Java代碼那樣,Once & Only Once

以單態類的一種實現技術爲例,講解編譯時依賴的解耦與匿名命名空間的使用。(首先,應該抵制單態設計的誘惑,單態其本質是面向對象技術中全局變量的替代品。濫用單態模式,猶如濫用全局變量,是一種典型的設計壞味道。只有肯定在系統中惟一存在的概念,才能使用單態模式)。

實現單態,須要對系統中惟一存在的概念進行封裝;但這個概念每每具備巨大的數據結構,若是將其聲明在頭文件中,無疑形成很大的編譯時依賴。

反例:

// ne/NetworkElementRepository.h
#ifndef UIJVASDF_8945873_YUQWTYRDF_85643
#define UIJVASDF_8945873_YUQWTYRDF_85643    

#include "base/Status.h"
#include "base/BaseTypes.h"
#include "transport/ne/NetworkElement.h"
#include <vector>

struct NetworkElementRepository
{
    static NetworkElement& getInstance();

    Status add(const U16 id);
    Status release(const U16 id);
    Status modify(const U16 id);
    
private:
    typedef std::vector<NetworkElement> NetworkElements;
    NetworkElements elements;
};

#endif

受文章篇幅的所限,NetworkElement.h未列出全部代碼實現,但咱們知道NetworkElement擁有巨大的數據結構,上述設計致使全部包含NetworkElementRepository的頭文件都被NetworkElement所間接污染。

此時,其中能夠將依賴置入到實現文件中,解除揭開其嚴重的編譯時依賴。更重要的是,它更好地遵照了按接口編程的原則,改善了軟件的擴展性。

正例:

// ne/NetworkElementRepository.h
#ifndef UIJVASDF_8945873_YUQWTYRDF_85643
#define UIJVASDF_8945873_YUQWTYRDF_85643    

#include "base/Status.h"
#include "base/BaseTypes.h"
#include "base/Role.h"

DEFINE_ROLE(NetworkElementRepository)
{
    static NetworkElementRepository& getInstance();

    ABSTRACT(Status add(const U16 id));
    ABSTRACT(Status release(const U16 id));
    ABSTRACT(Status modify(const U16 id));
};

#endif

其實現文件包含NetworkElement.h,將對其的依賴控制在本編譯單元內部。

// ne/NetworkElementRepository.cpp}]
#include "transport/ne/NetworkElementRepository.h"
#include "transport/ne/NetworkElement.h"
#include <vector>

namespace
{
    struct NetworkElementRepositoryImpl : NetworkElementRepository
    {
        OVERRIDE(Status add(const U16 id))
        {
            // inline implements
        }

        OVERRIDE(Status release(const U16 id))
        {
            // inline implements
        }

        OVERRIDE(Status modify(const U16 id))
        {
            // inline implements
        }
    
    private:
        typedef std::vector<NetworkElement> NetworkElements;
        NetworkElements elements;
    };
}

NetworkElementRepository& NetworkElementRepository::getInstance()
{
    static NetworkElementRepositoryImpl inst;
    return inst;
}

此處,對NetworkElementRepositoryImpl類的依賴是很是明確的,僅本編譯單元內,全部能夠直接進行inline,從而簡化了不少實現。

匿名namespace

匿名namespace的存在經常被人遺忘,但它的確是一個利器。匿名namespace的存在,使得全部受限於編譯單元內的實體擁有了明確的處所。

自此以後,全部C風格並侷限於編譯單元內的static函數和變量;以及相似Java中常見的private static的提取函數將經常被匿名namespace替代。

請記住匿名命名空間也是一種重要的信息隱藏技術。在實現文件中提倡使用匿名namespace, 以免潛在的命名衝突。

如上例,NetworkElementRepository.cpp經過匿名namespace,極大地減低了其頭文件的編譯時依賴。

struct VS. class

除了名字不一樣以外,classstruct惟一的差異是:默承認見性。這體如今定義和繼承時。struct在定義一個成員,或者繼承時,若是不指明,則默認爲public,而class則默認爲private

但這些都不是重點,重點在於定義接口和繼承時,冗餘public修飾符總讓人不舒服。簡單設計四原則告訴告訴咱們,全部冗餘的代碼都應該被剔除。

但不少人會認爲structC遺留問題,應該避免使用。但這不是問題,咱們不該該否定在寫C++程序時,依然在使用着不少C語言遺留的特性。關鍵在於,咱們使用的是C語言中能給設計帶來好處的特性,何樂而不爲呢?

正例:

// hamcrest/SelfDescribing.h
#ifndef OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
#define OIWER_NMVCHJKSD_TYT_48457_GSDFUIE

struct Description;

struct SelfDescribing
{
    virtual void describeTo(Description& description) const = 0;
    virtual ~SelfDescribing() {}
};

#endif

反例:

// hamcrest/SelfDescribing.h
#ifndef OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
#define OIWER_NMVCHJKSD_TYT_48457_GSDFUIE

class Description;

class SelfDescribing
{
public:
    virtual void describeTo(Description& description) const = 0;
    virtual ~SelfDescribing() {}
};

#endif

更重要的是,咱們確信「抽象」和「信息隱藏」對於軟件的重要性,這促使我將public接口總置於類的最前面成爲咱們的首選,class的特性正好與咱們的指望背道而馳(class的特性正好適合於將數據結構捧爲神物的程序員,它們經常將數據結構置於類聲明的最前面。)

無論你信仰那一個流派,切忌不能混合使用classstruct。在大量使用前導聲明的狀況下,一旦一個使用struct的類改成class,全部的前置聲明都須要修改。

萬惡的struct tag

定義C風格的結構體時,struct tag完全抑制告終構體前置聲明的可能性,從而阻礙了編譯優化的空間。

反例:

// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293

typedef struct tag_Cell
{
    WORD16 wCellId;
    WORD32 dwDlArfcn;
} T_Cell;

#endif
// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293

typedef struct
{
    WORD16 wCellId;
    WORD32 dwDlArfcn;
} T_Cell;

#endif

爲了兼容C併爲結構體前置聲明提供便利,以下解法是最合適的。

正例:

// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293

typedef struct T_Cell
{
    WORD16 wCellId;
    WORD32 dwDlArfcn;
} T_Cell;

#endif

須要注意的是,在C語言中,若是沒有使用typedef,則定義一個結構體的指針,必須顯式地加上struct關鍵字:struct T_Cell *pcell,而C++沒有這方面的要求。

PIMPL

若是性能不是關鍵問題,考慮使用PIMPL下降編譯時依賴。

反例:

// mockcpp/ApiHook.h
#ifndef OIWTQNVHD_10945_HDFIUE_23975_HFGA
#define OIWTQNVHD_10945_HDFIUE_23975_HFGA

#include "mockcpp/JmpOnlyApiHook.h"

struct ApiHook
{
    ApiHook(const void* api, const void* stub)
      : stubHook(api, stub)
    {}

private:
    JmpOnlyApiHook stubHook;
};

#endif

正例:

// mockcpp/ApiHook.h
#ifndef OIWTQNVHD_10945_HDFIUE_23975_HFGA
#define OIWTQNVHD_10945_HDFIUE_23975_HFGA

struct ApiHookImpl;

struct ApiHook
{
    ApiHook(const void* api, const void* stub);
    ~ApiHook();

private:
    ApiHookImpl* This;
};

#endif
// mockcpp/ApiHook.cpp
#include "mockcpp/ApiHook.h"
#include "mockcpp/JmpOnlyApiHook.h"

struct ApiHookImpl
{
   ApiHookImpl(const void* api, const void* stub)
     : stubHook(api, stub)
   {
   }

   JmpOnlyApiHook stubHook;
};

ApiHook::ApiHook( const void* api, const void* stub)
  : This(new ApiHookImpl(api, stub))
{
}

ApiHook::~ApiHook()
{
    delete This;
}

經過ApiHookImpl* This的橋接,在頭文件中解除了對JmpOnlyApiHook的依賴,將其依賴控制在本編譯單元內部。

template

編譯時依賴

當選擇模板時,不得不將其實現定義在頭文件中。當編譯時依賴開銷很是大時,編譯模板將成爲一種負擔。設法下降編譯時依賴,不只僅爲了縮短編譯時間,更重要的是爲了獲得一個低耦合的實現。

反例:

// oss/OssSender.h
#ifndef HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#define HGGAOO_4611330_NMSDFHW_86794303_HJHASI

#include "pub_typedef.h"
#include "pub_oss.h"
#include "oss_comm.h"
#include "pub_commdef.h"
#include "base/Assertions.h"
#include "base/Status.h"

struct OssSender
{
    OssSender(const PID& pid, const U8 commType)
      : pid(pid), commType(commType)
    {
    }
    
    template <typename MSG>
    Status send(const U16 eventId, const MSG& msg)
    {
        DCM_ASSERT_TRUE(OSS_SendAsynMsg(eventId, &msg, sizeof(msg), commType,(PID*)&pid) == OSS_SUCCESS); 
        return DCM_SUCCESS;
    }

private:
    PID pid;
    U8 commType;
};

#endif

爲了實現模板函數send,將OSS的一些實現細節暴露到了頭文件中,包含OssSender.h的全部文件將無心識地產生了對OSS頭文件的依賴。

提取一個私有的send函數,並將對OSS的依賴移入到OssSender.cpp中,對PID依賴經過前置聲明解除,最終實現如代碼所示。

正例:

// oss/OssSender.h
#ifndef HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#define HGGAOO_4611330_NMSDFHW_86794303_HJHASI

#include "base/Status.h"
#include "base/BaseTypes.h"

struct PID;

struct OssSender
{
    OssSender(const PID& pid, const U16 commType)
      : pid(pid), commType(commType) 
    {
    }
    
    template <typename MSG>
    Status send(const U16 eventId, const MSG& msg)
    {
        return send(eventId, (const void*)&msg, sizeof(MSG));    
    }

private:
    Status send(const U16 eventId, const void* msg, size_t size);

private:
    const PID& pid;
    U8 commType;
};

#endif

識別哪些與泛型相關,哪些與泛型無關的知識,並解開此類編譯時依賴是C++程序員的必備之技。

顯式模板實例化

模板的編譯時依賴存在兩個基本模型:包含模型,export模型。export模型受編譯技術實現的挑戰,最終被C++11標準放棄。

此時,彷佛咱們只能選擇包含模型。其實,存在一種特殊的場景,適時選擇顯式模板實例化(Explicit Template Instantiated),下降模板的編譯時依賴。是能作到下降模板編譯時依賴的。

反例:

// quantity/Quantity.h
#ifndef HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#define HGGQMVJK_892302_NGFSLEU_796YJ_GF5284

#include <quantity/Amount.h>

template <typename Unit>
struct Quantity
{
    Quantity(const Amount amount, const Unit& unit)      
      : amountInBaseUnit(unit.toAmountInBaseUnit(amount))
    {}
    
    bool operator==(const Quantity& rhs) const
    {
        return amountInBaseUnit == rhs.amountInBaseUnit;
    }
    
    bool operator!=(const Quantity& rhs) const
    {
        return !(*this == rhs);
    }
    
private:
    const Amount amountInBaseUnit;
};

#endif
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH
 
#include "quantity/Quantity.h"
#include "quantity/LengthUnit.h"

typedef Quantity<LengthUnit> Length;

#endif
// quantity/Volume.h
#ifndef HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#define HG764MD_NKGJKDSJLD_RY64930_NVHF977E
 
#include "quantity/Quantity.h"
#include "quantity/VolumeUnit.h"

typedef Quantity<VolumeUnit> Volume;

#endif

如上的設計,泛型類Quantity的實現都放在了頭文件,不穩定的實現細節,例如計算amountInBaseUnit的算法變化等因素,將致使包含LengthVolume的全部源文件都須要從新編譯。

更重要的是,由於LengthUnit, VolumeUnit頭文件的包含,若是因需求變化須要增長支持的單位,將間接致使了包含LengthVolume的全部源文件也須要從新編譯。

如何控制和隔離Quantity, LengthUnit, VolumeUnit變化的蔓延,而避免大部分的客戶代碼從新編譯,從而與客戶完全解偶呢?能夠經過顯式模板實例化將模板實現從頭文件中剝離出去,從而避免了沒必要要的依賴。

正例:

// quantity/Quantity.h
#ifndef HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#define HGGQMVJK_892302_NGFSLEU_796YJ_GF5284

#include <quantity/Amount.h>

template <typename Unit>
struct Quantity
{
    Quantity(const Amount amount, const Unit& unit);
        
    bool operator==(const Quantity& rhs) const;
    bool operator!=(const Quantity& rhs) const;
        
private:
    const Amount amountInBaseUnit;
};

#endif
// quantity/Quantity.tcc
#ifndef FKJHJT68302_NVGKS97474_YET122_HEIW8565
#define FKJHJT68302_NVGKS97474_YET122_HEIW8565

#include <quantity/Quantity.h>

template <typename Unit>
Quantity<Unit>::Quantity(const Amount amount, const Unit& unit)      
  : amountInBaseUnit(unit.toAmountInBaseUnit(amount))
{}
    
template <typename Unit>
bool Quantity<Unit>::operator==(const Quantity& rhs) const
{
    return amountInBaseUnit == rhs.amountInBaseUnit;
}
    
template <typename Unit>
bool Quantity<Unit>::operator!=(const Quantity& rhs) const
{
    return !(*this == rhs);
}

#endif
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH

#include "quantity/Quantity.h"

struct LengthUnit;
struct Length : Quantity<LengthUnit> {};

#endif
// quantity/Length.cpp
#include "quantity/Quantity.tcc"
#include "quantity/LengthUnit.h"

template struct Quantity<LengthUnit>;
// quantity/Volume.h
#ifndef HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#define HG764MD_NKGJKDSJLD_RY64930_NVHF977E

#include "quantity/Quantity.h"

struct VolumeUnit;
struct Volume : Quantity<VolumeUnit> {};

#endif
// quantity/Volume.cpp
#include "quantity/Quantity.tcc"
#include "quantity/VolumeUnit.h"

template struct Quantity<VolumeUnit>;

Length.h僅僅對Quantity.h產生依賴; 特殊地,Length.cpp沒有產生對Length.h的依賴,相反對Quantity.tcc產生了依賴。

另外,Length.hLengthUnit的依賴關係也簡化爲聲明依賴,而對其真正的編譯時依賴,也控制在模板實例化的時刻,即在Length.cpp內部。

LenghtUnit, VolumeUnit的變化,及其Quantity.tcc實現細節的變化,被徹底地控制在Length.cpp, Volume.cpp內部。

子類化優於typedef/using

若是使用typedef,若是存在對Length的依賴,即便是名字的聲明依賴,除了包含頭文件以外,別無選擇。

另外,若是Quantity存在virtual函數時,Length還有進一步擴展Quantity的可能性,從而使設計提供了更大的靈活性。

反例:

// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH

#include "quantity/Quantity.h"

struct LengthUnit;
typedef Quantity<LengthUnit> Length;

#endif

正例:

// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH

#include "quantity/Quantity.h"

struct LengthUnit;
struct Length : Quantity<LengthUnit> {};

#endif
相關文章
相關標籤/搜索