Visit and Overload Pattern

Posted on Tue 14 March 2023 in Journal

Abstract Visit and Overload Pattern
Authors Walter Fan
 Category    learning note  
Status v1.0
Updated 2023-03-14
License CC-BY-NC-ND 4.0

背景

Visitor 模式是经典的设计模式, 它的意图是表示一个作用于某对象结构中的各元素的操作, 可以在不改变各元素的类的前提下定义作用于这些元素的新操作.

我们可以针对一个或者多个类添加新的操作方法, 而不必修改这个类, 核心方法就是定义一个 visitor 访问者, 访问者针对不同的元素实施不同的操作.

在现在 C++ 中引入了 std::visit 和 std::variant , 让这些常用的操作变的简单

template <class Visitor, class... Variants>
constexpr visit( Visitor&& vis, Variants&&... vars);

template <class R, class Visitor, class... Variants>
constexpr R visit( Visitor&& vis, Variants&&... vars );

std::visit 可将 visitor 应用于 variants 容器。 visitor 必须是可调用的。 callable 可调用对象可以是函数、函数对象或 lambda。

示例

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>

template<typename ... Args>
void printMe(Args&& ... args) {
    (std::cout <<  ... <<  std::forward<Args>(args)) << '\n';
}

// the variant to visit
using var_t = std::variant<int, long, double, std::string>;

// helper constant for the visitor #3
template<class> inline constexpr bool always_false_v = false;

// 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...>;

class PrintVisitor {
public:
    void operator()(int i) {
        std::cout << "(int)" << i << std::endl;
    }
    void operator()(long i) {
        std::cout << "(long)" << i << std::endl;
    }
    void operator()(double i) {
        std::cout << "(double)" << i << std::endl;
    }
    void operator()(std::string str) {
        std::cout << "(string)" << str << std::endl;
    }
};

int main() {


    std::vector<var_t> vec = {10, 15l, 1.5, "hello"};

    printMe(
        std::get<int>(vec[0]), ", ",
        std::get<long>(vec[1]), ", ",
        std::get<double>(vec[2]),", ",
        std::get<std::string>(vec[3]));

    for (auto& v: vec) {

        // 1. void visitor, only called for side-effects (here, for I/O)
        std::visit([](auto&& arg){std::cout << arg;}, v);

        // 2. value-returning visitor, demonstrates the idiom of returning another variant
        var_t w = std::visit([](auto&& arg) -> var_t {return arg + arg;}, v);

        // 3. type-matching visitor: a lambda that handles each type differently
        std::cout << ". After doubling, variant holds ";
        std::visit([](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, int>)
                std::cout << "int with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, long>)
                std::cout << "long with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, double>)
                std::cout << "double with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, std::string>)
                std::cout << "std::string with value " << std::quoted(arg) << '\n';
            else 
                static_assert(always_false_v<T>, "non-exhaustive visitor!");
        }, w);
    }


    std::cout << "-------- visit variant with PrintVisitor --------" << std::endl;
    for (auto& v: vec) {
        std::visit(PrintVisitor(), v);
    }

    std::cout << "-------- visit variant with overloaded operator --------" << std::endl;
    for (auto& v: vec) {
        // 4. another type-matching visitor: a class with 3 overloaded operator()'s
        // Note: The `(auto arg)` template operator() will bind to `int` and `long`
        //       in this case, but in its absence the `(double arg)` operator()
        //       *will also* bind to `int` and `long` because both are implicitly
        //       convertible to double. When using this form, care has to be taken
        //       that implicit conversions are handled correctly.
        std::visit(overloaded {
            [](auto arg) { std::cout << arg << std::endl; },
            [](double arg) { std::cout << std::fixed << arg << std::endl; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << std::endl; }
        }, v);
    }
}

tips

struct Overload : Ts ... { using Ts::operator() ... ; }; ``` 上述这个结构体很特别, 它可以有任意的父类, 而它所做的就是将这些父类的调用操作符作为一个自己的一个公用成员函数


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。