C++设计模式之访问者模式-C++17的visit

时间:2024-03-21 11:06:14

C++17提供了std::visit和std::variant来帮助实现访问者模板,std::variant可以用来替代类的继承和多态。

using Pets = std::variant<class Cat, class Dog>;

template<typename Visitor, typename Pet>
void do_visit(const Visitor& v, const Pet& p) {
    std::visit(v, Pets{p});
}

class Cat {
public:
    Cat(const std::string& color) : color_(color) {}
    const std::string& color() const { return color_ ;}

private:
    std::string color_;
};

class Dog {
 // same as Cat
};

可以看到使用Pets对Cat及Dog进行包装,std::variant可以简单理解成union,其中可以存储Cat及Dog中类型的任意一种的对象。

然后Cat和Dog都没有继承和accept函数的实现,使用这种方式就省掉了这两个。

最后使用std::visit针对Dog或者Cat进行访问,使用Visitor和封装成Pets对象的两个参数调用。

那么这里Visitor如何实现才可以访问类型呢?

template<typename ...F>
struct overload_set : public F... {
    using F::operator()...;
};

template<typename ...F>
overload_set(F&& f) -> overload_set<F...>;

我们先来看overload_set这个实现,在很多开源库相信大家也看到过,我来解释下这段代码:
overload_set的模板参数这里一般是lambda表达式的类型,overload_set继承后使用using F::operator()...这行代码可以做到使用仿函数的形式去调用到父类也即不同的参数类型的lambda表达式的函数。然后使用c++17才有的模板参数推导帮助实例化时省去写模板参数的步骤,举例来说:

overload_set l{
    [](int i) {
        std::cout << "i=" << i << std::endl;
    },
    [](double d) {
        std::cout << "d=" << d << std::endl;
    },
};
l(3);
l(34.56);

我们定了一个overload_set的对象,这里就不用写模板参数,然后使用对象加上括号的形式就可以进行调用了。

那么其实std::visit就是将variant中实际存放的类型拆出来去调用Visitor仅此而已,那么我们自己实现一下自己的Visitor及使用代码:

auto pv = overload_set(
    [](const Cat& c) {
        std::cout << "Let the cat " << c.color() << std::endl;
    },
    [](const Dog& d) {
        std::cout << "Take the dog " << d.color() << std::endl;
    },
);

Cat c("orange");
Dog d("brown");

do_visit(pv, c);
do_visit(pv, d)

当然这里也还有比较简便的易于理解的定制Visitor的写法:

auto realVisit = [](const auto& val) 
{
    using T = std::decay_t<decltype(val)>;
    if constexpr (std::is_same_v<T, Cat>) {
        // do something
    }
    else if ( ... ) {

    }
};

使用泛型的lambda表达式,内部在判断类型,看你喜欢哪种形式了。