这个例子展示了一种在 Rust 中处理不同类型消息的优雅方式。它有 4 个变体,一些变体具有子成员,只有在枚举属于该特定类型时才能访问这些子成员。在 TypeScript 中也可以使用类似的模式。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Run Code Online (Sandbox Code Playgroud)
在 C++ 中,这很可能与以下代码进行比较。
struct Message
{
enum MessageType {QUIT, MOVE, WRITE, CHANGECOLOR} type;
union MessageContent
{
struct Move { int x; int y;} move;
std::string write;
std::tuple<int, int, int> changeColor;
} content;
};
Run Code Online (Sandbox Code Playgroud)
但是,这种方式不是类型安全的,并且内存管理会变得混乱(例如,Message如果MessageTypeis被破坏,则确保字符串被释放WRITE)。在现代 C++ 中执行此操作的最佳方法是什么?
0x5*_*453 17
这std::variant就是为了。
(但请注意,由于 C++ 没有模式匹配,因此与 Rust 等语言相比,使用变体仍然相当麻烦。)
使用std::variant,您的示例将如下所示:
struct Quit {};
struct Move { int32_t x; int32_t y; };
struct Write { std::string s; };
struct ChangeColor { int32_t r; int32_t g; int32_t b; };
using Message = std::variant<Quit, Move, Write, ChangeColor>;
Run Code Online (Sandbox Code Playgroud)
最接近 Rustmatch表达式的是std::visit. 访问此示例变体可能如下所示:
// Utility to allow overloading lambdas for use in std::visit
template<class... Ts>
struct overload : Ts... {
using Ts::operator()...;
};
template<class... Ts>
overload(Ts...) -> overload<Ts...>;
int main() {
auto visitor = overload{
[](const Quit& q) { std::cout << "Quit\n"; },
[](const Move& m) { std::cout << "Move " << m.x << " " << m.y << "\n"; },
[](const Write& w) { std::cout << "Write " << w.s << "\n"; },
[](const ChangeColor& c) { std::cout << "ChangeColor " << c.r << " " << c.g << " " << c.b << "\n"; }
};
Message m1{Quit{}};
Message m2{Move{1, 2}};
Message m3{Write{"a"}};
Message m4{ChangeColor{1, 2, 3}};
std::visit(visitor, m1);
std::visit(visitor, m2);
std::visit(visitor, m3);
std::visit(visitor, m4);
}
// This prints:
// Quit
// Move 1 2
// Write a
// ChangeColor 1 2 3
Run Code Online (Sandbox Code Playgroud)
小智 5
受到0x5453答案的启发,我编写了一个片段来帮助 Rust 风格的处理std::variant:
// https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
return std::visit(overloaded{ts...}, val);
}
Run Code Online (Sandbox Code Playgroud)
用法:
match(value, [](Type1& type1) {
;
},
[](Type2& type2) {
;
},
...
[](TypeN& typeN) {
;
}
);
Run Code Online (Sandbox Code Playgroud)
例子:
#include <iostream>
#include <variant>
#include <string>
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
template <typename Val, typename... Ts>
auto match(Val val, Ts... ts) {
return std::visit(overloaded{ts...}, val);
}
class A {
public:
A() : v_(0) {}
A(int v) : v_(v) {
std::cout << "Initializing " << V() << std::endl;
}
A(const A& a) : v_(a.v_) {
std::cout << "Copying " << V() << std::endl; }
A(A&& a) : v_(a.v_) {
std::cout << "Moving " << V() << std::endl;
a.Drop();
}
// Explicitly delete it just in case.
A& operator=(const A&) = delete;
A& operator=(A&& a) {
v_ = a.v_;
a.Drop();
std::cout << "Move assigning " << V() << std::endl;
return *this;
}
~A() {
if (V()) {
std::cout << "Deconstructing " << V() << std::endl;
}
}
int V() { return v_; }
private:
void Drop() {
v_ = 0;
}
int v_;
};
auto func(bool choice) -> std::variant<A, int> {
if (choice) {
// Initializing 1
return A(1);
// Moving 1
} else {
return 233;
}
};
auto main() -> int {
A a;
std::cout << match(func(true),
[&](A& ret) -> std::string {
// Move assigning 1
a = std::move(ret);
return "A";
},
[&](int ret) -> std::string {
a = A(ret);
return "int";
}
) << std::endl; // A
// 1
std::cout << a.V() << std::endl;
return 0;
// Deconstructing 1
}
Run Code Online (Sandbox Code Playgroud)
输出:
Initializing 1
Moving 1
Move assigning 1
A
1
Deconstructing 1
Run Code Online (Sandbox Code Playgroud)
参考:
https://en.cppreference.com/w/cpp/utility/variant/visit
https://polomack.eu/std-variant/