IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++简单模拟RUST的模式匹配 -> 正文阅读

[C++知识库]C++简单模拟RUST的模式匹配

??最近学习Rust时,对于其模式匹配印象颇为深刻,隐约记得C++似乎也有过类似的提案,翻来覆去还是找到了C++23模式匹配提案。不过等提案到编译器落地估计要个几年,所以这里先通过std::variant做一个简单模拟。
??先展示以下Rust的模式匹配:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

??上述代码首先声明了一个enum对象,然后根据enum对象的实际类型分派到不同的动作。除此之外,还有解包、逻辑运算等功能,不在这里多加描述。主要关注Rust根据一个enum对象的实际类型分派不同动作这样的行为。
??在C++17后,可以使用std::variant定义一个类型安全的union,以模拟Rust中的enum。如下:

int main()
{
    using CommonStr = std::variant<std::string, std::string_view, const char*>;

    CommonStr str1 = "str1";
    CommonStr str2 = "str2"s;
    CommonStr str3 = "str3"sv;
}

??接下来要做根据类型派发,一是可以通过万能的decltype,这里没必要搞这么复杂;二是通过std::variant的配套的一些api,std::holds_alternativestd::get

void dispatch(CommonStr str)
{
    if(std::holds_alternative<std::string>(str))
        std::cout << "std string:" << std::get<std::string>(str) << std::endl;
    else if(std::holds_alternative<std::string_view>(str))
        std::cout << "std string_view:" << std::get<std::string_view>(str) << std::endl;
    else if(std::holds_alternative<const char*>(str))
        std::cout << "c string:" << std::get<const char*>(str) << std::endl;
    else
        std::cout << "invalid string" << std::endl;
}

??这个方法可以写的很通用,不过有一个问题是类型派发是在运行期间进行的。实际上标准库提供了更完善的套件,也就是std::visit。直接来看它怎么使用的:

    std::visit([](auto&& str){
        std::cout << str << std::endl;
    }, str1);

&emsp?第一个参数是一个可调用对象,该可调用对象必须要能接受一个std::variant作为参数,第二个参数就是std::variant变量了。这里省去了我们检查类型是否存在等操作,从std::variant中解包并把可调用对象直接施加在解包后的std::variant中。但这样显然不够。为了做到类型分派,我们需要一个辅助类型,如下:

struct helper {
    void operator()(string_view str) {
        std::cout << "std string_view:" << str << std::endl;
    }
    void operator()(const std::string& str) {
        std::cout << "std string:" << str << std::endl;
    }
    void operator()(const char* str) {
        std::cout << "c string:" << str << std::endl;
    }
};

int main()
{

    CommonStr str1 = "str1";
    CommonStr str2 = "str2"s;
    CommonStr str3 = "str3"sv;

    std::visit(helper{}, str1);
    std::visit(helper{}, str2);
    std::visit(helper{}, str3);
}

??代码运行结果如下:

c string:str1
std string:str2
std string_view:str3

??上面的helper结构体通过函数重载进行自动的派发,这样也不需要我们手动写相关类型判断逻辑了。更重要的是,决定函数调用哪个重载是静态决议的,以降低运行期的负荷。但是每次都需要写一个辅助结构体,不太合理。在cppreference上给出了一个更优雅的解决方案,有以下辅助结构:

// helper type for the visitor #4
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...>;

int main()
{

    std::vector<CommonStr> vec{"str1", "str2"s, "str3"sv};

    for(const auto& str : vec)
        std::visit(overloaded{
            [](std::string_view str){std::cout << "std string_view:" << str << std::endl;},
            [](const std::string& str){std::cout << "std string:" << str << std::endl;},
            [](const char* str){std::cout << "c string:" << str << std::endl;}
        }, str);

}

??唔嗯,这样复用性和简洁性都比之前好了不少。这里传入一个overloaded结构体,并且通过多个针对不同类型参数的lambda去初始化该结构体。
??解释一下上面的代码,由于lambda的实现实际上重载了匿名类的operator()运算符,所以通过using Ts::operator()...;用于将传入的lambda表达式重载后的operator()引入到结构体overloaded中,这就起到了我们手动编写结构体并实现不同重载函数的作用。后面的语句template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;用于限定结构体的类型推断原则。一般而言,要使用实例化的类模板或者结构体模板,必须显示给出模板参数的类型。在C++17之后,可以根据构造函数实际参数的类型来推导模板参数类型,从而减少了一些程序员的代码工作。所以这里也是出于类似的考虑,限定了参数推导模式,使得可以从构造函数参数类型中推导模板参数类型。具体内容参考User-defined deduction guides
??不过还有一些不满意的地方,除去上面奇怪的语法,在Rust中模式匹配对于Option类型也是可用的。那么既然C++也有optional类型,没有理由不让他也使用模式匹配,于是乎有:

using CommonStr = std::variant<std::string, std::string_view, const char*, std::monostate>;

// helper type for the visitor #4
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 T>
concept is_variant = requires (T value) {
    std::holds_alternative<int>(value);
};

template <typename T>
concept is_optional = requires (T value) {
    value = std::nullopt;
};

template <typename ValueType, typename...FuncTypes>
void match(ValueType&& v, FuncTypes&&...funcs)
{
    constexpr auto optioal_match = [](auto v, auto&& f1, auto&& f2) {
        if(v)   f1(v.value());
        else    f2();
    };
    if constexpr (is_variant<std::decay_t<ValueType>>)
        std::visit(overloaded {std::forward<FuncTypes>(funcs)...}, std::forward<ValueType>(v));
    else if constexpr (is_optional<std::decay_t<ValueType>>)
        optioal_match(v, std::forward<FuncTypes>(funcs)...);
    else
        std::cerr << "error match type" << std::endl;
}

constexpr auto UnKnown = [](...) {
    std::cerr << "match error because of unknown type!" << std::endl;
};

constexpr auto Err = [](...) {
    std::cerr << "optional contains a invalid value!" << std::endl;
};

??上面的代码首先把std::visit函数通过match封装起来,看起来就更像Rust了。然后通过concept在编译期区分std::optionalstd::variant类型,然后做出不同的处理。注意针对std::optional的情况只需要接收两个lambda,多余或者不足都将导致编译报错,并且由于第二个lambda是针对optional为std::nullopt的情况,这种情况下通过optional值已经无法获取更多错误信息,所以实际上传入的lambda不需要任何参数。具体使用如下:

int main()
{
    std::vector<CommonStr> vec{"str1", "str2"s, "str3"sv, std::monostate()};
    for(const auto& str: vec)
        match(str,
            [](std::string_view str)    {std::cout << "std string_view  :" << str << std::endl;},
            [](const std::string& str)  {std::cout << "std string       :" << str << std::endl;},
            [](const char* str)         {std::cout << "ctype string     :" << str << std::endl;},
            UnKnown
        );

    std::cout << std::endl;
    std::vector<std::optional<std::string>> vec2{"str1", std::nullopt};
    for(auto var : vec2)
        match(var,
            [](const std::string& v)    {std::cout << "optional value = " << v << std::endl;},
            Err
        );
}

??至少从风格上说更贴近Rust的模式匹配了,也支持std::optional。为了方便加了两个预定义的lambda用于处理错误情况。运行结构如下:

ctype string     :str1
std string       :str2
std string_view  :str3
match error because of unknown type!

optional value = str1
optional contains a invalid value!
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-13 21:33:09  更:2022-03-13 21:34:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:38:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码