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 overloaded(Ts...) -> overloaded;用于限定结构体的类型推断原则。一般而言,要使用实例化的类模板或者结构体模板,必须显示给出模板参数的类型。在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++,rust,c++,开发语言)