Effective C++ 条款38:通过复合塑模出 has-a 或 \“根据某物实现出\

Effective C++ 条款38:通过复合塑模出 has-a 或 "根据某物实现出"

本篇为《Effective C++:改善程序与设计的 55 个具体做法》读书笔记系列第 38 篇。

开篇引言

在 C++ 的面向对象设计中,复合(composition)继承(inheritance) 是两种最基本的代码复用机制。很多开发者习惯性地选择继承,因为"它看起来更自然"。然而,Scott Meyers 在条款 38 中提醒我们:复合往往比继承更合适 。复合可以表达两种重要的语义关系:在应用域中表示 has-a(有一个) ,在实现域中表示 is-implemented-in-terms-of(根据某物实现出)。理解这两种语义,是写出灵活、可维护代码的关键。

核心概念:复合的两种语义

1. 应用域的 has-a(有一个)

在应用域(application domain),复合表示一个对象拥有另一个对象作为其组成部分:

cpp 复制代码
#include <iostream>
#include <string>

class Address {
public:
    Address(const std::string& city, const std::string& street)
        : city(city), street(street) {}
    
    void display() const {
        std::cout << city << ", " << street << std::endl;
    }

private:
    std::string city;
    std::string street;
};

class Person {
public:
    Person(const std::string& name, const Address& addr)
        : name(name), address(addr) {}
    
    void displayInfo() const {
        std::cout << "Name: " << name << std::endl;
        std::cout << "Address: ";
        address.display();
    }

private:
    std::string name;
    Address address;  // Person has-a Address
};

在这个例子中:

  • Person has-a Address
  • 地址是人的一个组成部分
  • 人不是地址的一种,地址也不是人的一种

2. 实现域的 is-implemented-in-terms-of

在实现域(implementation domain),复合表示一个类根据另一个类实现出自己的功能:

cpp 复制代码
#include <iostream>
#include <list>
#include <algorithm>

// 使用 std::list 实现一个 Set(集合)
template <typename T>
class Set {
public:
    bool member(const T& item) const {
        return std::find(rep.begin(), rep.end(), item) != rep.end();
    }
    
    void insert(const T& item) {
        if (!member(item)) {
            rep.push_back(item);
        }
    }
    
    void remove(const T& item) {
        auto it = std::find(rep.begin(), rep.end(), item);
        if (it != rep.end()) {
            rep.erase(it);
        }
    }
    
    std::size_t size() const {
        return rep.size();
    }

private:
    std::list<T> rep;  // Set is-implemented-in-terms-of std::list
};

在这个例子中:

  • Set is-implemented-in-terms-of std::list
  • Set 不是一个 std::list,它只是用 std::list 来实现自己的功能
  • 这避免了 std::list 的所有接口暴露给 Set 的用户

为什么继承是错误的:Set vs list 的例子

让我们看看如果使用 public 继承会发生什么:

cpp 复制代码
#include <list>
#include <iostream>

// 错误的做法:Set is-a list?不!
template <typename T>
class BadSet : public std::list<T> {
public:
    void insert(const T& item) {
        if (!member(item)) {
            std::list<T>::push_back(item);
        }
    }
    
    bool member(const T& item) const {
        return std::find(this->begin(), this->end(), item) != this->end();
    }
};

void demonstrateProblem() {
    BadSet<int> s;
    s.insert(1);
    s.insert(2);
    s.insert(1);  // 不会重复插入,很好
    
    // 但是...用户可以直接调用 list 的方法!
    s.push_back(1);  // 糟糕!现在 Set 中有重复元素了!
    s.push_front(3); // 更糟糕!这违反了集合的语义!
    
    std::cout << "Set size: " << s.size() << std::endl;  // 可能包含重复元素
}

继承带来的问题

问题 说明
接口污染 std::list 的所有 public 接口都暴露给了 BadSet 的用户
语义破坏 push_backpush_front 等方法破坏了集合的不变性
Liskov 替换原则违反 BadSet 不能在所有需要 std::list 的地方正确工作
维护困难 基类的任何变化都可能影响派生类

正确的复合实现

cpp 复制代码
template <typename T>
class GoodSet {
public:
    bool member(const T& item) const {
        return std::find(rep.begin(), rep.end(), item) != rep.end();
    }
    
    void insert(const T& item) {
        if (!member(item)) {
            rep.push_back(item);
        }
    }
    
    void remove(const T& item) {
        auto it = std::find(rep.begin(), rep.end(), item);
        if (it != rep.end()) {
            rep.erase(it);
        }
    }
    
    std::size_t size() const {
        return rep.size();
    }
    
    // 只暴露需要的迭代器能力
    using iterator = typename std::list<T>::iterator;
    using const_iterator = typename std::list<T>::const_iterator;
    
    const_iterator begin() const { return rep.begin(); }
    const_iterator end() const { return rep.end(); }

private:
    std::list<T> rep;  // 完全控制内部实现
};

复合 vs 继承的对比

特性 复合(Composition) Public 继承
语义 has-a / is-implemented-in-terms-of is-a
接口暴露 完全控制,只暴露需要的接口 继承所有 public 接口
耦合度 低,只依赖实现细节 高,接口和实现都耦合
灵活性 高,可以在运行期替换组件 低,编译期固定
封装性 好,内部实现对外不可见 差,基类接口全部暴露
多态 不支持(除非通过指针/引用组合) 原生支持

实际应用场景

场景 1:汽车与引擎(has-a)

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>

class Engine {
public:
    enum Type { Gasoline, Diesel, Electric, Hybrid };
    
    Engine(Type type, int horsepower) 
        : type(type), horsepower(horsepower) {}
    
    void start() const {
        std::cout << "Engine starting... (" << horsepower << " HP)" << std::endl;
    }
    
    void stop() const {
        std::cout << "Engine stopping..." << std::endl;
    }

private:
    Type type;
    int horsepower;
};

class Transmission {
public:
    enum Type { Manual, Automatic, CVT };
    
    explicit Transmission(Type type) : type(type) {}
    
    void shift(int gear) const {
        std::cout << "Shifting to gear " << gear << std::endl;
    }

private:
    Type type;
};

class Car {
public:
    Car(const Engine& engine, const Transmission& transmission)
        : engine(engine), transmission(transmission) {}
    
    void start() {
        std::cout << "Starting car..." << std::endl;
        engine.start();
    }
    
    void drive(int gear) {
        transmission.shift(gear);
        std::cout << "Car is moving" << std::endl;
    }
    
    void stop() {
        std::cout << "Stopping car..." << std::endl;
        engine.stop();
    }

private:
    Engine engine;           // Car has-a Engine
    Transmission transmission; // Car has-a Transmission
};

void testCar() {
    Engine v8Engine(Engine::Gasoline, 450);
    Transmission autoTrans(Transmission::Automatic);
    
    Car sportsCar(v8Engine, autoTrans);
    sportsCar.start();
    sportsCar.drive(3);
    sportsCar.stop();
}

场景 2:线程安全的队列(is-implemented-in-terms-of)

cpp 复制代码
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>

template <typename T>
class ThreadSafeQueue {
public:
    void push(const T& item) {
        {
            std::lock_guard<std::mutex> lock(mutex);
            queue.push(item);
        }
        condition.notify_one();
    }
    
    std::optional<T> tryPop() {
        std::lock_guard<std::mutex> lock(mutex);
        if (queue.empty()) {
            return std::nullopt;
        }
        T item = queue.front();
        queue.pop();
        return item;
    }
    
    T pop() {
        std::unique_lock<std::mutex> lock(mutex);
        condition.wait(lock, [this] { return !queue.empty(); });
        T item = queue.front();
        queue.pop();
        return item;
    }
    
    bool empty() const {
        std::lock_guard<std::mutex> lock(mutex);
        return queue.empty();
    }
    
    std::size_t size() const {
        std::lock_guard<std::mutex> lock(mutex);
        return queue.size();
    }

private:
    std::queue<T> queue;              // is-implemented-in-terms-of std::queue
    mutable std::mutex mutex;         // 同步原语
    std::condition_variable condition; // 条件变量
};

场景 3:策略模式(运行时替换组件)

cpp 复制代码
#include <iostream>
#include <memory>
#include <vector>

class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) const = 0;
};

class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) const override {
        std::cout << "Sorting using QuickSort" << std::endl;
        quickSort(data, 0, data.size() - 1);
    }

private:
    void quickSort(std::vector<int>& data, int low, int high) const {
        if (low < high) {
            int pi = partition(data, low, high);
            quickSort(data, low, pi - 1);
            quickSort(data, pi + 1, high);
        }
    }
    
    int partition(std::vector<int>& data, int low, int high) const {
        int pivot = data[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (data[j] < pivot) {
                i++;
                std::swap(data[i], data[j]);
            }
        }
        std::swap(data[i + 1], data[high]);
        return i + 1;
    }
};

class MergeSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) const override {
        std::cout << "Sorting using MergeSort" << std::endl;
        if (data.size() > 1) {
            mergeSort(data, 0, data.size() - 1);
        }
    }

private:
    void mergeSort(std::vector<int>& data, int left, int right) const {
        if (left < right) {
            int mid = left + (right - left) / 2;
            mergeSort(data, left, mid);
            mergeSort(data, mid + 1, right);
            merge(data, left, mid, right);
        }
    }
    
    void merge(std::vector<int>& data, int left, int mid, int right) const {
        std::vector<int> temp(right - left + 1);
        int i = left, j = mid + 1, k = 0;
        
        while (i <= mid && j <= right) {
            if (data[i] <= data[j]) temp[k++] = data[i++];
            else temp[k++] = data[j++];
        }
        
        while (i <= mid) temp[k++] = data[i++];
        while (j <= right) temp[k++] = data[j++];
        
        for (i = left, k = 0; i <= right; i++, k++) {
            data[i] = temp[k];
        }
    }
};

class Sorter {
public:
    explicit Sorter(std::unique_ptr<SortStrategy> strategy)
        : strategy(std::move(strategy)) {}
    
    void setStrategy(std::unique_ptr<SortStrategy> newStrategy) {
        strategy = std::move(newStrategy);
    }
    
    void sort(std::vector<int>& data) const {
        if (strategy) {
            strategy->sort(data);
        }
    }

private:
    std::unique_ptr<SortStrategy> strategy;  // Sorter has-a SortStrategy
};

void testStrategy() {
    std::vector<int> data = {64, 34, 25, 12, 22, 11, 90};
    
    Sorter sorter(std::make_unique<QuickSort>());
    sorter.sort(data);  // 使用 QuickSort
    
    sorter.setStrategy(std::make_unique<MergeSort>());
    sorter.sort(data);  // 切换到 MergeSort
}

复合的高级技巧

1. 委托构造与成员初始化

cpp 复制代码
class Resource {
public:
    explicit Resource(const std::string& name) : name(name) {}
    
private:
    std::string name;
};

class Manager {
public:
    Manager() : resource("default") {}  // 委托给成员构造函数
    explicit Manager(const std::string& name) : resource(name) {}

private:
    Resource resource;  // 成员对象通过构造函数初始化
};

2. Pimpl 惯用法(Pointer to Implementation)

cpp 复制代码
// Widget.h - 公开接口
class Widget {
public:
    Widget();
    ~Widget();
    Widget(Widget&& other) noexcept;
    Widget& operator=(Widget&& other) noexcept;
    
    void draw() const;
    void setData(int data);

private:
    class Impl;           // 前置声明
    std::unique_ptr<Impl> pImpl;  // Widget is-implemented-in-terms-of Impl
};

// Widget.cpp - 实现细节
class Widget::Impl {
public:
    void draw() const {
        std::cout << "Drawing widget with data: " << data << std::endl;
    }
    
    int data = 0;
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&& other) noexcept = default;
Widget& Widget::operator=(Widget&& other) noexcept = default;

void Widget::draw() const {
    pImpl->draw();
}

void Widget::setData(int data) {
    pImpl->data = data;
}

常见误区与解决方案

误区 1:"继承比复合更简单"

cpp 复制代码
// 看似简单的继承
template <typename T>
class BadStack : public std::vector<T> {
public:
    void push(const T& item) {
        std::vector<T>::push_back(item);
    }
    
    void pop() {
        std::vector<T>::pop_back();
    }
    
    T& top() {
        return std::vector<T>::back();
    }
};

问题

  • 用户可以直接调用 clear()insert() 等方法破坏栈的语义
  • std::vector 的接口全部暴露
  • 如果 std::vector 的实现改变,BadStack 可能受影响

误区 2:"我需要访问所有基类功能"

cpp 复制代码
// 不需要暴露所有功能
template <typename T>
class GoodStack {
public:
    void push(const T& item) {
        data.push_back(item);
    }
    
    void pop() {
        if (!data.empty()) {
            data.pop_back();
        }
    }
    
    T& top() {
        return data.back();
    }
    
    bool empty() const {
        return data.empty();
    }
    
    std::size_t size() const {
        return data.size();
    }

private:
    std::vector<T> data;  // 只使用需要的功能
};

何时使用继承?

场景 推荐方案
真正的 is-a 关系 Public 继承
需要访问 protected 成员 Private 继承(条款 39)
需要重写 virtual 函数 Private/Public 继承
代码复用 + 接口隔离 复合
运行时替换组件 复合 + 策略模式

总结

核心要点

要点 说明
应用域:has-a 复合表示对象拥有另一个对象作为组成部分
实现域:is-implemented-in-terms-of 复合表示根据某物实现出另一个类
复合优于继承 除非真正的 is-a 关系,否则优先使用复合
接口控制 复合可以精确控制暴露给用户的接口

记忆口诀

复合 has-a 关系明,实现域中塑模行。

继承 is-a 才用应,接口污染要警醒。

优先组合后继承,代码灵活又清晰。

条款 38 的核心建议

通过复合塑模出 has-a 或 "根据某物实现出"。 当你需要复用代码时:

  1. 首先考虑复合
  2. 只有在真正的 is-a 关系时才使用 public 继承
  3. 复合让你完全控制接口暴露
  4. 复合支持运行期组件替换(通过指针/引用)

参考阅读:

  • 《Effective C++》Scott Meyers,条款 38
  • 《设计模式》GoF,关于组合优于继承的原则
  • 《C++ Primer》Stanley B. Lippman 等,关于类设计的章节

系列预告: 下一篇将深入解析条款 39------明智而审慎地使用 private 继承,探讨 private 继承与复合的取舍,以及 empty base optimization(空白基类最优化)的应用。


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

相关推荐
枫叶丹41 小时前
【HarmonyOS 6.0】MDM Kit:PC/2in1设备用户行为限制策略详解
开发语言·华为·harmonyos
weilaieqi11 小时前
微短剧 + 时代到来,短剧内容正在赋能文旅、品牌与数字文化产业
开发语言
ytttr8731 小时前
航天器姿态控制 MATLAB 仿真程序
开发语言·matlab
charlie1145141911 小时前
嵌入式Linux驱动开发——从轮询到中断
linux·开发语言·驱动开发·嵌入式
caimouse1 小时前
Reactos 第 9 章 设备驱动 — 9.14 IRP请求的完成与返回
windows
凡人叶枫1 小时前
Effective C++ 条款40:明智而审慎地使用多重继承
java·数据库·c++·嵌入式开发·effective c++
无限进步_1 小时前
【Linux】系统级文件I/O与文件描述符深度剖析
linux·运维·服务器
放弃 治疗1 小时前
宝塔面板安装 JDK 完整教程|Java 环境配置详解
java·开发语言
虾壳云官方1 小时前
openclaw 一键安装教程(2026年6月15最新)
运维·人工智能·windows·自动化·openclaw