
- 設(shè)計原則
- 代碼規(guī)范
- 最佳實踐
- 附錄: 實用宏
設(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定義的實體 - 繼承
- 宏
inlinetemplate- 引用類內(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);
}
...
override和private
所有override的函數(shù)(除override的virtual析構(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++11的default函數(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
除了名字不同之外,class和struct唯一的差別是:默認可見性。這體現(xiàn)在定義和繼承時。struct在定義一個成員,或者繼承時,如果不指明,則默認為public,而class則默認為private。
但這些都不是重點,重點在于定義接口和繼承時,冗余public修飾符總讓人不舒服。簡單設(shè)計四原則告訴告訴我們,所有冗余的代碼都應該被剔除。
但很多人會認為struct是C遺留問題,應該避免使用。但這不是問題,我們不應該否認在寫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)置于類聲明的最前面。)
不管你信仰那一個流派,切忌不能混合使用class和struct。在大量使用前導聲明的情況下,一旦一個使用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的算法變化等因素,將導致包含Length或Volume的所有源文件都需要重新編譯。
更重要的是,因為LengthUnit, VolumeUnit頭文件的包含,如果因需求變化需要增加支持的單位,將間接導致了包含Length或Volume的所有源文件也需要重新編譯。
如何控制和隔離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.h對LengthUnit的依賴關(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)。