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++ 代码。
超级会员免费看
800

被折叠的 条评论
为什么被折叠?



