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

C++ 将std::function应用于访问者设计模式,c++,c++11,visitor,C++,C++11,Visitor,我对ood有些陌生。阅读GoF的设计模式,我找到了访客 我的访客模式版本比中提到的更具体。因此,我的想法是通过在构建过程中提供私有std::函数来创建具体的访问者。然后,每个访问函数将调用相应的私有std::函数 我的问题是:实现上面提到的访问者是一种好的实践吗?如果不是,为什么 唯一让人想到的缺点是模棱两可,也就是说,很难知道访问者的特定实例会在合成上做什么 使用std::function visitors实现visitor的方法是更改元素的accept部分。您失去了双重调度的成本,但您确实有

我对ood有些陌生。阅读GoF的设计模式,我找到了访客

我的访客模式版本比中提到的更具体。因此,我的想法是通过在构建过程中提供私有std::函数来创建具体的访问者。然后,每个访问函数将调用相应的私有std::函数

我的问题是:实现上面提到的访问者是一种好的实践吗?如果不是,为什么


唯一让人想到的缺点是模棱两可,也就是说,很难知道访问者的特定实例会在合成上做什么

使用std::function visitors实现visitor的方法是更改元素的accept部分。您失去了双重调度的成本,但您确实有点抽象了迭代的样板文件

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

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

在基于std::function的函数中,只需编写一个具有不同名称的新接受类型函数;该名称位于方法的名称中,而不是访问者类型的名称中,因为访问者类型是匿名的

在使用SFINAE std::function smarts的C++14中,您可以使用一个重载的accept,但随后必须向访问者传递一个“visit标记”,以确定它所期望的访问类型。这可能不值得费心

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

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

在传统的element/visitor模式下,您将有一个带有3个重载的accept函数,每个重载包含一个Saver*或一个Loader*或一个Displayer*。每个Saver Loader和Displayer都有一组visitelement*和visitderived_element_type*方法

在std::function-visitoring下,元素有一个savestd::函数、一个load和一个display方法。没有执行双重分派,因为std::function只公开一个接口

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

当用int调用时,打印int,当用double调用时,打印double

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

使用这种技术,您可以使用std::function类型接口进行双重分派。visitor simple必须传入一个callable,该callable可以处理您分派的每个重载,并且元素必须详细说明它希望visitor在其签名函数中能够支持的所有类型

您会注意到,如果您实现了这一点,您将在访问现场获得一些真正神奇的多态性。您将使用正在访问的对象的静态类型动态地调用您,并且您只需要编写一个方法体。在accept方法的接口声明中,向合同添加新的需求发生在一个位置,而不是accept方法中的2+K,在visit类型的接口中,在visit类的各个重载中,我承认可以用CRTP消除这些重载

上述函数存储函数的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,这样它就不会试图从不兼容的东西构建

它将传入可调用函数的一个副本存储在一个唯一的\u ptr中,它从所有函数继承的无数std::函数将std::reverence\u包装存储到其内容中

同样缺少的是复制构造。

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

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

此外,访问者不一定是无国籍的。它可以维护它访问的每一个结构的状态,例如:维护元素计数或总计。虽然在visitor类级别编写代码很容易,但在std::函数中编写代码更难。这意味着您的访问者实现将有一些限制 在可能的用途上存在着分歧

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


在这里,您可以找到一个抽象访问者的简单示例,一个派生的具体有状态访问者使用不相关的具体元素

当然可以,但可能不清楚每个访问者的行为是什么。在您的实现下,我可以有两个相同类型的访问者,他们的访问方式不同。我正在考虑转发参数。我看了Scott Meyers关于转发的谈话,感到非常害怕。这可能是一种危险吗?不,转发您的参数会保留它们的l值或r值性质。只有当你从一个你不应该有的物体上移开时才有危险。
functions< void(int), void(double) > f = [](auto&& x){std::cout << x << '\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());
  }
};