While learning Rust recently, I was quite impressed by its pattern matching. I vaguely remembered that C++ seemed to have a similar proposal, and I found C++23 pattern matching proposal by going back and forth. However, it will probably take several years for the proposal to arrive, so a simple simulation will be done through STD :: Variant. Let’s start by showing Rust pattern matching, an example from the Match control flow operator – Rust programming language:

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,}}Copy the code

The above code first declares an enum object and then dispatches different actions based on the actual type of the enum object. In addition, there are unpacking, logical operations and other functions, not described here. It focuses on the behavior of Rust to dispatch different actions based on the actual type of an enum object. After C++17, you can define a type-safe union using STD ::variant to emulate the enum in Rust. As follows:

int main(a)
{
    using CommonStr = std::variant<std::string, std::string_view, const char* >; CommonStr str1 ="str1";
    CommonStr str2 = "str2"s;
    CommonStr str3 = "str3"sv;
}
Copy the code

Next to do according to the type of distribution, one is through the universal decltype, there is no need to do so complex; Some apis for STD :: Variant, STD :: Holds_alternative and STD :: 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;
}
Copy the code

This method can be written generically, but one problem is that type dispatch occurs at run time. In fact, the standard library provides a more complete suite, namely STD :: VISIT. To see how it works directly:

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

The first argument to &emsp STD ::visit is a callable that must accept a STD :: Variant, so it is generally declared as auto or auto&&. The second parameter is the STD :: Variant variable. This function can be unpacked from STD :: Variant and apply callable objects directly to the unpacked object. But that’s not enough. To do type dispatch, we need a helper type like this:

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

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

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

The code results are as follows:

c string:str1
std string:str2
std string_view:str3
Copy the code

The above helper structure is distributed automatically through function overloading, so that we do not need to write the type determination logic manually. More importantly, deciding which overload to call a function is statically resolved to reduce runtime load. But it doesn’t make sense to write an auxiliary structure every time. A more elegant solution is given on CPpreference with the following auxiliary structure:

// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator(a).; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...)-> overloaded<Ts... >;

int main(a)
{

    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); }Copy the code

Well, it’s much more reusable and much more concise than before. Here an overloaded structure is passed and initialized with multiple Lambdas for different types of parameters. To explain the code above, since lambda’s implementation actually overrides the operator() operator of an anonymous class, using Ts::operator()… ; The operator() that is used to overload the passed lambda expression to the overloaded structure overloaded serves the purpose of manually writing the overloaded structure and implementing the different overloaded functions. The statement template

overloaded(Ts…) -> overloaded

; Type inference principles used to qualify structures. In general, to use a template class, you must display the types that give the template arguments, such as STD ::vector

vec{1, 2, 3}; This usage is used to display the type of the template parameter in the template class. After C++17, it was possible to derive template parameter types from the types of the actual constructor arguments, thus reducing some programmer’s code work. For example, in C++17, we could simply write STD ::vector vec{1, 2, 3}; , where the compiler automatically deduces template parameter types for us via constructors. But not all template classes allow the compiler to derive template parameter types, especially the iUI class we wrote ourselves. So C++ provides methods to qualify the argument inference pattern so that our own template classes can also derive template argument types from constructor argument types. See user-defined deduction guides for details. template

overloaded(Ts…) -> overloaded

; This is a strange statement that states how to derive template parameter types from constructor parameter types when using constructors, sparing us the task of writing template parameters by hand. There are a few gripes, though. Despite the odd syntax above, pattern matching is also available for Option types in Rust. Since C++ also has optional types, there is no reason not to include them in pattern matching:




// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator(a).; };
// 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(a); };if constexpr (is_variant<std::decay_t<ValueType>>)
        std::visit(overloaded {std::forward
       
        (funcs)... }, std::forward
        
         (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(a); }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;
};
Copy the code

The above code first wraps the STD :: Visit function with match, which looks more like Rust. It then uses a simple concept to distinguish STD :: Optional from STD :: Variant at compile time and then handles it differently. I don’t have much research on concept, so it feels ugly to write like this. Please remind me if there is a more elegant writing method. Note that only two lambdas are required for STD :: Optional. Any more or less will result in an error, and since the second lambda is for optional STD :: Nullopt, there is no further error in this case with optional. So actually the lambda passed in doesn’t need any arguments. Specific use is as follows:

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

int main(a)
{
    std::vector<CommonStr> vec{"str1"."str2"s, "str3"sv, std::monostate(a)};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 ); }Copy the code

At least stylistically more closely matches Rust’s pattern, and also supports STD :: Optional. For convenience, two predefined lambdas are added to handle error cases, with Unknown indicating that a type does not exist in variant and Err indicating that optioal is not set. The running results are as follows:

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

optional value = str1
optional contains a invalid value!
Copy the code