Effective C 条款39明智而审慎地使用 private 继承本篇为《Effective C改善程序与设计的 55 个具体做法》读书笔记系列第 39 篇。开篇引言在 C 中public 继承表示is-a关系这是面向对象设计的基石。但 C 还提供了另一种继承方式private 继承。与 public 继承不同private 继承不表示 is-a而是表示is-implemented-in-terms-of根据某物实现出。Scott Meyers 在条款 39 中提醒我们private 继承通常比复合的级别低应当尽可能使用复合只有在特定场景下才使用 private 继承。本文将深入探讨 private 继承的语义、与复合的对比以及它的正当使用场景。核心概念private 继承的语义private 继承不是 is-a让我们通过一个直观的例子来理解 private 继承的语义#includeiostreamclassPerson{public:voideat()const{std::coutPerson is eatingstd::endl;}voidsleep()const{std::coutPerson is sleepingstd::endl;}};classStudent:privatePerson{// private 继承public:voidstudy()const{std::coutStudent is studyingstd::endl;}// 如果需要可以公开某些基类功能usingPerson::eat;// 将 eat 公开};voidtest(){Student s;s.eat();// OKeat 被 using 声明公开了s.study();// OK// s.sleep(); // 错误sleep 是 private}// 更关键的是voideatAsPerson(constPersonp){p.eat();}// eatAsPerson(s); // 错误Student 不是 Personprivate 继承的关键特性特性public 继承private 继承语义is-ais-implemented-in-terms-of基类 public 成员在派生类中仍为 public在派生类中变为 private基类 protected 成员在派生类中仍为 protected在派生类中变为 private隐式转换派生类可隐式转为基类不允许隐式转换设计层面意义有概念关系无纯实现技术private 继承 vs 复合场景 1需要重写 virtual 函数这是使用 private 继承最常见的正当理由#includeiostream#includememoryclassTimer{public:explicitTimer(inttickFrequency):frequency(tickFrequency){}virtualvoidonTick()const{std::coutTimer tick!std::endl;}voidstart()const{// 模拟定时器触发onTick();}virtual~Timer()default;private:intfrequency;};// 方案一使用 private 继承classWidget:privateTimer{public:Widget():Timer(1000){}voidenableTicking(boolenable){tickingEnabledenable;}private:virtualvoidonTick()constoverride{if(tickingEnabled){std::coutWidget processing tick...std::endl;// 执行 Widget 特定的定时任务}}booltickingEnabledtrue;};// 方案二使用复合更灵活但代码更多classWidgetWithComposition{private:classWidgetTimer:publicTimer{public:explicitWidgetTimer(WidgetWithComposition*widget):Timer(1000),widget(widget){}virtualvoidonTick()constoverride{if(widgetwidget-tickingEnabled){widget-onTick();}}private:WidgetWithComposition*widget;};public:WidgetWithComposition():timer(std::make_uniqueWidgetTimer(this)){}voidenableTicking(boolenable){tickingEnabledenable;}private:voidonTick()const{std::coutWidget (composition) processing tick...std::endl;}std::unique_ptrWidgetTimertimer;booltickingEnabledtrue;friendclassWidgetTimer;// 允许 WidgetTimer 访问私有成员};两种方案的对比特性private 继承复合代码复杂度简单较复杂需要嵌套类编译依赖高Widget 依赖 Timer 的定义低可以用指针前向声明派生类能否重写 onTick能不能WidgetTimer 是 private灵活性低高推荐程度一般优先场景 2需要访问 protected 成员#includeiostreamclassBase{public:voidpublicMethod()const{}protected:voidprotectedMethod()const{std::coutProtected method calledstd::endl;}intprotectedData42;};// 需要访问 Base 的 protected 成员classDerived:privateBase{public:voiddoSomething(){protectedMethod();// OK可以访问 protected 成员std::coutData: protectedDatastd::endl;}};// 用复合无法实现除非修改 Base 的设计classDerivedWithComposition{public:voiddoSomething(){// base.protectedMethod(); // 错误无法访问 protected 成员base.publicMethod();// OK只能访问 public 成员}private:Base base;};Empty Base Optimization空白基类最优化这是 private 继承最特殊也最有价值的应用场景。问题空类也占用空间#includeiostreamclassEmpty{// 没有非静态成员变量// 没有 virtual 函数// 没有 virtual 基类};classHoldsAnInt{private:intx;Empty e;// 理论上不需要内存};intmain(){std::coutsizeof(Empty): sizeof(Empty)std::endl;// 通常为 1std::coutsizeof(HoldsAnInt): sizeof(HoldsAnInt)std::endl;// 通常为 8不是 4return0;}原因C 标准规定独立的非附属对象必须有非零大小。编译器通常会为空对象插入一个char再加上对齐填充padding导致HoldsAnInt的大小变成sizeof(int) 对齐填充。解决方案private 继承实现 EBO#includeiostreamclassEmpty{// 空类};// 使用 private 继承替代成员变量classHoldsAnIntOptimized:privateEmpty{private:intx;};intmain(){std::coutsizeof(HoldsAnIntOptimized): sizeof(HoldsAnIntOptimized)std::endl;// 通常为 4Empty 基类不占用额外空间return0;}EBO 的实际应用STL 中的函数对象#includeiostream#includefunctional// 自定义比较器空类structMyLess{booloperator()(inta,intb)const{returnab;}// 没有成员变量典型的空类};// STL 中的 std::set 使用比较器templatetypenameT,typenameComparestd::lessTclassOptimizedSet{// 如果 Compare 是空类使用继承优化// 实际 STL 实现类似这样private:structNode{T value;Node*left;Node*right;};Node*root;// 不使用Compare comp; // 会多占用空间// 而是使用 private 继承如果 Compare 是空类};// 实际验证classSetWithMember{int*root;MyLess comp;// 成员变量};classSetWithInheritance:privateMyLess{int*root;};intmain(){std::coutsizeof(SetWithMember): sizeof(SetWithMember)std::endl;std::coutsizeof(SetWithInheritance): sizeof(SetWithInheritance)std::endl;// 在 64 位系统上// SetWithMember: 16 (8 8对齐)// SetWithInheritance: 8 (EBO 生效)return0;}EBO 的限制限制说明仅适用于单一继承多继承下的 EBO 不一定有效基类必须真正为空不能包含非静态成员变量、virtual 函数、virtual 基类编译器相关大多数现代编译器支持但不是标准强制要求实际应用场景场景 1自定义分配器Allocator#includeiostream#includememory// 状态less分配器空类templatetypenameTclassStatelessAllocator{public:usingvalue_typeT;T*allocate(std::size_t n){returnstatic_castT*(::operatornew(n*sizeof(T)));}voiddeallocate(T*p,std::size_t){::operatordelete(p);}// 没有成员变量};// 使用 private 继承实现 EBOtemplatetypenameT,typenameAllocatorStatelessAllocatorTclassCustomVector:privateAllocator{// EBO 可能生效public:explicitCustomVector(std::size_t size0):data(size0?Allocator::allocate(size):nullptr),sz(size),cap(size){}~CustomVector(){if(data){Allocator::deallocate(data,cap);}}Toperator[](std::size_t index){returndata[index];}std::size_tsize()const{returnsz;}private:T*data;std::size_t sz;std::size_t cap;};// 验证 EBOclassVectorWithMember{int*data;std::size_t sz;std::size_t cap;StatelessAllocatorintalloc;// 额外 1 字节 对齐填充};intmain(){std::coutsizeof(CustomVectorint): sizeof(CustomVectorint)std::endl;std::coutsizeof(VectorWithMember): sizeof(VectorWithMember)std::endl;return0;}场景 2策略模式的编译期版本#includeiostream// 日志策略空类classConsoleLogger{public:voidlog(conststd::stringmessage)const{std::cout[CONSOLE] messagestd::endl;}};classFileLogger{public:voidlog(conststd::stringmessage)const{// 模拟文件日志std::cout[FILE] messagestd::endl;}};// 使用 private 继承 CRTP 模式templatetypenameDerivedclassLoggerMixin:privateDerived{public:voidlogMessage(conststd::stringmsg){// 访问基类的 log 方法this-log(msg);}};classApplication:publicLoggerMixinConsoleLogger{public:voidrun(){logMessage(Application started);// 执行业务逻辑logMessage(Application finished);}};常见误区与解决方案误区 1“private 继承和复合完全一样”// 不完全一样classA{};classB1:privateA{};// private 继承classB2{A a;};// 复合// B1 可以访问 A 的 protected 成员// B2 不能访问 A 的 protected 成员// B1 可以重写 A 的 virtual 函数// B2 不能重写 A 的 virtual 函数误区 2“为了 EBO 到处使用 private 继承”// 不好的做法过度使用 private 继承classWidget:privatestd::string{// Widget is-a string不public:// ...};// 好的做法除非真的需要 EBO否则使用复合classWidget{private:std::string label;// Widget has-a label};误区 3“private 继承可以替代所有复合”// 错误private 继承破坏了封装classBadDesign:privatestd::vectorint{public:// 用户可能误以为这是 vector 的一种};// 正确复合明确表达 has-a 关系classGoodDesign{public:voidadd(intvalue){data.push_back(value);}// 只暴露需要的接口std::size_tsize()const{returndata.size();}private:std::vectorintdata;};决策流程图需要复用代码 ├── 需要访问 protected 成员 │ └── 是 → 考虑 private 继承 ├── 需要重写 virtual 函数 │ └── 是 → 考虑 private 继承或复合 嵌套类 ├── 需要 EBO空类最优化 │ └── 是 → 考虑 private 继承 └── 以上都不是 └── 使用复合推荐总结核心要点要点说明private 继承语义is-implemented-in-terms-of不是 is-a优先使用复合复合更容易理解更灵活private 继承的正当理由访问 protected 成员、重写 virtual 函数、EBOEBO 的价值对内存敏感的场景如 STL 实现很重要记忆口诀Private 继承非 is-a实现细节藏其下。复合优先继承后protected 访问才用它。空类最优化空间审慎使用莫滥用。条款 39 的核心建议明智而审慎地使用 private 继承。当你考虑使用 private 继承时首先尝试复合它更简单、更灵活、更容易理解只有在以下情况使用 private 继承需要访问基类的 protected 成员需要重写基类的 virtual 函数需要 empty base optimization且你确实在意那一点内存记住private 继承纯粹是一种实现技术在设计层面没有意义参考阅读《Effective C》Scott Meyers条款 39《C Primer》Stanley B. Lippman 等关于继承的章节《STL 源码剖析》侯捷关于 allocator 和 EBO 的实现系列预告下一篇将深入解析条款 40——明智而审慎地使用多重继承探讨多重继承的复杂性、菱形继承问题以及 virtual 继承的成本与收益。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。
Effective C++ 条款39:明智而审慎地使用 private 继承
Effective C 条款39明智而审慎地使用 private 继承本篇为《Effective C改善程序与设计的 55 个具体做法》读书笔记系列第 39 篇。开篇引言在 C 中public 继承表示is-a关系这是面向对象设计的基石。但 C 还提供了另一种继承方式private 继承。与 public 继承不同private 继承不表示 is-a而是表示is-implemented-in-terms-of根据某物实现出。Scott Meyers 在条款 39 中提醒我们private 继承通常比复合的级别低应当尽可能使用复合只有在特定场景下才使用 private 继承。本文将深入探讨 private 继承的语义、与复合的对比以及它的正当使用场景。核心概念private 继承的语义private 继承不是 is-a让我们通过一个直观的例子来理解 private 继承的语义#includeiostreamclassPerson{public:voideat()const{std::coutPerson is eatingstd::endl;}voidsleep()const{std::coutPerson is sleepingstd::endl;}};classStudent:privatePerson{// private 继承public:voidstudy()const{std::coutStudent is studyingstd::endl;}// 如果需要可以公开某些基类功能usingPerson::eat;// 将 eat 公开};voidtest(){Student s;s.eat();// OKeat 被 using 声明公开了s.study();// OK// s.sleep(); // 错误sleep 是 private}// 更关键的是voideatAsPerson(constPersonp){p.eat();}// eatAsPerson(s); // 错误Student 不是 Personprivate 继承的关键特性特性public 继承private 继承语义is-ais-implemented-in-terms-of基类 public 成员在派生类中仍为 public在派生类中变为 private基类 protected 成员在派生类中仍为 protected在派生类中变为 private隐式转换派生类可隐式转为基类不允许隐式转换设计层面意义有概念关系无纯实现技术private 继承 vs 复合场景 1需要重写 virtual 函数这是使用 private 继承最常见的正当理由#includeiostream#includememoryclassTimer{public:explicitTimer(inttickFrequency):frequency(tickFrequency){}virtualvoidonTick()const{std::coutTimer tick!std::endl;}voidstart()const{// 模拟定时器触发onTick();}virtual~Timer()default;private:intfrequency;};// 方案一使用 private 继承classWidget:privateTimer{public:Widget():Timer(1000){}voidenableTicking(boolenable){tickingEnabledenable;}private:virtualvoidonTick()constoverride{if(tickingEnabled){std::coutWidget processing tick...std::endl;// 执行 Widget 特定的定时任务}}booltickingEnabledtrue;};// 方案二使用复合更灵活但代码更多classWidgetWithComposition{private:classWidgetTimer:publicTimer{public:explicitWidgetTimer(WidgetWithComposition*widget):Timer(1000),widget(widget){}virtualvoidonTick()constoverride{if(widgetwidget-tickingEnabled){widget-onTick();}}private:WidgetWithComposition*widget;};public:WidgetWithComposition():timer(std::make_uniqueWidgetTimer(this)){}voidenableTicking(boolenable){tickingEnabledenable;}private:voidonTick()const{std::coutWidget (composition) processing tick...std::endl;}std::unique_ptrWidgetTimertimer;booltickingEnabledtrue;friendclassWidgetTimer;// 允许 WidgetTimer 访问私有成员};两种方案的对比特性private 继承复合代码复杂度简单较复杂需要嵌套类编译依赖高Widget 依赖 Timer 的定义低可以用指针前向声明派生类能否重写 onTick能不能WidgetTimer 是 private灵活性低高推荐程度一般优先场景 2需要访问 protected 成员#includeiostreamclassBase{public:voidpublicMethod()const{}protected:voidprotectedMethod()const{std::coutProtected method calledstd::endl;}intprotectedData42;};// 需要访问 Base 的 protected 成员classDerived:privateBase{public:voiddoSomething(){protectedMethod();// OK可以访问 protected 成员std::coutData: protectedDatastd::endl;}};// 用复合无法实现除非修改 Base 的设计classDerivedWithComposition{public:voiddoSomething(){// base.protectedMethod(); // 错误无法访问 protected 成员base.publicMethod();// OK只能访问 public 成员}private:Base base;};Empty Base Optimization空白基类最优化这是 private 继承最特殊也最有价值的应用场景。问题空类也占用空间#includeiostreamclassEmpty{// 没有非静态成员变量// 没有 virtual 函数// 没有 virtual 基类};classHoldsAnInt{private:intx;Empty e;// 理论上不需要内存};intmain(){std::coutsizeof(Empty): sizeof(Empty)std::endl;// 通常为 1std::coutsizeof(HoldsAnInt): sizeof(HoldsAnInt)std::endl;// 通常为 8不是 4return0;}原因C 标准规定独立的非附属对象必须有非零大小。编译器通常会为空对象插入一个char再加上对齐填充padding导致HoldsAnInt的大小变成sizeof(int) 对齐填充。解决方案private 继承实现 EBO#includeiostreamclassEmpty{// 空类};// 使用 private 继承替代成员变量classHoldsAnIntOptimized:privateEmpty{private:intx;};intmain(){std::coutsizeof(HoldsAnIntOptimized): sizeof(HoldsAnIntOptimized)std::endl;// 通常为 4Empty 基类不占用额外空间return0;}EBO 的实际应用STL 中的函数对象#includeiostream#includefunctional// 自定义比较器空类structMyLess{booloperator()(inta,intb)const{returnab;}// 没有成员变量典型的空类};// STL 中的 std::set 使用比较器templatetypenameT,typenameComparestd::lessTclassOptimizedSet{// 如果 Compare 是空类使用继承优化// 实际 STL 实现类似这样private:structNode{T value;Node*left;Node*right;};Node*root;// 不使用Compare comp; // 会多占用空间// 而是使用 private 继承如果 Compare 是空类};// 实际验证classSetWithMember{int*root;MyLess comp;// 成员变量};classSetWithInheritance:privateMyLess{int*root;};intmain(){std::coutsizeof(SetWithMember): sizeof(SetWithMember)std::endl;std::coutsizeof(SetWithInheritance): sizeof(SetWithInheritance)std::endl;// 在 64 位系统上// SetWithMember: 16 (8 8对齐)// SetWithInheritance: 8 (EBO 生效)return0;}EBO 的限制限制说明仅适用于单一继承多继承下的 EBO 不一定有效基类必须真正为空不能包含非静态成员变量、virtual 函数、virtual 基类编译器相关大多数现代编译器支持但不是标准强制要求实际应用场景场景 1自定义分配器Allocator#includeiostream#includememory// 状态less分配器空类templatetypenameTclassStatelessAllocator{public:usingvalue_typeT;T*allocate(std::size_t n){returnstatic_castT*(::operatornew(n*sizeof(T)));}voiddeallocate(T*p,std::size_t){::operatordelete(p);}// 没有成员变量};// 使用 private 继承实现 EBOtemplatetypenameT,typenameAllocatorStatelessAllocatorTclassCustomVector:privateAllocator{// EBO 可能生效public:explicitCustomVector(std::size_t size0):data(size0?Allocator::allocate(size):nullptr),sz(size),cap(size){}~CustomVector(){if(data){Allocator::deallocate(data,cap);}}Toperator[](std::size_t index){returndata[index];}std::size_tsize()const{returnsz;}private:T*data;std::size_t sz;std::size_t cap;};// 验证 EBOclassVectorWithMember{int*data;std::size_t sz;std::size_t cap;StatelessAllocatorintalloc;// 额外 1 字节 对齐填充};intmain(){std::coutsizeof(CustomVectorint): sizeof(CustomVectorint)std::endl;std::coutsizeof(VectorWithMember): sizeof(VectorWithMember)std::endl;return0;}场景 2策略模式的编译期版本#includeiostream// 日志策略空类classConsoleLogger{public:voidlog(conststd::stringmessage)const{std::cout[CONSOLE] messagestd::endl;}};classFileLogger{public:voidlog(conststd::stringmessage)const{// 模拟文件日志std::cout[FILE] messagestd::endl;}};// 使用 private 继承 CRTP 模式templatetypenameDerivedclassLoggerMixin:privateDerived{public:voidlogMessage(conststd::stringmsg){// 访问基类的 log 方法this-log(msg);}};classApplication:publicLoggerMixinConsoleLogger{public:voidrun(){logMessage(Application started);// 执行业务逻辑logMessage(Application finished);}};常见误区与解决方案误区 1“private 继承和复合完全一样”// 不完全一样classA{};classB1:privateA{};// private 继承classB2{A a;};// 复合// B1 可以访问 A 的 protected 成员// B2 不能访问 A 的 protected 成员// B1 可以重写 A 的 virtual 函数// B2 不能重写 A 的 virtual 函数误区 2“为了 EBO 到处使用 private 继承”// 不好的做法过度使用 private 继承classWidget:privatestd::string{// Widget is-a string不public:// ...};// 好的做法除非真的需要 EBO否则使用复合classWidget{private:std::string label;// Widget has-a label};误区 3“private 继承可以替代所有复合”// 错误private 继承破坏了封装classBadDesign:privatestd::vectorint{public:// 用户可能误以为这是 vector 的一种};// 正确复合明确表达 has-a 关系classGoodDesign{public:voidadd(intvalue){data.push_back(value);}// 只暴露需要的接口std::size_tsize()const{returndata.size();}private:std::vectorintdata;};决策流程图需要复用代码 ├── 需要访问 protected 成员 │ └── 是 → 考虑 private 继承 ├── 需要重写 virtual 函数 │ └── 是 → 考虑 private 继承或复合 嵌套类 ├── 需要 EBO空类最优化 │ └── 是 → 考虑 private 继承 └── 以上都不是 └── 使用复合推荐总结核心要点要点说明private 继承语义is-implemented-in-terms-of不是 is-a优先使用复合复合更容易理解更灵活private 继承的正当理由访问 protected 成员、重写 virtual 函数、EBOEBO 的价值对内存敏感的场景如 STL 实现很重要记忆口诀Private 继承非 is-a实现细节藏其下。复合优先继承后protected 访问才用它。空类最优化空间审慎使用莫滥用。条款 39 的核心建议明智而审慎地使用 private 继承。当你考虑使用 private 继承时首先尝试复合它更简单、更灵活、更容易理解只有在以下情况使用 private 继承需要访问基类的 protected 成员需要重写基类的 virtual 函数需要 empty base optimization且你确实在意那一点内存记住private 继承纯粹是一种实现技术在设计层面没有意义参考阅读《Effective C》Scott Meyers条款 39《C Primer》Stanley B. Lippman 等关于继承的章节《STL 源码剖析》侯捷关于 allocator 和 EBO 的实现系列预告下一篇将深入解析条款 40——明智而审慎地使用多重继承探讨多重继承的复杂性、菱形继承问题以及 virtual 继承的成本与收益。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。