为了验证头文件设计的自知足原则,实现文件的第一条语句必然是包含其对应的头文件。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); } ...
override
和private
全部override
的函数(除override
的virtual
析构函数以外)都应该是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++
天生给咱们的设计带来的重复。这是一件痛苦的事情,驱使部分程序员直接将函数实现为inline
。ide
但inline
函数的代码做为一种不稳定的内部实现细节,被放置在头文件里,其变动所致使的大面积的从新编译是个大几率事件,为改善微乎其微的函数调用性能与其相比将得不偿失。函数
除非有相关profiling
性能测试报告,代表这部分关键的热点代码须要被放回头文件中。性能
但须要注意在特殊的状况,能够将实现inline
在头文件中,由于为它们建立实现文件过于累赘和麻烦。
virtual
析构函数
空的virtual
函数实现
C++11
的default
函数
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
除了名字不一样以外,class
和struct
惟一的差异是:默承认见性。这体如今定义和继承时。struct
在定义一个成员,或者继承时,若是不指明,则默认为public
,而class
则默认为private
。
但这些都不是重点,重点在于定义接口和继承时,冗余public
修饰符总让人不舒服。简单设计四原则告诉告诉咱们,全部冗余的代码都应该被剔除。
但不少人会认为struct
是C
遗留问题,应该避免使用。但这不是问题,咱们不该该否定在写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
的特性正好适合于将数据结构捧为神物的程序员,它们经常将数据结构置于类声明的最前面。)
无论你信仰那一个流派,切忌不能混合使用class
和struct
。在大量使用前导声明的状况下,一旦一个使用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
的算法变化等因素,将致使包含Length
或Volume
的全部源文件都须要从新编译。
更重要的是,由于LengthUnit, VolumeUnit
头文件的包含,若是因需求变化须要增长支持的单位,将间接致使了包含Length
或Volume
的全部源文件也须要从新编译。
如何控制和隔离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.h
对LengthUnit
的依赖关系也简化为声明依赖,而对其真正的编译时依赖,也控制在模板实例化的时刻,即在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