51、C++ 中编译时访客模式与 std::variant 的应用

C++ 中编译时访客模式与 std::variant 的应用

在 C++ 编程中,访客模式是一种强大的设计模式,它允许我们在不修改类定义的情况下,为类添加新的成员函数。本文将深入探讨编译时访客模式以及 C++17 中引入的 std::variant 对访客模式的影响。

编译时访客模式

在模板上下文中,访客模式的多重分派变得简单。模板函数可以为 T1 T2 类型的任何组合轻松运行不同的算法,且基于多类型进行不同的调用分派不会产生额外成本(当然,需要为所有需要处理的组合编写代码)。

下面是一个简单的示例,展示了如何在编译时模拟经典的访客模式:

class Pet {
    std::string color_;
public:
    Pet(std::string_view color) : color_(color) {}
    const std::string& color() const { return color_; }
    template <typename Visitable, typename Visitor>
    static void accept(Visitable& p, Visitor& v) {
        v.visit(p);
    }
};

class Cat : public Pet {
public:
    using Pet::Pet;
};

class Dog : public Pet {
public:
    using Pet::Pet;
};

class FeedingVisitor {
public:
    void visit(Cat& c) {
        std::cout << "Feed tuna to the " << c.color()
                  << " cat" << std::endl;
    }
    void visit(Dog& d) {
        std::cout << "Feed steak to the " << d.color()
                  << " dog" << std::endl;
    }
};

// 使用示例
int main() {
    Cat c("orange");
    Dog d("brown");
    FeedingVisitor fv;
    Pet::accept(c, fv);
    Pet::accept(d, fv);
    return 0;
}

在这个示例中, accept 函数是一个模板静态成员函数,其第一个参数(可访问对象)的实际类型将在编译时推导。访客类不需要从公共基类派生,因为类型解析在编译时完成。

结合组合模式与反射

当访客模式与组合模式结合时,会产生更有趣的可能性,这与 C++ 中缺少的反射特性相关。反射是指程序检查和内省自身源代码,并根据内省结果生成新行为的能力。虽然 C++ 没有原生的反射能力,但我们可以使用编译时访客模式实现类似的功能。

以几何对象层次结构为例,下面是 Point 类的实现:

class Point {
public:
    Point() = default;
    Point(double x, double y) : x_(x), y_(y) {}
    template <typename This, typename Visitor>
    static void accept(This& t, Visitor& v) {
        v.visit(t.x_);
        v.visit(t.y_);
    }
private:
    double x_ {};
    double y_ {};
};

Line 类由两个 Point 对象组成:

class Line {
public:
    Line() = default;
    Line(Point p1, Point p2) : p1_(p1), p2_(p2) {}
    template <typename This, typename Visitor>
    static void accept(This& t, Visitor& v) {
        v.visit(t.p1_);
        v.visit(t.p2_);
    }
private:
    Point p1_;
    Point p2_;
};

Intersection 类是一个模板类,可容纳不同类型的几何对象:

template <typename G1, typename G2>
class Intersection {
public:
    Intersection() = default;
    Intersection(G1 g1, G2 g2) : g1_(g1), g2_(g2) {}
    template <typename This, typename Visitor>
    static void accept(This& t, Visitor& v) {
        v.visit(t.g1_);
        v.visit(t.g2_);
    }
private:
    G1 g1_;
    G2 g2_;
};

接下来,我们实现二进制序列化和反序列化的访客类:

class BinarySerializeVisitor {
public:
    BinarySerializeVisitor(char* buffer, size_t size) :
        buf_(buffer), size_(size) {}
    void visit(double x) {
        if (size_ < sizeof(x))
            throw std::runtime_error("Buffer overflow");
        memcpy(buf_, &x, sizeof(x));
        buf_ += sizeof(x);
        size_ -= sizeof(x);
    }
    template <typename T> void visit(const T& t) {
        T::accept(t, *this);
    }
private:
    char* buf_;
    size_t size_;
};

class BinaryDeserializeVisitor {
public:
    BinaryDeserializeVisitor(const char* buffer, size_t size)
        : buf_(buffer), size_(size) {}
    void visit(double& x) {
        if (size_ < sizeof(x))
            throw std::runtime_error("Buffer overflow");
        memcpy(&x, buf_, sizeof(x));
        buf_ += sizeof(x);
        size_ -= sizeof(x);
    }
    template <typename T> void visit(T& t) {
        T::accept(t, *this);
    }
private:
    const char* buf_;
    size_t size_;
};

使用这些访客类,我们可以将对象序列化为二进制数据并发送到另一台机器,然后在接收端进行反序列化:

// 发送端
Line l = ...;
Circle c = ...;
Intersection<Circle, Circle> x = ...;
char buffer[1024];
BinarySerializeVisitor serializer(buffer, sizeof(buffer));
serializer.visit(l);
serializer.visit(c);
serializer.visit(x);
// 发送 buffer 到接收端

// 接收端
Line l;
Circle c;
Intersection<Circle, Circle> x;
BinaryDeserializeVisitor deserializer(buffer, sizeof(buffer));
deserializer.visit(l);
deserializer.visit(c);
deserializer.visit(x);
访客模式的变体

在实际应用中,我们可以对访客模式进行一些变体。例如,将 visit 成员函数改为 operator() ,使对象可调用:

class BinarySerializeVisitor {
public:
    void operator()(double x);
    template <typename T> void operator()(const T& t);
    ...
};

class Point {
public:
    static void accept(This& t, Visitor& v) {
        v(t.x_);
        v(t.y_);
    }
    ...
};

还可以实现包装函数,使用可变参数模板来对多个对象调用访客:

// C++17 之前的递归模板实现
template <typename V, typename T>
void visitation(V& v, T& t) {
    v(t);
}
template <typename V, typename T, typename... U>
void visitation(V& v, T& t, U&... u) {
    v(t);
    visitation(v, u ...);
}

// C++17 的折叠表达式实现
template <typename V, typename T, typename... U>
void visitation(V& v, U&... u) {
    (v(u), ...);
}

// C++14 的模拟折叠表达式实现
template <typename V, typename T, typename... U>
void visitation(V& v, U&... u) {
    using fold = int[];
    (void)fold { 0, (v(u), 0)... };
}
编译时访客模式的优势

编译时访客模式解决了与经典访客模式相同的问题,允许我们在不编辑类定义的情况下为类添加新的成员函数。与运行时版本相比,编译时访客模式通常更容易实现,因为模板提供了开箱即用的多重分派功能。

下面是编译时访客模式与运行时访客模式的对比表格:
| 模式 | 多重分派成本 | 类型解析时间 | 实现复杂度 |
| ---- | ---- | ---- | ---- |
| 编译时访客模式 | 无额外成本 | 编译时 | 较低 |
| 运行时访客模式 | 有一定成本 | 运行时 | 较高 |

编译时访客模式的调用流程
graph TD;
    A[创建可访问对象] --> B[创建访客对象];
    B --> C[调用 accept 函数];
    C --> D[访客对象调用 visit 函数];
    D --> E[处理可访问对象];

编译时访客模式为 C++ 编程带来了新的可能性,尤其是在处理复杂对象和序列化问题时。通过结合组合模式,我们可以在一定程度上模拟反射功能。同时,C++17 引入的 std::variant 进一步扩展了访客模式的应用场景,我们将在后续内容中详细探讨。

C++ 中编译时访客模式与 std::variant 的应用

std::variant 与访客模式

C++17 引入的 std::variant 为访客模式带来了新的变化。 std::variant 本质上是一个“智能联合”,它可以安全地存储不同类型的值,并且能够在运行时检查当前存储的类型。与传统联合不同的是, std::variant 能避免访问错误类型导致的未定义行为。

以下是 std::variant 的使用示例:

#include <iostream>
#include <variant>

int main() {
    std::variant<int, double, std::string> v;
    std::get<int>(v) = 0; // 初始化为 int
    std::cout << v.index(); // 输出 0,int 的索引
    ++std::get<0>(v); // 操作 int 类型
    try {
        std::get<1>(v); // 抛出 std::bad_variant_access 异常
    } catch (const std::bad_variant_access& e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

std::variant 提供了类似于基于继承的运行时多态的能力,但有两个主要区别:一是 std::variant 不要求所有类型来自同一层次结构;二是 std::variant 对象只能存储其声明中列出的类型之一,而基类指针可以指向任何派生类。

使用 std::visit 进行访问

std::visit 函数用于对 std::variant 进行访问,它接受一个可调用对象和一个或多个 std::variant 参数。可调用对象必须为 std::variant 中可能存储的每种类型都定义 operator()

以下是使用 std::visit 的示例:

#include <iostream>
#include <variant>

struct Print {
    void operator()(int i) { std::cout << i; }
    void operator()(double d) { std::cout << d; }
    void operator()(const std::string& s) { std::cout << s; }
};

int main() {
    std::variant<int, double, std::string> v = 10;
    Print print;
    std::visit(print, v);
    return 0;
}
重新实现宠物访客模式

我们可以使用 std::variant std::visit 重新实现宠物访客模式。首先,将 Pet 定义为 std::variant 类型:

#include <iostream>
#include <variant>
#include <string>

// 基类
class PetBase {
public:
    PetBase(std::string_view color) : color_(color) {}
    const std::string& color() const { return color_; }
private:
    const std::string color_;
};

// 派生类
class Cat : private PetBase {
public:
    using PetBase::PetBase;
    using PetBase::color;
};

class Dog : private PetBase {
public:
    using PetBase::PetBase;
    using PetBase::color;
};

class Lorikeet {
public:
    Lorikeet(std::string_view body, std::string_view head) :
        body_(body), head_(head) {}
    std::string color() const {
        return body_ + " and " + head_;
    }
private:
    const std::string body_;
    const std::string head_;
};

// 定义 Pet 为 variant 类型
using Pet = std::variant<Cat, Dog, Lorikeet>;

// 访客类
class FeedingVisitor {
public:
    void operator()(const Cat& c) {
        std::cout << "Feed tuna to the " << c.color()
                  << " cat" << std::endl;
    }
    void operator()(const Dog& d) {
        std::cout << "Feed steak to the " << d.color()
                  << " dog" << std::endl;
    }
    void operator()(const Lorikeet& l) {
        std::cout << "Feed grain to the " << l.color()
                  << " bird" << std::endl;
    }
};

int main() {
    Pet p = Cat("orange");
    FeedingVisitor v;
    std::visit(v, p);
    return 0;
}
使用 lambda 作为访客

我们可以使用 lambda 表达式作为访客,但需要处理多种类型。一种方法是使用多态 lambda,在 lambda 体内使用 if constexpr 处理不同类型:

#include <iostream>
#include <variant>
#include <string>

// 前面定义的 Cat, Dog, Lorikeet 类

#define SAME(v, T) \
    std::is_same_v<std::decay_t<decltype(v)>, T>

using Pet = std::variant<Cat, Dog, Lorikeet>;

auto fv = [](const auto& p) {
    if constexpr (SAME(p, Cat)) {
        std::cout << "Feed tuna to the " << p.color()
                  << " cat" << std::endl;
    } else if constexpr (SAME(p, Dog)) {
        std::cout << "Feed steak to the " << p.color()
                  << " dog" << std::endl;
    } else if constexpr (SAME(p, Lorikeet)) {
        std::cout << "Feed grain to the " << p.color()
                  << " bird" << std::endl;
    } else abort();
};

int main() {
    Pet p = Cat("orange");
    std::visit(fv, p);
    return 0;
}

这种方法的缺点是没有编译时验证所有可能的类型都被处理,但只要不调用未定义的类型,程序就可以正常工作。

另一种方法是使用重载集,通过继承多个 lambda 来创建一组重载的 operator()

#include <iostream>
#include <variant>
#include <string>

// 前面定义的 Cat, Dog, Lorikeet 类

using Pet = std::variant<Cat, Dog, Lorikeet>;

template <typename... T> struct overloaded : T... {
    using T::operator()...;
};

template <typename... T>
overloaded(T...)->overloaded<T...>;

auto pv = overloaded {
    [](const Cat& c) {
        std::cout << "Play with feather with the " << c.color()
                  << " cat" << std::endl;
    },
    [](const Dog& d) {
        std::cout << "Play fetch with the " << d.color()
                  << " dog" << std::endl;
    },
    [](const Lorikeet& l) {
        std::cout << "Teach words to the " << l.color()
                  << " bird" << std::endl;
    }
};

int main() {
    Pet l = Lorikeet("yellow", "green");
    std::visit(pv, l);
    return 0;
}
多参数的 std::visit

std::visit 可以接受多个 std::variant 参数,从而可以执行依赖于多个运行时条件的操作。访客类需要处理所有可能的类型组合。

以下是一个示例:

#include <iostream>
#include <variant>
#include <string>

// 前面定义的 Cat, Dog 类

using Pet = std::variant<Cat, Dog>;

class CareVisitor {
public:
    void operator()(const Cat& c1, const Cat& c2) {
        std::cout << "Let the " << c1.color() << " and the "
                  << c2.color() << " cats play" << std::endl;
    }
    void operator()(const Dog& d, const Cat& c) {
        std::cout << "Keep the " << d.color()
                  << " dog safe from the vicious " << c.color()
                  << " cat" << std::endl;
    }
    // 其他组合的处理函数
};

int main() {
    Pet c1 = Cat("orange");
    Pet c2 = Cat("black");
    Pet d = Dog("brown");
    CareVisitor cv;
    std::visit(cv, c1, c2); // 两只猫
    std::visit(cv, c1, d); // 猫和狗
    return 0;
}
std::visit 的调用流程
graph TD;
    A[创建 std::variant 对象] --> B[创建访客对象];
    B --> C[调用 std::visit 函数];
    C --> D[根据 variant 存储的类型调用相应的 operator()];
    D --> E[执行相应的操作];
总结

编译时访客模式和 std::variant 的结合为 C++ 编程带来了更多的灵活性和强大的功能。编译时访客模式通过模板实现了多重分派,避免了运行时的额外开销,并且可以在一定程度上模拟反射功能。而 std::variant 则提供了一种安全的方式来处理多种类型, std::visit 函数进一步扩展了访客模式的应用场景,使得我们可以轻松地对不同类型进行操作。

在实际应用中,我们可以根据具体需求选择合适的访客模式实现方式。如果需要在编译时确定类型,并且对性能有较高要求,可以选择编译时访客模式;如果需要处理多种不同类型,并且希望在运行时动态选择操作,可以使用 std::variant std::visit

通过合理运用这些技术,我们可以编写出更加灵活、高效和可维护的 C++ 代码。

AI 代码审查Review工具 是一个旨在自动化代码审查流程的工具。它通过集成版本控制系统(如 GitHub 和 GitLab)的 Webhook,利用大型语言模型(LLM)对代码变更进行分析,并将审查意见反馈到相应的 Pull Request 或 Merge Request 中。此外,它还支持将审查结果通知到企业微信等通讯工具。 一个基于 LLM 的自动化代码审查助手。通过 GitHub/GitLab Webhook 监听 PR/MR 变更,调用 AI 分析代码,并将审查意见自动评论到 PR/MR,同时支持多种通知渠道。 主要功能 多平台支持: 集成 GitHub 和 GitLab Webhook,监听 Pull Request / Merge Request 事件。 智能审查模式: 详细审查 (/github_webhook, /gitlab_webhook): AI 对每个变更文件进行分析,旨在找出具体问题。审查意见会以结构化的形式(例如,定位到特定代码行、问题分类、严重程度、分析和建议)逐条评论到 PR/MR。AI 模型会输出 JSON 格式的分析结果,系统再将其转换为多条独立的评论。 通用审查 (/github_webhook_general, /gitlab_webhook_general): AI 对每个变更文件进行整体性分析,并为每个文件生成一个 Markdown 格式的总结性评论。 自动化流程: 自动将 AI 审查意见(详细模式下为多条,通用模式下为每个文件一条)发布到 PR/MR。 在所有文件审查完毕后,自动在 PR/MR 中发布一条总结性评论。 即便 AI 未发现任何值得报告的问题,也会发布相应的友好提示和总结评论。 异步处理审查任务,快速响应 Webhook。 通过 Redis 防止对同一 Commit 的重复审查。 灵活配置: 通过环境变量设置基
【直流微电网】径向直流微电网的状态空间建模线性化:一种耦合DC-DC变换器状态空间平均模型的方法 (Matlab代码实现)内容概要:本文介绍了径向直流微电网的状态空间建模线性化方法,重点提出了一种基于耦合DC-DC变换器的状态空间平均模型的建模策略。该方法通过数学建模手段对直流微电网系统进行精确的状态空间描述,并对其进行线性化处理,以便于系统稳定性分析控制器设计。文中结合Matlab代码实现,展示了建模仿真过程,有助于研究人员理解和复现相关技术,推动直流微电网系统的动态性能研究工程应用。; 适合人群:具备电力电子、电力系统或自动化等相关背景,熟悉Matlab/Simulink仿真工具,从事新能源、微电网或智能电网研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①掌握直流微电网的动态建模方法;②学习DC-DC变换器在耦合条件下的状态空间平均建模技巧;③实现系统的线性化分析并支持后续控制器设计(如电压稳定控制、功率分配等);④为科研论文撰写、项目仿真验证提供技术支持代码参考。; 阅读建议:建议读者结合Matlab代码逐步实践建模流程,重点关注状态变量选取、平均化处理和线性化推导过程,同时可扩展应用于更复杂的直流微电网拓扑结构中,提升系统分析设计能力。
内容概要:本文介绍了基于物PINN驱动的三维声波波动方程求解(Matlab代码实现)理信息神经网络(PINN)求解三维声波波动方程的Matlab代码实现方法,展示了如何利用PINN技术在无需大量标注数据的情况下,结合物理定律约束进行偏微分方程的数值求解。该方法将神经网络物理方程深度融合,适用于复杂波动问题的建模仿真,并提供了完整的Matlab实现方案,便于科研人员理解和复现。此外,文档还列举了多个相关科研方向和技术服务内容,涵盖智能优化算法、机器学习、信号处理、电力系统等多个领域,突出其在科研仿真中的广泛应用价值。; 适合人群:具备一定数学建模基础和Matlab编程能力的研究生、科研人员及工程技术人员,尤其适合从事计算物理、声学仿真、偏微分方程数值解等相关领域的研究人员; 使用场景及目标:①学习并掌握PINN在求解三维声波波动方程中的应用原理实现方式;②拓展至其他物理系统的建模仿真,如电磁场、热传导、流体力学等问题;③为科研项目提供可复用的代码框架和技术支持参考; 阅读建议:建议读者结合文中提供的网盘资源下载完整代码,按照目录顺序逐步学习,重点关注PINN网络结构设计、损失函数构建及物理边界条件的嵌入方法,同时可借鉴其他案例提升综合仿真能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值