淺談C++ 物理設(shè)計

軟件設(shè)計是一個守破離的過程
  1. 設(shè)計原則
  2. 代碼規(guī)范
  3. 最佳實踐
  4. 附錄: 實用宏

設(shè)計原則

原則 基本含義
自滿足原則 頭文件本身是可以編譯通過的
單一職責原則 頭文件包含的實體的職責是單一的
最小依賴原則 絕不包含不必要的頭文件
最小可見性原則 盡量封裝隱藏類的成員

自滿足原則

所有頭文件都應該自滿足的??匆粋€具體的示例代碼,這里定義了一個TestCase.h頭文件。TestCase對父類TestLeaf, TestFixture都存在編譯時依賴,但沒有包含基類的頭文件。

反例:

// cppunit/TestCase.h
#ifndef EOPTIAWE_23908576823_MSLKJDFE_0567925
#define EOPTIAWE_23908576823_MSLKJDFE_0567925    

struct TestCase : TestLeaf, TestFixture
{
    TestCase(const std::string &name="");
    
private:
    OVERRIDE(void run(TestResult *result));
    OVERRIDE(std::string getName() const);

private:
    ABSTRACT(void runTest());
    
private:
    const std::string name;
};

#endif

為了滿足自滿足原則,其自身必須包含其所有父類的頭文件。

正例:

// cppunit/TestCase.h
#ifndef EOPTIAWE_23908576823_MSLKJDFE_0567925
#define EOPTIAWE_23908576823_MSLKJDFE_0567925    

#include "cppunit/core/TestLeaf.h"
#include "cppunit/core/TestFixture.h"

struct TestCase : TestLeaf, TestFixture
{
    TestCase(const std::string &name="");
    
private:
    OVERRIDE(void run(TestResult &result));
    OVERRIDE(std::string getName() const);

private:
    ABSTRACT(void runTest());
    
private:
    const std::string name;
};

#endif

即使TestCase直接持有name的成員變量,但沒有必要包含std::string的頭文件,因為TestCase覆寫了其父類的getName成員函數(shù),父類為了保證自滿足原則,自然已經(jīng)包含了std::string的頭文件。

同樣的原因,也沒有必要在此前置聲明TestResult,因為父類可定已經(jīng)聲明過了。

單一職責

這是SRP(Single Reponsibility Priciple)在頭文件設(shè)計時的一個具體運用。頭文件如果包含了其它不相關(guān)的元素,則包含該頭文件的所有實現(xiàn)文件都將被這些不相關(guān)的元素所污染,重編譯將成為一件高概率的事件。

如示例代碼,將OutputStream, InputStream同時定義在一個頭文件中,將違背該原則。本來只需只讀接口,無意中被只寫接口所污染。

反例:

// io/Stream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_980341
#define LDGOUIETA_437689Q20_ASIOHKFGP_980341

#include "base/Role.h"

DEFINE_ROLE(OutputStream)
{
    ABSTRACT(void write());
};

DEFINE_ROLE(InputStream)
{
    ABSTRACT(void read());
};

#endif

正例: 先創(chuàng)建一個OutputStream.h文件:

// io/OutputStream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_010234
#define LDGOUIETA_437689Q20_ASIOHKFGP_010234
    
#include "base/Role.h"

DEFINE_ROLE(OutputStream)
{
    ABSTRACT(void write());
};

#endif

再創(chuàng)建一個InputStream.h文件:

// io/InputStream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_783621
#define LDGOUIETA_437689Q20_ASIOHKFGP_783621

#include "base/Role.h"

DEFINE_ROLE(InputStream)
{
    ABSTRACT(void read());
};

#endif

最小依賴

一個頭文件只應該包含必要的實體,尤其在頭文件中僅僅對實體的聲明產(chǎn)生依賴,那么前置聲明是一種有效的降低編譯時依賴的技術(shù)。

反例:

// cppunit/Test.h
#ifndef PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#define PERTP_8792346_QQPKSKJ_09472_HAKHKAIE

#include <base/Role.h>
#include <cppunit/core/TestResult.h>
#include <string>

DEFINE_ROLE(Test)
{
    ABSTRACT(void run(TestResult& result));
    ABSTRACT(int countTestCases() const);
    ABSTRACT(int getChildTestCount() const);
    ABSTRACT(std::string getName() const);
};

#endif

如示例代碼,定義了一個xUnit框架中的Test頂級接口,其對TestResult的依賴僅僅是一個聲明依賴,并沒有必要包含TestResult.h,前置聲明是解開這類編譯依賴的鑰匙。

值得注意的是,對標準庫std::string的依賴,即使它僅作為返回值,但因為它實際上是一個typedef,所以必須老實地包含其對應的頭文件。事實上,如果產(chǎn)生了對標準庫名稱的依賴,基本上都需要包含對應的頭文件。

另外,對DEFINE_ROLE宏定義的依賴則需要包含相應的頭文件,以便實現(xiàn)該頭文件的自滿足。

但是,TestResult僅作為成員函數(shù)的參數(shù)出現(xiàn)在頭文件中,所以對TestResult的依賴只需前置聲明即可。

正例:

// cppunit/Test.h
#ifndef PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#define PERTP_8792346_QQPKSKJ_09472_HAKHKAIE

#include <base/Role.h>
#include <string>

struct TestResult;

DEFINE_ROLE(Test)
{
    ABSTRACT(void run(TestResult& result));
    ABSTRACT(int countTestCases() const);
    ABSTRACT(int getChildTestCount() const);
    ABSTRACT(std::string getName() const);
};

#endif

在選擇包含頭文件還是前置聲明時,很多程序員感到迷茫。其實規(guī)則很簡單,在如下場景前置聲明即可,無需包含頭文件:

  • 指針
  • 引用
  • 返回值
  • 函數(shù)參數(shù)

相反地,如果編譯器需要知道實體的真正內(nèi)容時,則必須包含頭文件,此依賴也常常稱為強編譯時依賴。強編譯時依賴主要包括如下幾種場景:

  • typedef定義的實體
  • 繼承
  • inline
  • template
  • 引用類內(nèi)部成員時
  • sizeof運算

最小可見性

在頭文件中定義一個類時,清晰、準確的public, protected, private是傳遞設(shè)計意圖的指示燈。其中private做為一種實現(xiàn)細節(jié)被隱藏起來,為適應未來不明確的變化提供便利的措施。

不要將所有的實體都public,這無疑是一種自殺式做法。應該以一種相反的習慣性思維,盡最大可能性將所有實體private,直到你被迫不得不這么做為止,依次放開可見性的權(quán)限。

如下例代碼所示,按照public-private, function-data依次排列類的成員,并對具有相同特征的成員歸類,將大大改善類的整體布局,給讀者留下清晰的設(shè)計意圖。

反例:

//trans-dsl/sched/SimpleAsyncAction.h
#ifndef IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#define IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK

#include "trans-dsl/action/Action.h"
#include "trans-dsl/utils/EventHandlerRegistry.h"

struct SimpleAsyncAction : Action
{
    template<typename T>
    Status waitOn(const EventId eventId, T* thisPointer,
             Status (T::*handler)(const TransactionInfo&, const Event&), 
             bool forever = false)
    {
        return registry.addHandler(eventId, thisPointer, handler, forever);
    }

    Status waitUntouchEvent(const EventId eventId);

    OVERRIDE(Status handleEvent(const TransactionInfo&, const Event&));
    OVERRIDE(void kill(const TransactionInfo&, const Status)); 

    DEFAULT(void, doKill(const TransactionInfo&, const Status));

    EventHandlerRegistry registry;
};

#endif

正例:

// trans-dsl/sched/SimpleAsyncAction.h
#ifndef IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#define IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK

#include "trans-dsl/action/Action.h"
#include "trans-dsl/utils/EventHandlerRegistry.h"

struct SimpleAsyncAction : Action
{
    template<typename T>
    Status waitOn(const EventId eventId, T* thisPointer,
             Status (T::*handler)(const TransactionInfo&, const Event&), 
             bool forever = false)
    {
        return registry.addHandler(eventId, thisPointer, handler, forever);
    }

    Status waitUntouchEvent(const EventId eventId);

private:
    OVERRIDE(Status handleEvent(const TransactionInfo&, const Event&));
    OVERRIDE(void kill(const TransactionInfo&, const Status)); 

private:
    DEFAULT(void, doKill(const TransactionInfo&, const Status));

private:
    EventHandlerRegistry registry;
};

#endif

代碼規(guī)范

頭文件保護宏

每一個頭文件都應該具有獨一無二的保護宏,并保持命名規(guī)則的一致性,其中命名規(guī)則包括兩種風格:

  • INCL_<PROJECT>_<MODULE>_<FILE>_H
  • 全局唯一的隨機序列碼

第一種命名規(guī)則問題在于:當文件名重命名或移動目錄時,需要同步修改頭文件保護宏;推薦使用IDE隨機自動地生成頭文件保護宏,其更加快捷、簡單、安全、有效。

反例:

// thread/Runnable.h
// 因名稱太短,存在名字沖突的可能性
#ifndef RUNNABLE_H
#define RUNNABLE_H

#include "base/Role.h"

DEFINE_ROLE(Runnable)
{
    ABSTRACT(void run());
};

#endif

正例:

// cppunit/AutoRegisterSuite.h
#ifndef INCL_CPPUNIT_AUTO_REGISTER_SUITE_H
#define INCL_CPPUNIT_AUTO_REGISTER_SUITE_H

#include "base/Role.h"

struct TestSuite;

DEFINE_ROLE(AtuoRegisterSuite)
{
    ABSTRACT(void add(TestSuite&));
};

#endif

正例:

// cppunit/AutoRegisterSuite.h
// IDE自動生成
#ifndef INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H
#define INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H

#include "base/Role.h"

struct TestSuite;

DEFINE_ROLE(AtuoRegisterSuite)
{
    ABSTRACT(void add(TestSuite&));
};

#endif

下劃線與駝峰

路徑名一律使用小寫、下劃線或中劃線風格的名稱;文件名應該與程序主要實體名稱相同,可以使用駝峰命名,也可以使用小寫、下劃線或中劃線分割的名字;實現(xiàn)文件的名字必須和頭文件保持一致;包含頭文件時,必須保持路徑名、文件名大小寫敏感。

反例:

// 路徑名htmlParser使用了駝峰命名風格
#include "htmlParser/core/Attribute.h"

正例:

// 正確的頭文件包含
#include "html-parser/core/Attribute.h"
#include "yaml_parser.h"

最后,值得注意的是,團隊內(nèi)必須保持一致的命名風格。

大小寫敏感

包含頭文件時,必須保持路徑名、文件名大小寫敏感。因為在\ascii{Windows},其大小寫不敏感,編譯時檢查失效,代碼失去了可移植性,所以在包含頭文件時必須保持文件名的大小寫敏感。

假如存在兩個物理文件名分別為SynchronizedObject.h, yaml_parser.h的兩個文件。

反例:

// 路徑名、文件名大小寫與真實物理路徑、物理文件名稱不符
#include "CppUnit/Core/SynchronizedObject.h"
#include "YAML_Parser.h"

正例:

#include "cppunit/core/SynchronizedObject.h"
#include "yaml_parser.h"

最后,值得注意的是,團隊內(nèi)必須保持一致的命名風格。

分隔符

包含頭文件時,路徑分隔符一律使用Unix風格,拒絕使用Windows風格;即采用/而不是使用\分割路徑。

反例:

// 使用了Windows風格的路徑分割符
#include "cppunit\core\SynchronizedObject.h"

正例:

// 使用了Unix風格的路徑分割符
#include "cppunit/core/SynchronizedObject.h"

extern "C"

使用extern "C"時,不要包括include語句。

反例:

//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
    
#ifdef  __cplusplus
extern "C" {
#endif

// 錯誤地將include放在了extern "C"中
#include "oss_common.h"

void* oss_alloc(size_t);
void  oss_free(void*);

#ifdef  __cplusplus
}
#endif

#endif

正例:

//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
    
#include "oss_common.h"

#ifdef  __cplusplus
extern "C" {
#endif

void* oss_alloc(size_t);
void  oss_free(void*);

#ifdef  __cplusplus
}
#endif

#endif

兼容性

當以C提供實現(xiàn)時,頭文件中必須使用extern "C"聲明,以便支持C++的擴展。

反例:

// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8    

#include "oss_common.h"

void* oss_alloc(size_t);
void  oss_free(void*);

#endif

正例:

// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8    

#include "oss_common.h"

#ifdef  __cplusplus
extern "C" {
#endif

void* oss_alloc(size_t);
void  oss_free(void*);

#ifdef  __cplusplus
}
#endif

#endif

上帝頭文件

拒絕創(chuàng)建巨型頭文件,將所有實體聲明都放到頭文件中,而僅僅將外部依賴的實體聲明放到頭文件中。

信息隱藏

實現(xiàn)文件也是一種信息隱藏的慣用技術(shù),如果一些程序的實體不對外所依賴,則放在自己的實現(xiàn)文件中,一則可降低依賴關(guān)系,二則實現(xiàn)更好的信息隱藏。

對于上帝頭文件,其很多聲明和定義本來是不應該放到頭文件,而應該放會實現(xiàn)文件以便實現(xiàn)更好地信息隱藏。

編譯時依賴

巨型頭文件必然造成了巨大的編譯時依賴,不僅僅帶來巨大的編譯時開銷,更重要的是這樣的設(shè)計將太多的實現(xiàn)細節(jié)暴露給用戶,導致后續(xù)版本兼容性的問題,阻礙了頭文件進一步演進、修改、擴展的可能性,從而失去了軟件的可擴展性。

include順序依賴

不要認為提供一個大而全的頭文件會給你的用戶帶來方便,用戶因此而更加困擾。對于一個巨大的頭文件,其依賴關(guān)系很難一眼看清楚,其自滿足性很難得到保證,用戶在包含此頭文件時,還要關(guān)心頭文件之間的依賴關(guān)系,甚至關(guān)心include語句的順序,但這樣的代碼實現(xiàn)是及其脆弱的。

最佳實踐

自滿足驗證

為了驗證頭文件設(shè)計的自滿足原則,實現(xiàn)文件的第一條語句必然是包含其對應的頭文件。

反例:

// 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的函數(shù)(除overridevirtual析構(gòu)函數(shù)之外)都應該是private的,以保證按接口編程的良好設(shè)計原則。

反例:

// 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*);

     // 設(shè)計缺陷:本應該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函數(shù),除非性能報告指出此函數(shù)是性能的關(guān)鍵瓶頸。

C++語言將聲明和實現(xiàn)進行分離,程序員為此不得不在頭文件和實現(xiàn)文件中重復地對函數(shù)進行聲明。這是C/C++天生給我們的設(shè)計帶來的重復。這是一件痛苦的事情,驅(qū)使部分程序員直接將函數(shù)實現(xiàn)為inline。

inline函數(shù)的代碼作為一種不穩(wěn)定的內(nèi)部實現(xiàn)細節(jié),被放置在頭文件里,其變更所導致的大面積的重新編譯是個大概率事件,為改善微乎其微的函數(shù)調(diào)用性能與其相比將得不償失。

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

但需要注意在特殊的情況,可以將實現(xiàn)inline在頭文件中,因為為它們創(chuàng)建實現(xiàn)文件過于累贅和麻煩。

  • virtual析構(gòu)函數(shù)
  • 空的virtual函數(shù)實現(xiàn)
  • C++11default函數(shù)
鼓勵實現(xiàn)文件中inline

對于在編譯單元內(nèi)部定義的類而言,因為它的客戶數(shù)量是確定的,就是它本身。另外,由于它本來就定義在源代碼文件中,因此并沒有增加任何“物理耦合”。所以,對于這樣的類,我們大可以將其所有函數(shù)都實現(xiàn)為inline的,就像寫Java代碼那樣,Once & Only Once

以單態(tài)類的一種實現(xiàn)技術(shù)為例,講解編譯時依賴的解耦與匿名命名空間的使用。(首先,應該抵制單態(tài)設(shè)計的誘惑,單態(tài)其本質(zhì)是面向?qū)ο蠹夹g(shù)中全局變量的替代品。濫用單態(tài)模式,猶如濫用全局變量,是一種典型的設(shè)計壞味道。只有確定在系統(tǒng)中唯一存在的概念,才能使用單態(tài)模式)。

實現(xiàn)單態(tài),需要對系統(tǒng)中唯一存在的概念進行封裝;但這個概念往往具有巨大的數(shù)據(jù)結(jié)構(gòu),如果將其聲明在頭文件中,無疑造成很大的編譯時依賴。

反例:

// 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未列出所有代碼實現(xiàn),但我們知道NetworkElement擁有巨大的數(shù)據(jù)結(jié)構(gòu),上述設(shè)計導致所有包含NetworkElementRepository的頭文件都被NetworkElement所間接污染。

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

正例:

// 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

其實現(xiàn)文件包含NetworkElement.h,將對其的依賴控制在本編譯單元內(nèi)部。

// 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類的依賴是非常明確的,僅本編譯單元內(nèi),所有可以直接進行inline,從而簡化了很多實現(xiàn)。

匿名namespace

匿名namespace的存在常常被人遺忘,但它的確是一個利器。匿名namespace的存在,使得所有受限于編譯單元內(nèi)的實體擁有了明確的處所。

自此之后,所有C風格并局限于編譯單元內(nèi)的static函數(shù)和變量;以及類似Java中常見的private static的提取函數(shù)將常常被匿名namespace替代。

請記住匿名命名空間也是一種重要的信息隱藏技術(shù)。在實現(xiàn)文件中提倡使用匿名namespace, 以避免潛在的命名沖突。

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

struct VS. class

除了名字不同之外,classstruct唯一的差別是:默認可見性。這體現(xiàn)在定義和繼承時。struct在定義一個成員,或者繼承時,如果不指明,則默認為public,而class則默認為private。

但這些都不是重點,重點在于定義接口和繼承時,冗余public修飾符總讓人不舒服。簡單設(shè)計四原則告訴告訴我們,所有冗余的代碼都應該被剔除。

但很多人會認為structC遺留問題,應該避免使用。但這不是問題,我們不應該否認在寫C++程序時,依然在使用著很多C語言遺留的特性。關(guān)鍵在于,我們使用的是C語言中能給設(shè)計帶來好處的特性,何樂而不為呢?

正例:

// 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的特性正好適合于將數(shù)據(jù)結(jié)構(gòu)捧為神物的程序員,它們常常將數(shù)據(jù)結(jié)構(gòu)置于類聲明的最前面。)

不管你信仰那一個流派,切忌不能混合使用classstruct。在大量使用前導聲明的情況下,一旦一個使用struct的類改為class,所有的前置聲明都需要修改。

萬惡的struct tag

定義C風格的結(jié)構(gòu)體時,struct tag徹底抑制了結(jié)構(gòu)體前置聲明的可能性,從而阻礙了編譯優(yōu)化的空間。

反例:

// 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并為結(jié)構(gòu)體前置聲明提供便利,如下解法是最合適的。

正例:

// 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,則定義一個結(jié)構(gòu)體的指針,必須顯式地加上struct關(guān)鍵字:struct T_Cell *pcell,而C++沒有這方面的要求。

PIMPL

如果性能不是關(guān)鍵問題,考慮使用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的依賴,將其依賴控制在本編譯單元內(nèi)部。

template

編譯時依賴

當選擇模板時,不得不將其實現(xiàn)定義在頭文件中。當編譯時依賴開銷非常大時,編譯模板將成為一種負擔。設(shè)法降低編譯時依賴,不僅僅為了縮短編譯時間,更重要的是為了得到一個低耦合的實現(xiàn)。

反例:

// 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

為了實現(xiàn)模板函數(shù)send,將OSS的一些實現(xiàn)細節(jié)暴露到了頭文件中,包含OssSender.h的所有文件將無意識地產(chǎn)生了對OSS頭文件的依賴。

提取一個私有的send函數(shù),并將對OSS的依賴移入到OssSender.cpp中,對PID依賴通過前置聲明解除,最終實現(xiàn)如代碼所示。

正例:

// 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

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

顯式模板實例化

模板的編譯時依賴存在兩個基本模型:包含模型,export模型。export模型受編譯技術(shù)實現(xiàn)的挑戰(zhàn),最終被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

如上的設(shè)計,泛型類Quantity的實現(xiàn)都放在了頭文件,不穩(wěn)定的實現(xiàn)細節(jié),例如計算amountInBaseUnit的算法變化等因素,將導致包含LengthVolume的所有源文件都需要重新編譯。

更重要的是,因為LengthUnit, VolumeUnit頭文件的包含,如果因需求變化需要增加支持的單位,將間接導致了包含LengthVolume的所有源文件也需要重新編譯。

如何控制和隔離Quantity, LengthUnit, VolumeUnit變化的蔓延,而避免大部分的客戶代碼重新編譯,從而與客戶徹底解偶呢?可以通過顯式模板實例化將模板實現(xiàn)從頭文件中剝離出去,從而避免了不必要的依賴。

正例:

// 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產(chǎn)生依賴; 特殊地,Length.cpp沒有產(chǎn)生對Length.h的依賴,相反對Quantity.tcc產(chǎn)生了依賴。

另外,Length.hLengthUnit的依賴關(guān)系也簡化為聲明依賴,而對其真正的編譯時依賴,也控制在模板實例化的時刻,即在Length.cpp內(nèi)部。

LenghtUnit, VolumeUnit的變化,及其Quantity.tcc實現(xiàn)細節(jié)的變化,被完全地控制在Length.cpp, Volume.cpp內(nèi)部。

子類化優(yōu)于typedef/using

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

另外,如果Quantity存在virtual函數(shù)時,Length還有進一步擴展Quantity的可能性,從而使設(shè)計提供了更大的靈活性。

反例:

// 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

附錄:實用宏

為了提高代碼的表現(xiàn)力,規(guī)范中使用了一部分實用的宏定義。

// base/Default.h
#ifndef GKOQWPRT_1038935_NCVBNMZHJS_8909603
#define GKOQWPRT_1038935_NCVBNMZHJS_8909603

namespace details
{
   template <typename T>
   struct DefaultValue
   {
      static T value()
      {
         return T();
      }
   };

   template <typename T>
   struct DefaultValue<T*>
   {
       static T* value()
       {
           return 0;
       }
   };

   template <typename T>
   struct DefaultValue<const T*>
   {
       static T* value()
       {
           return 0;
       }
   };

   template <>
   struct DefaultValue<void>
   {
      static void value()
      {
      }
   };
}

#define DEFAULT(type, method)  \
    virtual type method { return ::details::DefaultValue<type>::value(); }

#endif

DEFAULT對于定義空實現(xiàn)的virtual函數(shù)非常方便。需要注意的是,所有計算都是發(fā)生在編譯時的。

// base/Keywords.h
#ifndef H16274882_9153_4DB2_A2E2_F23D4CCB9381
#define H16274882_9153_4DB2_A2E2_F23D4CCB9381

#include "base/Config.h"
#include "base/Default.h"

#define ABSTRACT(...) virtual __VA_ARGS__ = 0

#if __SUPPORT_VIRTUAL_OVERRIDE
#   define OVERRIDE(...) virtual __VA_ARGS__ override
#else
#   define OVERRIDE(...) virtual __VA_ARGS__
#endif

#define EXTENDS(...) , ##__VA_ARGS__
#define IMPLEMENTS(...) EXTENDS(__VA_ARGS__)

#endif

Config.h提供了編譯器支持C++11特性的配置信息。ABSTRACT, OVERRIDE, EXTENDS, IMPLEMENTS等關(guān)鍵字,使得Java程序員也能看懂C++的代碼,也極大地改善了C++的表現(xiàn)力。

// base/Role.h
#ifndef HF95EF112_D6C6_4DB0_8C1A_BE5A6CF8E3F1
#define HF95EF112_D6C6_4DB0_8C1A_BE5A6CF8E3F1
#include <base/Keywords.h>

namespace details
{
   template <typename T>
   struct Role
   {
      virtual ~Role() {}
   };
}

#define DEFINE_ROLE(type) struct type : ::details::Role<type>

#endif

通過DEFINE_ROLE的宏定義來實現(xiàn)對接口的定義,從而可以消除子類對虛擬析構(gòu)函數(shù)的重復實現(xiàn)。

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

相關(guān)閱讀更多精彩內(nèi)容

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