将 std::function 应用于访问者设计模式

applying std::function to visitor design pattern

我对 ood 有点陌生。阅读 GoF 的设计模式我发现了 Visitor。

我的访客模式版本比 Generic visitor using variadic templates 中提到的更具体。因此,我的想法是通过在构建期间提供私有 std::functions 来创建具体的访问者。然后,每个访问函数都会调用相应的private std::function

我的问题:如上所述实施访问者是否是一种好的做法,如果不是,为什么?

唯一想到的缺点是歧义,也就是说,很难知道访问者的特定实例将在组合上做什么。

使用 std::function 访问者实现访问者的方式是更改元素的接受部分。你失去了双分派作为成本,但你确实抽象了一点迭代的样板。

代替元素上的一个 accept 方法,每个 种类 访问有一个 accept

当您想在标准访问者中以不止一种方式访问​​事物时,您可以编写更多访问者类型,并添加新的 accept 重载来接受它们。

在基于 std::function 的函数中,您只需编写一个新的 accept 类型的函数 并使用不同的名称;该名称位于方法的名称中,而不是访问者类型的名称中(因为访问者类型是匿名的)。

在具有 SFINAE std::function smarts 的 C++14 中,您可以使用一个重载 accept,但是您必须向访问者传递一个 'visit tag'确定它期望什么样的访问。这可能不值得麻烦。

第二个问题是std::function不支持参数类型的多重重载。 visitor的用途之一就是我们根据元素的动态类型进行不同的调度——full double dispatch.


作为一个具体的案例研究,想象一下3种访问:保存、加载和显示。保存和显示之间的主要区别是显示剔除不可见的东西(被遮挡或设置为不可见)。

在传统的 element/visitor 下,您将有一个带有 3 个重载的接受函数,每个重载采用 Saver*Loader*Displayer*Saver LoaderDisplayer 中的每一个都有一堆 visit(element*)visit(derived_element_type*) 方法。

std::function 访问下,您的元素改为具有 save(std::function<void(element*)>load( 以及 display( 方法。没有进行双重分派,因为 std::function 只公开一个接口。

现在,如果需要,我们可以编写一个 std::function 式的多重调度重载机制。然而,这是高级 C++。


template<class Is, size_t I>
struct add;
template<class Is, size_t I>
using add_t=typename add<Is,I>::type;

template<size_t...Is, size_t I>
struct add<std::index_sequence<Is...>, I>{
  using type=std::index_sequence<(I+Is)...>;
};

template<template<class...>class Z, class Is, class...Ts>
struct partial_apply;
template<template<class...>class Z, class Is, class...Ts>
using partial_apply_t=typename partial_apply<Z,Is,Ts...>::type;

template<template<class...>class Z, size_t...Is, class...Ts>
struct partial_apply<Z,std::index_sequence<Is...>, Ts...> {
  using tup = std::tuple<Ts...>;
  template<size_t I> using e = std::tuple_element_t<I, tup>;

  using type=Z< e<Is>... >;
};

template<template<class...>class Z, class...Ts>
struct split {
  using left = partial_apply_t<Z, std::make_index_sequence<sizeof...(Ts)/2>, Ts...>;
  using right = partial_apply_t<Z, add_t<
    std::make_index_sequence<(1+sizeof...(Ts))/2>,
    sizeof...(Ts)/2
  >, Ts...>;
};
template<template<class...>class Z, class...Ts>
using right=typename split<Z,Ts...>::right;
template<template<class...>class Z, class...Ts>
using left=typename split<Z,Ts...>::left;

template<class...Sigs>
struct functions_impl;

template<class...Sigs>
using functions = typename functions_impl<Sigs...>::type;

template<class...Sigs>
struct functions_impl:
  left<functions, Sigs...>,
  right<functions, Sigs...>
{
   using type=functions_impl;
   using A = left<functions, Sigs...>;
   using B = right<functions, Sigs...>;
   using A::operator();
   using B::operator();
   template<class F>
   functions_impl(F&& f):
     A(f),
     B(std::forward<F>(f))
   {}
};
template<class Sig>
struct functions_impl<Sig> {
  using type=std::function<Sig>;
};

它给你一个 std::function 支持多个签名(但只有一个功能)。要使用它,请尝试以下操作:

functions< void(int), void(double) > f = [](auto&& x){std::cout << x << '\n'; };

当使用 int 调用时,打印一个 int,当使用 double 调用时,打印一个 double。

如前所述,这是高级 C++:我只是将其包括在内是为了说明该语言功能强大,足以处理该问题。

Live example.

使用该技术,您可以使用 std::function 类型的接口进行双重分派。您的简单访问者必须传入一个可调用对象来处理您分派的每个重载,并且您的元素必须详细说明它希望访问者能够在其 functions 签名中支持的所有类型。

您会注意到,如果您实现它,您将在访问的视线中获得一些非常神奇的多态性。将使用您正在访问的事物的 静态类型 动态调用您,您只需编写一个方法体。向合约添加新需求发生在一个地方(在 accept 方法的接口声明处),而不是像 classic 访问那样的 2+K(在 accept 方法中,在访问类型的接口中,在访问的各种重载中的每一个 class(我承认可以用 CRTP 消除)。

上面的functions<Sigs...>存储了N份函数。更优化的方法是存储一次 tur 函数和 N 个调用视图。那是一个更难的触摸,但只是一个触摸。

template<class...Sigs>
struct efficient_storage_functions:
  functions<Sigs...>
{
  std::unique_ptr<void, void(*)(void*)> storage;
  template<class F> // insert SFINAE here
  efficient_storage_functions(F&& f):
    storage{
      new std::decay_T<F>(std::forward<F>(f)),
      [](void* ptr){
        delete static_cast<std::decay_t<F>*>(ptr);
      }
    },
    functions<Sigs...>(
      std::reference_wrapper<std::decay_t<F>>(
        get<std::decay_t<F>>()
      )
    )
    {}
  template<class F>
  F& get() {
    return *static_cast<F*>(storage.get());
  }
  template<class F>
  F const& get() const {
    return *static_cast<F const*>(storage.get());
  }
};

接下来需要通过小对象优化(不将类型存储在堆栈中)和 SFINAE 支持来改进,因此它不会尝试从不兼容的事物构建。

它将传入调用的一个副本存储在 unique_ptr 中,它从所有存储 std::reverence_wrapper 中继承的无数 std::function 到它的内容。

还缺少复制构造。

您在构造时为 visitor 提供 std::function 的想法面临双重分派的挑战:访问者必须为其可能访问的每个具体对象类型实现一个访问函数。

您可以提供一个 std::function 来应对这一挑战(例如:所有具体元素都是同一基础 class 的派生物)。但这并不总是可能的。

另外,访问者不一定是无国籍的。它可以为其访问的每个结构维护状态(例如:维护元素计数或总计)。虽然这在访问者 class 级别很容易编写代码,但在 std::function 级别则更难。这意味着您的访问者实现在其可能的用途上会有一些限制。

因此,我宁愿推荐使用派生访问者 class:这更具可读性,即使具体元素不相关也能工作,并为您提供更大的灵活性,例如有状态访问者。

(在此 other answer 中,您可以找到抽象访问者的简单示例,其中派生的具体状态访问者使用不相关的具体元素)