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
};
在这个例子中:
Personhas-aAddress- 地址是人的一个组成部分
- 人不是地址的一种,地址也不是人的一种
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
};
在这个例子中:
Setis-implemented-in-terms-ofstd::listSet不是一个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_back、push_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 或 "根据某物实现出"。 当你需要复用代码时:
- 首先考虑复合
- 只有在真正的 is-a 关系时才使用 public 继承
- 复合让你完全控制接口暴露
- 复合支持运行期组件替换(通过指针/引用)
参考阅读:
- 《Effective C++》Scott Meyers,条款 38
- 《设计模式》GoF,关于组合优于继承的原则
- 《C++ Primer》Stanley B. Lippman 等,关于类设计的章节
系列预告: 下一篇将深入解析条款 39------明智而审慎地使用 private 继承,探讨 private 继承与复合的取舍,以及 empty base optimization(空白基类最优化)的应用。
如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。