C++简单模拟RUST的模式匹配

  最近学习Rust时,对于其模式匹配印象颇为深刻,隐约记得C++似乎也有过类似的提案,翻来覆去找到了C++23模式匹配提案。不过等提案落地估计要个几年,所以这里先通过std::variant做一个简单模拟。
  先展示Rust的模式匹配,例子取自match 控制流运算符 – 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::visit第一个参数是一个可调用对象,该可调用对象必须要能接受一个std::variant作为参数,所以一般来说这个参数类型声明成auto或者auto&&型别;第二个参数就是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...>;用于限定结构体的类型推断原则。一般而言,要使用模板类,必须显示给出模板参数的类型,像是std::vector<int> vec{1, 2, 3};这种用法,都是显示的给出模板类中模板参数的类型。在C++17之后,就可以根据构造函数实际参数的类型来推导模板参数类型,从而减少了一些程序员的代码工作,比如在C+++17可以直接这么写:std::vector vec{1, 2, 3};,这里编译器通过构造函数为我们自动推导了模板参数类型。但是并不是所有模板类都可以让编译器帮忙推导模板参数类型,尤其是我们自己编写的模板类iui。所以C++提供了一些方法用于限定参数推导模式,使得我们自己编写的模板类也可以从构造函数参数类型中推导模板参数类型。具体内容参考User-defined deduction guidestemplate<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;这个奇怪的语句,就是用于声明,当使用构造函数时,如何通过构造函数的参数类型去推导模板参数类型,免去我们手写模板参数的工作。

  不过还有一些不满意的地方,除去上面奇怪的语法,在Rust中模式匹配对于Option类型也是可用的。那么既然C++也有optional类型,没有理由不让他参与到模式匹配中,于是乎有:

// 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>>)
    {
        if constexpr (sizeof...(FuncTypes) != 2)
        {
            std::cerr << "match error! optional need 2 functors" << std::endl;
            std::terminate();
        }
        else
            optioal_match(v, std::forward<FuncTypes>(funcs)...);
    }
    else
        std::cerr << "match error! dismatch 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类型,然后做出不同的处理。我对于concept研究不多,这么写感觉比较丑,若有更优雅的写法麻烦提醒一波。注意针对std::optional的情况只需要接收两个lambda,多余或者不足都将导致编译报错,并且由于第二个lambda是针对optional为std::nullopt的情况,这种情况下通过optional值已经无法获取更多错误信息,所以实际上传入的lambda不需要任何参数。具体使用如下:

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

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用于处理错误情况,Unknown表示variant中不存在的类型,Err表示optioal未被设置。运行结果如下:

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

optional value = str1
optional contains a invalid value!
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享