C++ 访问者模式在 AST 遍历与代码生成中的应用

哈喽,各位好!今天咱们来聊聊C++的访问者模式,这玩意儿听起来好像很高大上,其实理解起来也没那么难,而且在AST(抽象语法树)遍历和代码生成里,那可是相当实用。

啥是访问者模式?别慌,先讲故事

想象一下,你是个博物馆馆长,博物馆里摆满了各种各样的文物,比如雕塑、画作、青铜器等等。每个文物都有自己的特点,比如雕塑有材质、高度,画作有作者、风格。

现在,来了几波游客:

  • 第一波: 想给所有文物拍照留念。
  • 第二波: 想给所有文物做价值评估。
  • 第三波: 想给所有青铜器进行防氧化处理。

如果让每个文物自己去实现这些功能,那文物类就得不断膨胀,而且如果以后再来一波“想给所有画作做修复”的游客,那就又得改文物类。这显然不符合“开闭原则”(对扩展开放,对修改关闭)。

这时候,访问者模式就派上用场了。

我们可以定义一个“访问者”接口,里面包含针对每种文物类型的访问方法,比如visit(Sculpture& sculpture)visit(Painting& painting)visit(BronzeWare& bronzeWare)

然后,每个游客(也就是每个操作)都实现一个具体的访问者类,比如PhotographerValuationExpertAntioxidantHandler。这些访问者类会实现visit方法,在这些方法里完成具体的操作。

最后,文物类只需要提供一个accept(Visitor& visitor)方法,这个方法接收一个访问者对象,然后调用访问者对象对应的visit方法。

这样,我们就可以灵活地添加新的操作,而无需修改文物类本身。

访问者模式的结构

用UML图来表示一下,大概是这样:

+---------------+       +----------------+       +----------------+
|   Element     |------>|   Visitor      |------>| ConcreteVisitor|
+---------------+       +----------------+       +----------------+
| accept(Visitor)|       | visit(ElementA) |       | visit(ElementA) |
+---------------+       | visit(ElementB) |       | visit(ElementB) |
                        +----------------+       +----------------+
                                                    (具体的操作)

+---------------+
|  ConcreteElementA |
+---------------+
| accept(Visitor)|
+---------------+

+---------------+
|  ConcreteElementB |
+---------------+
| accept(Visitor)|
+---------------+
  • Element (元素): 定义 accept 方法,接受一个访问者对象。
  • ConcreteElement (具体元素): 实现 accept 方法,将自身传递给访问者。
  • Visitor (访问者): 定义访问每个具体元素的方法。
  • ConcreteVisitor (具体访问者): 实现访问者接口,定义针对每个具体元素的操作。

C++ 代码示例 (博物馆文物的故事)

#include <iostream>
#include <vector>

// 前置声明
class Sculpture;
class Painting;
class BronzeWare;

// 访问者接口
class Visitor {
public:
    virtual void visit(Sculpture& sculpture) = 0;
    virtual void visit(Painting& painting) = 0;
    virtual void visit(BronzeWare& bronzeWare) = 0;
    virtual ~Visitor() {} // 记得加虚析构函数,这是个好习惯
};

// 元素接口
class Artifact {
public:
    virtual void accept(Visitor& visitor) = 0;
    virtual ~Artifact() {}
};

// 具体元素 - 雕塑
class Sculpture : public Artifact {
public:
    Sculpture(std::string material, int height) : material_(material), height_(height) {}
    void accept(Visitor& visitor) override {
        visitor.visit(*this);
    }
    std::string getMaterial() const { return material_; }
    int getHeight() const { return height_; }

private:
    std::string material_;
    int height_;
};

// 具体元素 - 画作
class Painting : public Artifact {
public:
    Painting(std::string artist, std::string style) : artist_(artist), style_(style) {}
    void accept(Visitor& visitor) override {
        visitor.visit(*this);
    }
    std::string getArtist() const { return artist_; }
    std::string getStyle() const { return style_; }

private:
    std::string artist_;
    std::string style_;
};

// 具体元素 - 青铜器
class BronzeWare : public Artifact {
public:
    BronzeWare(int age) : age_(age) {}
    void accept(Visitor& visitor) override {
        visitor.visit(*this);
    }
    int getAge() const { return age_; }

private:
    int age_;
};

// 具体访问者 - 摄影师
class Photographer : public Visitor {
public:
    void visit(Sculpture& sculpture) override {
        std::cout << "Photographer: Taking a photo of sculpture (Material: "
                  << sculpture.getMaterial() << ", Height: " << sculpture.getHeight() << ")n";
    }
    void visit(Painting& painting) override {
        std::cout << "Photographer: Taking a photo of painting (Artist: "
                  << painting.getArtist() << ", Style: " << painting.getStyle() << ")n";
    }
    void visit(BronzeWare& bronzeWare) override {
        std::cout << "Photographer: Taking a photo of bronze ware (Age: "
                  << bronzeWare.getAge() << " years)n";
    }
};

// 具体访问者 - 价值评估专家
class ValuationExpert : public Visitor {
public:
    void visit(Sculpture& sculpture) override {
        std::cout << "Valuation Expert: Evaluating sculpture (Material: "
                  << sculpture.getMaterial() << ", Height: " << sculpture.getHeight()
                  << ") - Estimated value: $" << sculpture.getHeight() * 100 << "n";
    }
    void visit(Painting& painting) override {
        std::cout << "Valuation Expert: Evaluating painting (Artist: "
                  << painting.getArtist() << ", Style: " << painting.getStyle()
                  << ") - Estimated value: $" << (painting.getArtist().length() + painting.getStyle().length()) * 50 << "n";
    }
    void visit(BronzeWare& bronzeWare) override {
        std::cout << "Valuation Expert: Evaluating bronze ware (Age: "
                  << bronzeWare.getAge() << " years) - Estimated value: $" << bronzeWare.getAge() * 20 << "n";
    }
};

int main() {
    std::vector<Artifact*> artifacts;
    artifacts.push_back(new Sculpture("Marble", 150));
    artifacts.push_back(new Painting("Van Gogh", "Post-Impressionism"));
    artifacts.push_back(new BronzeWare(500));

    Photographer photographer;
    ValuationExpert valuationExpert;

    for (Artifact* artifact : artifacts) {
        artifact->accept(photographer);
        artifact->accept(valuationExpert);
    }

    for (Artifact* artifact : artifacts) {
        delete artifact;
    }

    return 0;
}

访问者模式在 AST 遍历中的应用

现在,让我们把这个模式应用到AST遍历中。AST是编译器前端的重要组成部分,它将源代码转换成一种树状结构,方便后续的分析和代码生成。

AST的节点类型很多,比如表达式节点、语句节点、声明节点等等。我们需要对AST进行遍历,执行各种操作,比如:

  • 类型检查: 检查变量类型是否匹配。
  • 代码优化: 移除冗余代码。
  • 代码生成: 将AST转换成目标代码。

这时候,访问者模式就可以大显身手了。

AST 节点结构

假设我们有如下简单的AST节点类型:

// 抽象节点类
class ASTNode {
public:
    virtual void accept(Visitor& visitor) = 0;
    virtual ~ASTNode() {}
};

// 表达式节点
class ExpressionNode : public ASTNode {
public:
    ExpressionNode(std::string value) : value_(value) {}
    void accept(Visitor& visitor) override;
    std::string getValue() const { return value_; }

private:
    std::string value_;
};

// 语句节点
class StatementNode : public ASTNode {
public:
    StatementNode(ASTNode* child) : child_(child) {}
    void accept(Visitor& visitor) override;
    ASTNode* getChild() const { return child_; }

private:
    ASTNode* child_;
};

// 声明节点
class DeclarationNode : public ASTNode {
public:
    DeclarationNode(std::string name, std::string type) : name_(name), type_(type) {}
    void accept(Visitor& visitor) override;
    std::string getName() const { return name_; }
    std::string getType() const { return type_; }

private:
    std::string name_;
    std::string type_;
};

访问者实现

我们定义一个访问者接口,针对每种节点类型都有一个visit方法:

class Visitor {
public:
    virtual void visit(ExpressionNode& node) = 0;
    virtual void visit(StatementNode& node) = 0;
    virtual void visit(DeclarationNode& node) = 0;
    virtual ~Visitor() {}
};

然后,在具体的节点类中实现accept方法:

void ExpressionNode::accept(Visitor& visitor) {
    visitor.visit(*this);
}

void StatementNode::accept(Visitor& visitor) {
    visitor.visit(*this);
}

void DeclarationNode::accept(Visitor& visitor) {
    visitor.visit(*this);
}

代码生成器

现在,我们可以创建一个代码生成器,它继承自Visitor类:

#include <sstream>

class CodeGenerator : public Visitor {
public:
    void visit(ExpressionNode& node) override {
        codeStream_ << node.getValue();
    }

    void visit(StatementNode& node) override {
        // 递归访问子节点
        node.getChild()->accept(*this);
        codeStream_ << ";n"; // 假设语句以分号结尾
    }

    void visit(DeclarationNode& node) override {
        codeStream_ << node.getType() << " " << node.getName() << ";n";
    }

    std::string getCode() const {
        return codeStream_.str();
    }

private:
    std::stringstream codeStream_;
};

使用示例

int main() {
    // 构建一个简单的AST
    DeclarationNode* declaration = new DeclarationNode("x", "int");
    ExpressionNode* expression = new ExpressionNode("x + 1");
    StatementNode* statement = new StatementNode(expression);

    // 创建代码生成器
    CodeGenerator generator;

    // 遍历AST并生成代码
    declaration->accept(generator);
    statement->accept(generator);

    // 输出生成的代码
    std::cout << generator.getCode() << std::endl;

    // 释放内存
    delete declaration;
    delete expression;
    delete statement;

    return 0;
}

这段代码会生成如下代码:

int x;
x + 1;

访问者模式的优点

  • 解耦: 将数据结构(AST节点)和操作(代码生成、类型检查)分离。
  • 扩展性: 可以方便地添加新的操作,而无需修改AST节点类。
  • 灵活性: 可以对不同的节点类型执行不同的操作。
  • 符合开闭原则: 对扩展开放,对修改关闭。

访问者模式的缺点

  • 复杂性: 如果节点类型很多,访问者类的数量也会很多,会增加代码的复杂性。
  • 节点类型变化敏感: 如果添加了新的节点类型,需要修改所有的访问者类。

访问者模式的适用场景

  • 当需要对一个对象结构中的对象进行很多不同的操作,并且希望避免让这些操作“污染”这些对象类的时候。
  • 当一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
  • 当定义在该对象结构上的操作需要经常变化的时候。

访问者模式与其他模式的比较

模式 优点 缺点 适用场景
访问者模式 解耦数据结构和操作,扩展性好,灵活性高,符合开闭原则。 复杂性较高,节点类型变化敏感。 需要对对象结构进行多种不同操作,且操作经常变化,或者需要避免操作“污染”对象类。
策略模式 将算法封装成独立的类,可以动态切换算法,易于扩展。 需要定义多个策略类,增加代码量。 需要在运行时动态选择算法,或者需要将算法从上下文中解耦。
模板方法模式 定义算法的骨架,将一些步骤延迟到子类实现,可以避免代码重复。 子类需要实现抽象方法,可能会导致代码分散。 算法的步骤基本固定,但某些步骤需要根据具体情况进行调整。
迭代器模式 提供一种访问聚合对象元素的方法,而无需暴露其内部结构。 需要定义迭代器类,增加代码量。 需要遍历聚合对象,且不想暴露其内部结构。

总结

访问者模式是一种强大的设计模式,它可以将数据结构和操作分离,提高代码的灵活性和可扩展性。在AST遍历和代码生成等场景中,访问者模式可以发挥重要作用。当然,访问者模式也有其缺点,需要根据具体情况进行权衡。

希望这次讲解能帮助大家理解访问者模式,并在实际项目中灵活运用。以后有机会再和大家分享其他的设计模式和编程技巧。

各位,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注