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 继承的语义:

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 继承时:

  1. 首先尝试复合:它更简单、更灵活、更容易理解
  2. 只有在以下情况使用 private 继承
    • 需要访问基类的 protected 成员
    • 需要重写基类的 virtual 函数
    • 需要 empty base optimization(且你确实在意那一点内存)
  3. 记住:private 继承纯粹是一种实现技术,在设计层面没有意义

参考阅读:

  • 《Effective C++》Scott Meyers,条款 39
  • 《C++ Primer》Stanley B. Lippman 等,关于继承的章节
  • 《STL 源码剖析》侯捷,关于 allocator 和 EBO 的实现

系列预告: 下一篇将深入解析条款 40------明智而审慎地使用多重继承,探讨多重继承的复杂性、菱形继承问题,以及 virtual 继承的成本与收益。


如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。

相关推荐
基德爆肝c语言1 小时前
MySQL表的操作
前端·数据库·mysql
TDengine (老段)1 小时前
TDengine 连接算子 — Inner/Outer/ASOF/Window Join 的实现与使用
大数据·数据库·物联网·哈希算法·时序数据库·tdengine·涛思数据
不想写代码的星星1 小时前
伪共享:逻辑无共享,物理打成狗
c++
轻刀快马2 小时前
跨越软硬件的共鸣(二):从 Cache 写策略看 Redis 与 DB 的一致性博弈
java·开发语言·redis·计算机组成原理
折哥的程序人生 · 物流技术专研2 小时前
Java 23 种设计模式:从踩坑到精通 | 装饰器模式 —— 比继承更灵活的扩展方式,你用过吗?
java·装饰器模式·java面试·结构型模式·java设计模式·javaio·从踩坑到精通
lili00122 小时前
2026 企业 AI 选型新范式:OpenRouter Fusion 证明多模型融合性价比远超单模型,企业该如何重构技术栈? - 微元算力(weytoken)
java·人工智能·python·重构·ai编程
Keano Reurink2 小时前
搜索API与GSC数据对比:发现数据盲区
数据库·python·数据挖掘
shushangyun_2 小时前
汽车服务行业B2B平台+AI解决方案哪家专业:2026年最新测评
java·运维·网络·数据库·人工智能·汽车
A.说学逗唱的Coke2 小时前
【大模型专题】Spring AI Alibaba × Skill 整合实战:让 AI 真正“会干活
java·人工智能·spring