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 继承的语义:
cpp
#include <iostream>
class Person {
public:
void eat() const {
std::cout << "Person is eating" << std::endl;
}
void sleep() const {
std::cout << "Person is sleeping" << std::endl;
}
};
class Student : private Person { // private 继承
public:
void study() const {
std::cout << "Student is studying" << std::endl;
}
// 如果需要,可以公开某些基类功能
using Person::eat; // 将 eat 公开
};
void test() {
Student s;
s.eat(); // OK:eat 被 using 声明公开了
s.study(); // OK
// s.sleep(); // 错误:sleep 是 private
}
// 更关键的是:
void eatAsPerson(const Person& p) {
p.eat();
}
// eatAsPerson(s); // 错误!Student 不是 Person!
private 继承的关键特性
| 特性 | public 继承 | private 继承 |
|---|---|---|
| 语义 | is-a | is-implemented-in-terms-of |
| 基类 public 成员 | 在派生类中仍为 public | 在派生类中变为 private |
| 基类 protected 成员 | 在派生类中仍为 protected | 在派生类中变为 private |
| 隐式转换 | 派生类可隐式转为基类 | 不允许隐式转换 |
| 设计层面意义 | 有(概念关系) | 无(纯实现技术) |
private 继承 vs 复合
场景 1:需要重写 virtual 函数
这是使用 private 继承最常见的正当理由:
cpp
#include <iostream>
#include <memory>
class Timer {
public:
explicit Timer(int tickFrequency) : frequency(tickFrequency) {}
virtual void onTick() const {
std::cout << "Timer tick!" << std::endl;
}
void start() const {
// 模拟定时器触发
onTick();
}
virtual ~Timer() = default;
private:
int frequency;
};
// 方案一:使用 private 继承
class Widget : private Timer {
public:
Widget() : Timer(1000) {}
void enableTicking(bool enable) {
tickingEnabled = enable;
}
private:
virtual void onTick() const override {
if (tickingEnabled) {
std::cout << "Widget processing tick..." << std::endl;
// 执行 Widget 特定的定时任务
}
}
bool tickingEnabled = true;
};
// 方案二:使用复合(更灵活,但代码更多)
class WidgetWithComposition {
private:
class WidgetTimer : public Timer {
public:
explicit WidgetTimer(WidgetWithComposition* widget)
: Timer(1000), widget(widget) {}
virtual void onTick() const override {
if (widget && widget->tickingEnabled) {
widget->onTick();
}
}
private:
WidgetWithComposition* widget;
};
public:
WidgetWithComposition() : timer(std::make_unique<WidgetTimer>(this)) {}
void enableTicking(bool enable) {
tickingEnabled = enable;
}
private:
void onTick() const {
std::cout << "Widget (composition) processing tick..." << std::endl;
}
std::unique_ptr<WidgetTimer> timer;
bool tickingEnabled = true;
friend class WidgetTimer; // 允许 WidgetTimer 访问私有成员
};
两种方案的对比
| 特性 | private 继承 | 复合 |
|---|---|---|
| 代码复杂度 | 简单 | 较复杂(需要嵌套类) |
| 编译依赖 | 高(Widget 依赖 Timer 的定义) | 低(可以用指针前向声明) |
| 派生类能否重写 onTick | 能 | 不能(WidgetTimer 是 private) |
| 灵活性 | 低 | 高 |
| 推荐程度 | 一般 | 优先 |
场景 2:需要访问 protected 成员
cpp
#include <iostream>
class Base {
public:
void publicMethod() const {}
protected:
void protectedMethod() const {
std::cout << "Protected method called" << std::endl;
}
int protectedData = 42;
};
// 需要访问 Base 的 protected 成员
class Derived : private Base {
public:
void doSomething() {
protectedMethod(); // OK:可以访问 protected 成员
std::cout << "Data: " << protectedData << std::endl;
}
};
// 用复合无法实现(除非修改 Base 的设计)
class DerivedWithComposition {
public:
void doSomething() {
// base.protectedMethod(); // 错误:无法访问 protected 成员
base.publicMethod(); // OK:只能访问 public 成员
}
private:
Base base;
};
Empty Base Optimization(空白基类最优化)
这是 private 继承最特殊也最有价值的应用场景。
问题:空类也占用空间
cpp
#include <iostream>
class Empty {
// 没有非静态成员变量
// 没有 virtual 函数
// 没有 virtual 基类
};
class HoldsAnInt {
private:
int x;
Empty e; // 理论上不需要内存
};
int main() {
std::cout << "sizeof(Empty): " << sizeof(Empty) << std::endl; // 通常为 1
std::cout << "sizeof(HoldsAnInt): " << sizeof(HoldsAnInt) << std::endl; // 通常为 8(不是 4!)
return 0;
}
原因 :C++ 标准规定,独立的(非附属)对象必须有非零大小。编译器通常会为空对象插入一个 char,再加上对齐填充(padding),导致 HoldsAnInt 的大小变成 sizeof(int) + 对齐填充。
解决方案:private 继承实现 EBO
cpp
#include <iostream>
class Empty {
// 空类
};
// 使用 private 继承替代成员变量
class HoldsAnIntOptimized : private Empty {
private:
int x;
};
int main() {
std::cout << "sizeof(HoldsAnIntOptimized): " << sizeof(HoldsAnIntOptimized) << std::endl;
// 通常为 4!Empty 基类不占用额外空间
return 0;
}
EBO 的实际应用:STL 中的函数对象
cpp
#include <iostream>
#include <functional>
// 自定义比较器(空类)
struct MyLess {
bool operator()(int a, int b) const {
return a < b;
}
// 没有成员变量,典型的空类
};
// STL 中的 std::set 使用比较器
template <typename T, typename Compare = std::less<T>>
class OptimizedSet {
// 如果 Compare 是空类,使用继承优化
// 实际 STL 实现类似这样:
private:
struct Node {
T value;
Node* left;
Node* right;
};
Node* root;
// 不使用:Compare comp; // 会多占用空间
// 而是使用 private 继承(如果 Compare 是空类)
};
// 实际验证
class SetWithMember {
int* root;
MyLess comp; // 成员变量
};
class SetWithInheritance : private MyLess {
int* root;
};
int main() {
std::cout << "sizeof(SetWithMember): " << sizeof(SetWithMember) << std::endl;
std::cout << "sizeof(SetWithInheritance): " << sizeof(SetWithInheritance) << std::endl;
// 在 64 位系统上:
// SetWithMember: 16 (8 + 8,对齐)
// SetWithInheritance: 8 (EBO 生效!)
return 0;
}
EBO 的限制
| 限制 | 说明 |
|---|---|
| 仅适用于单一继承 | 多继承下的 EBO 不一定有效 |
| 基类必须真正为空 | 不能包含非静态成员变量、virtual 函数、virtual 基类 |
| 编译器相关 | 大多数现代编译器支持,但不是标准强制要求 |
实际应用场景
场景 1:自定义分配器(Allocator)
cpp
#include <iostream>
#include <memory>
// 状态less分配器(空类)
template <typename T>
class StatelessAllocator {
public:
using value_type = T;
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t) {
::operator delete(p);
}
// 没有成员变量
};
// 使用 private 继承实现 EBO
template <typename T, typename Allocator = StatelessAllocator<T>>
class CustomVector : private Allocator { // EBO 可能生效
public:
explicit CustomVector(std::size_t size = 0)
: data(size > 0 ? Allocator::allocate(size) : nullptr), sz(size), cap(size) {}
~CustomVector() {
if (data) {
Allocator::deallocate(data, cap);
}
}
T& operator[](std::size_t index) { return data[index]; }
std::size_t size() const { return sz; }
private:
T* data;
std::size_t sz;
std::size_t cap;
};
// 验证 EBO
class VectorWithMember {
int* data;
std::size_t sz;
std::size_t cap;
StatelessAllocator<int> alloc; // 额外 1 字节 + 对齐填充
};
int main() {
std::cout << "sizeof(CustomVector<int>): " << sizeof(CustomVector<int>) << std::endl;
std::cout << "sizeof(VectorWithMember): " << sizeof(VectorWithMember) << std::endl;
return 0;
}
场景 2:策略模式的编译期版本
cpp
#include <iostream>
// 日志策略(空类)
class ConsoleLogger {
public:
void log(const std::string& message) const {
std::cout << "[CONSOLE] " << message << std::endl;
}
};
class FileLogger {
public:
void log(const std::string& message) const {
// 模拟文件日志
std::cout << "[FILE] " << message << std::endl;
}
};
// 使用 private 继承 + CRTP 模式
template <typename Derived>
class LoggerMixin : private Derived {
public:
void logMessage(const std::string& msg) {
// 访问基类的 log 方法
this->log(msg);
}
};
class Application : public LoggerMixin<ConsoleLogger> {
public:
void run() {
logMessage("Application started");
// 执行业务逻辑
logMessage("Application finished");
}
};
常见误区与解决方案
误区 1:"private 继承和复合完全一样"
cpp
// 不完全一样!
class A {};
class B1 : private A {}; // private 继承
class B2 { A a; }; // 复合
// B1 可以访问 A 的 protected 成员
// B2 不能访问 A 的 protected 成员
// B1 可以重写 A 的 virtual 函数
// B2 不能重写 A 的 virtual 函数
误区 2:"为了 EBO 到处使用 private 继承"
cpp
// 不好的做法:过度使用 private 继承
class Widget : private std::string { // Widget is-a string?不!
public:
// ...
};
// 好的做法:除非真的需要 EBO,否则使用复合
class Widget {
private:
std::string label; // Widget has-a label
};
误区 3:"private 继承可以替代所有复合"
cpp
// 错误:private 继承破坏了封装
class BadDesign : private std::vector<int> {
public:
// 用户可能误以为这是 vector 的一种
};
// 正确:复合明确表达 has-a 关系
class GoodDesign {
public:
void add(int value) {
data.push_back(value);
}
// 只暴露需要的接口
std::size_t size() const { return data.size(); }
private:
std::vector<int> data;
};
决策流程图
需要复用代码?
├── 需要访问 protected 成员?
│ └── 是 → 考虑 private 继承
├── 需要重写 virtual 函数?
│ └── 是 → 考虑 private 继承(或复合 + 嵌套类)
├── 需要 EBO(空类最优化)?
│ └── 是 → 考虑 private 继承
└── 以上都不是?
└── 使用复合(推荐)
总结
核心要点
| 要点 | 说明 |
|---|---|
| private 继承语义 | is-implemented-in-terms-of,不是 is-a |
| 优先使用复合 | 复合更容易理解,更灵活 |
| private 继承的正当理由 | 访问 protected 成员、重写 virtual 函数、EBO |
| EBO 的价值 | 对内存敏感的场景(如 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 继承的成本与收益。
如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。