【读书笔记】《C++ Software Design》第四章:The Visitor Design Pattern
《C++ Software Design》第四章:The Visitor Design Pattern
本章通过对比多种实现方式(面向过程、动态多态、std::variant 及 acyclic visitor),深入探讨如何在“类型扩展”与“操作扩展”之间进行权衡,并给出实用建议。
- 经典 Visitor 模式 UML 图
+----------------+ +--------------------+
| Visitor |<--------| ConcreteVisitor |
+----------------+ +--------------------+
| +visit(Circle) | | +visit(Circle) |
| +visit(Square) | | +visit(Square) |
+----------------+ +--------------------+
▲ ▲
| |
+----------------+ +------------------+
| Shape |<-------| ConcreteShape |
+----------------+ +------------------+
| +accept(Visitor)| | +accept(Visitor) |
+----------------+ +------------------+
Shape为抽象元素,定义accept()。ConcreteShape实现accept()并调用Visitor.visit(this)。Visitor定义所有visit()函数。ConcreteVisitor提供操作的实现(如 Draw、Area)。
Guideline 15: Design for the Addition of Types or Operations
本节关注“在已有数据结构上增加新操作”的设计问题 —— 典型的开放-封闭冲突问题。
15.1 A Procedural Solution
-
描述:最直接的方式是使用
switch-case或if-else语句基于类型进行分发。 -
示例:
enum class ShapeType { Circle, Square }; struct Shape { ShapeType type; // union or polymorphic data omitted }; void draw(const Shape& s) { switch (s.type) { case ShapeType::Circle: /* draw circle */ break; case ShapeType::Square: /* draw square */ break; } } -
问题:
- 易于添加“新操作”(如
draw、area),但添加“新类型”则需修改所有switch。 - 不符合开闭原则(Open-Closed Principle)。
- 易于添加“新操作”(如
15.2 An Object-Oriented Solution
-
策略:使用继承 + 虚函数封装行为,遵循 OCP。
-
示例:
struct Shape { virtual void draw() const = 0; virtual ~Shape() = default; }; struct Circle : Shape { void draw() const override { /* ... */ } }; struct Square : Shape { void draw() const override { /* ... */ } }; -
利点:添加新类型(如 Triangle)无需改动旧代码。
-
缺点:若要添加新操作(如
area()),则需修改所有类,不利于“操作的扩展”。
15.3 Be Aware of the Design Choice in Dynamic Polymorphism
-
选择权衡:
- 如果“类型稳定,操作频繁变化”,应选择 Visitor。
- 如果“类型频繁变化”,应使用虚函数或策略模式。
Guideline 16: Use Visitor to Extend Operations
16.1 Analyzing the Design Issues
- 核心问题:如何在不修改类定义的前提下添加新行为?
- 背景:传统的虚函数机制“封闭类型、开放行为”——Visitor 颠倒了这一点。
16.2 The Visitor Design Pattern Explained
-
结构:
struct Visitor { virtual void visit(const Circle&) = 0; virtual void visit(const Square&) = 0; }; struct Shape { virtual void accept(Visitor&) const = 0; }; struct Circle : Shape { void accept(Visitor& v) const override { v.visit(*this); } }; struct DrawVisitor : Visitor { void visit(const Circle& c) override { /* draw */ } void visit(const Square& s) override { /* draw */ } }; -
优点:
- 可在不改动原有类型的前提下添加操作。
- 非侵入式操作统一分发逻辑。
-
典型应用:编译器 AST、图形系统、财务数据处理。
16.3 Analyzing the Shortcomings of the Visitor Design Pattern
-
问题:
- 添加新类型需要修改所有 Visitor(不满足类型扩展开放)。
- 类型之间强依赖(
visit()参数硬编码)。 - 类型与 Visitor 紧耦合(
accept()强制内嵌)。 - RTTI 方案难以替代时支持
const,non-const,mutable等情况繁琐。
-
改进方向:尝试
std::variant或基于 CRTP/模板的 Visitor。
Guideline 17: Consider std::variant for Implementing Visitor
C++17 起,std::variant 提供了一个值语义的、类型安全的 sum type,可以替代部分 Visitor 功能。
17.1 Introduction to std::variant
std::variant 实现结构图
+----------------------------+
| std::variant |
| <Circle, Square, ...> |
+----------------------------+
|
v
+----------------------------+
| std::visit() |
| overloaded visitors |
+----------------------------+
说明:
-
std::variant使用值语义,避免继承与虚函数。 -
通过
std::visit()进行访问,支持编译期多态。 -
每个操作通过 lambda 或
std::function实现,无需嵌入原类型。 -
示例:
using Shape = std::variant<Circle, Square>; struct Draw { void operator()(const Circle&) const { /* draw circle */ } void operator()(const Square&) const { /* draw square */ } }; void draw(const Shape& s) { std::visit(Draw{}, s); } -
核心优势:\n
- 无需基类,无需虚函数。
- 值语义,结构更扁平、组合灵活。
17.2 Refactoring the Drawing of Shapes as a Value-Based, Nonintrusive Solution
- 非侵入性:类型本身无需知道 Visitor 的存在(无
accept()方法)。 - 组合性:多个
std::variant可嵌套或组合,便于构造更复杂的数据结构。
17.3 Performance Benchmarks
-
动态多态 vs std::variant:
- 虚函数:动态分派,运行时效率高于 naive RTTI,但无法内联。
- variant:分派由
std::visit()完成,可被编译器优化(如内联、多态擦除)。 - 在某些场景下,variant 更适合数据密集型、计算密集型任务(如图形渲染管线)。
17.4 Analyzing the Shortcomings of the std::variant Solution
-
劣势:
- 类型必须在编译时列举所有可能性(不适用于运行时可扩展系统)。
std::visit的嵌套 lambda 可读性差,维护成本高。- 类似 CRTP 结构时组合受限。
-
建议:
- 若类型集合固定,推荐使用
variant; - 若需插件或运行时多态,仍推荐 Visitor 或策略模式。
- 若类型集合固定,推荐使用
Guideline 18: Beware the Performance of Acyclic Visitor
-
介绍:
- Acyclic Visitor 是一种更灵活的 Visitor 变体(支持运行时扩展)。
- 每个
visit()调用由 RTTI+dynamic_cast支持,避免了强耦合。
-
实现方式:
struct BaseVisitor { virtual ~BaseVisitor() = default; }; struct Circle; struct Square; struct VisitorForCircle { virtual void visit(const Circle&) = 0; }; struct Circle : Shape { void accept(BaseVisitor& v) const override { if (auto* vc = dynamic_cast<VisitorForCircle*>(&v)) { vc->visit(*this); } } }; -
性能问题:
- 每个
accept()都可能包含多个dynamic_cast,分支判断复杂,导致性能下降。
- 每个
-
建议:
- 限制 acyclic visitor 用于插件系统或需热扩展场景;
- 普通业务逻辑仍应首选静态 Visitor 或
std::variant。
小结
| 指导原则 | 核心思想 | 推荐场景 |
|---|---|---|
| Guideline 15 | 明确类型/操作变化频率,选择动态多态或静态调度 | 所有领域 |
| Guideline 16 | 使用 Visitor 实现“操作的开放性” | AST、图形系统 |
| Guideline 17 | 用 std::variant 实现值语义、非侵入式 Visitor | 类型固定系统、现代 C++ 项目 |
| Guideline 18 | 谨慎使用 acyclic visitor,关注性能 | 插件架构、运行时类型系统 |
已为第四章《The Visitor Design Pattern》补充完整展开内容,包括每条 Guideline 的背景动机、典型实现、优缺点分析以及适用建议。内容涵盖:
- 面向过程 vs 面向对象 vs Visitor 的对比
std::variant的现代替代方案- acyclic visitor 的结构与性能警示
- 各种设计在 C++ 实践中的具体实现形式
延伸
C++ 模板版 Visitor(静态多态 CRTP)
适用于类型固定、操作多变的场景(完全去虚函数,纯编译期类型分派):
// Step 1: 定义所有可访问的类型
struct Circle { void draw() const { std::cout << "Draw Circle\n"; } };
struct Square { void draw() const { std::cout << "Draw Square\n"; } };
// Step 2: 定义泛型访问器接口
template<typename T>
struct StaticVisitor {
void visit(const T& t) const {
static_cast<const T&>(t).draw(); // 默认调用 draw() 方法
}
};
// Step 3: 类型持有容器 + 统一 dispatch
template<typename... Ts>
class Variant {
std::variant<Ts...> data_;
public:
Variant(std::variant<Ts...> d): data_(std::move(d)) {}
template<typename Visitor>
void accept(Visitor&& v) const {
std::visit([&v](const auto& obj){ v.visit(obj); }, data_);
}
};
// 使用:
int main() {
using MyShape = Variant<Circle, Square>;
MyShape shape1{ Circle{} };
MyShape shape2{ Square{} };
StaticVisitor<Circle> visitor;
shape1.accept(visitor); // 输出 Draw Circle
shape2.accept(visitor); // 输出 Draw Square
}
✅ 优点:零运行时开销,完全可内联,可用于编译期组合行为(元编程支持)。
进阶模板技巧:Overloaded Helper
用于组合多个 lambda 表达式以实现静态多态访问:
template<class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;
std::variant<Circle, Square> shape = Circle{};
std::visit(Overloaded{
[](const Circle& c){ c.draw(); },
[](const Square& s){ s.draw(); }
}, shape);
Summary
Visitor 实现方式对比
| 方案 | 类型扩展性 | 操作扩展性 | 值语义 | 性能 | 易用性 | 备注 |
|---|---|---|---|---|---|---|
| 面向对象 + 虚函数 | ✅ | ❌ | ❌ | 高(动态分派) | 高 | 常规方法 |
| Visitor 模式 | ❌ | ✅ | ❌ | 中(visit调用) | 中 | 经典方案 |
| Acyclic Visitor | ✅ | ✅ | ❌ | 低(RTTI) | 低 | 插件化结构 |
std::variant | ❌ | ✅ | ✅ | 高(编译期) | 中 | 现代推荐 |
| 模板版静态 Visitor | ✅ | ✅ | ✅ | 最高(全静态) | 低(需模板) | 高级泛型编程 |
| 场景需求 | 推荐模式 |
|---|---|
| 类型固定、操作多变 | Visitor / variant |
| 操作固定、类型频繁变化 | 继承+虚函数 |
| 插件系统、运行时扩展 | Acyclic Visitor |
| 性能敏感、低耦合、高重用 | 模板静态 Visitor |
| 面向值语义,使用现代 C++ 特性 | std::variant |
原文地址:https://blog.csdn.net/YZJincsdn/article/details/149306618
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!
