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

时间:2015-12-18 19:50:04

标签: c++ c++11 visitor

我对ood有点新鲜。阅读GoF的设计模式我找到了访客。

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

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

只有浮现在脑海中的缺点是含糊不清,也就是说,很难知道访问者的特定实例会对复合做什么。

2 个答案:

答案 0 :(得分:3)

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

而不是元素上的一个accept方法,每个访问一个accept

如果您希望在标准访问者中以多种方式访问​​内容,则可以编写更多访问者类型,并添加新的accept重载以接受它们。

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

在使用SFINAE std::function智能的C ++ 14中,你可以使用一个超载的accept,但是你必须传入一个访问标记'访问者确定它期待什么样的访问。这可能不值得打扰。

第二个问题是std::function不支持参数类型的多次重载。访问者的一个用途是我们根据元素的动态类型进行不同的调度 - 完全双重调度。

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

在传统元素/访问者中,您有一个具有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 - esque多调度重载机制。这是高级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方法的接口声明中),而不是像经典访问那样2 + K(在accept方法中,在访问类型的界面中,以及在每个各种重载中)访问类(可以通过CRTP I&#39; ll承认消除))。

以上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::function中存储了无数std::reverence_wrapper到其内容。

还缺少copy-construct。

答案 1 :(得分:1)

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

您可以提供满足此挑战的单个std::function(例如:所有具体元素都是同一基类的派生)。但这并不总是可行的。

访客也不一定是无国籍人。它可以维持它访问的evry结构的状态(例如:保持元素数或总数)。虽然这很容易在访问者级别编码,但在std::function中更难。这意味着您的访问者实现在其可能的用途中会有一些限制。

因此,我建议使用派生的访客类:这更具可读性,即使具体元素不相关也可以使用,并为有状态访问者提供更多灵活性。

(在这个other answer中,您可以找到一个抽象访问者的天真示例,其中派生的具体有状态访问者使用不相关的具体元素)