如何使访问者接口适应迭代器接口?

发布于 2024-10-28 07:55:25 字数 464 浏览 2 评论 0原文

我想知道是否有一个好的设计模式或习惯用法来实现以下内容:

您有一个仅提供访问者界面的现有类,如下所示

类访客{
民众:
  虚拟 ~Visitor() { }
  虚拟无效访问(节点*n)= 0;
};

类树{
民众:
  无效接受(访客*v);
};

并且您希望有一个可以按如下方式使用的接口,该接口应该按照访问者调用其 visit 函数的顺序迭代树。

for(迭代器 it(...), ite(...); it != ite; ++it) {
  /* 进程节点 */
}

问题似乎是,当我们只调用 visit 时,我们就失去了控制,并且无法暂时“返回”循环体来执行一个节点的操作。这看起来应该在现实世界的程序中经常发生。知道如何解决吗?

I'm wondering whether there is a good design pattern or idiom to realize the following:

You have an existing class that provides only a visitor interface, as follows

class Visitor {
public:
  virtual ~Visitor() { }
  virtual void visit(Node *n) = 0;
};

class Tree {
public:
  void accept(Visitor *v);
};

And you want to have an interface that can be used as follows, which should iterate through the tree in the same order that the visitor would have its visit function called.

for(iterator it(...), ite(...); it != ite; ++it) {
  /* process node */
}

The problem appears to be that when we just call visit, we are out of control, and can't temporarily "go back" to the loop body to execute the action for one node. This looks like it should occur regularly in real world programs. Any idea how to solve it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

做个少女永远怀春 2024-11-04 07:55:26

在一般情况下,我认为这是不可能的,至少不可能。

至少按照通常的定义,迭代器期望处理同构集合。即,迭代器通常定义如下:

template <class Element>
class iterator // ...

...因此特定迭代器只能处理一种特定类型的元素。要处理不同类型,您最多可以做的是创建一个指向基类(指向基类的指针/引用)的迭代器,并让它处理派生类的对象。

相比之下,编写这样的访问者非常容易:

class MyVisitor {
public:
    void VisitOneType(OneType const *element);
    void VisitAnotherType(AnotherType const *element);
};

它可以访问 OneTypeAnotherType 的节点,即使两者完全无关。基本上,对于能够访问的每种不同类型的,您的 Visitor 类中都有一个 Visit 成员函数。

从稍微不同的方向来看,迭代器基本上是一种专门形式的访问者,仅适用于一种类型的对象。您交换了对访问模式的更多控制权,以换取失去访问不相关类型的对象的能力。

如果您只需要处理一种类型(尽管一种类型可能是基类,并且访问的对象具有各种派生类型),那么显而易见的方法是构建一个访问对象的“桥梁”类(<代码>Tree 节点,在您的示例中),当调用它的 visit 时,它只是将其正在访问的节点的地址复制到某个支持迭代器的集合中:

template <class T>
class Bridge { 
    std::vector<T *> nodes;
public:
    virtual void visit(T *n) { 
        nodes.push_back(n);
    }

    typedef std::vector<T *>::iterator iterator;

    iterator begin() { return nodes.begin(); }
    iterator end() { return nodes.end(); }
};

使用这将是两个步骤过程:首先像访问者通常那样访问节点,然后将感兴趣的节点收集在一起,您可以迭代它们,就像您对任何其他提供迭代器的集合一样。此时,您的访问模式仅受您在桥中使用的集合提供的迭代器类的限制。

In the general case, I don't think it's possible, at least not cleanly.

At least as it's usually defined, an iterator expects to deal with a homogeneous collection. I.e., an iterator is normally defined something like:

template <class Element>
class iterator // ...

...so a specific iterator can only work with elements of one specific type. The most you can do to work with differing types is create an iterator to (a pointer/reference to) a base class, and let it deal with objects of derived classes.

By contrast, it's pretty easy to write a visitor like this:

class MyVisitor {
public:
    void VisitOneType(OneType const *element);
    void VisitAnotherType(AnotherType const *element);
};

This can visit nodes of either OneType or AnotherType, even if the two are completely unrelated. Basically, you have one Visit member function in your Visitor class for every different type of class that it will be able to visit.

Looked at from a slightly different direction, an iterator is basically a specialized form of visitor that only works for one type of object. You exchange a little more control over the visitation pattern in exchange for losing the ability to visit unrelated types of objects.

If you only need to deal with one type (though that one type may be a base class, and the visited objects are of various derived types), then the obvious method would be to build a "bridge" class that visits objects (Tree nodes, in your example), and when its visit is called, it just copies the address of the node it's visiting into some collection that supports iterators:

template <class T>
class Bridge { 
    std::vector<T *> nodes;
public:
    virtual void visit(T *n) { 
        nodes.push_back(n);
    }

    typedef std::vector<T *>::iterator iterator;

    iterator begin() { return nodes.begin(); }
    iterator end() { return nodes.end(); }
};

Using this would be a two-step process: first visit the nodes like a visitor normally would, then having collected together the nodes of interest you can iterate through them just like you would any other collection that provides iterators. At that point, your visitation pattern is limited only by the class of iterator provided by the collection you use in your bridge.

幻想少年梦 2024-11-04 07:55:26

我在现实环境中遇到了这个问题,使用提供访问者接口的 R 树实现,而我需要一个迭代器接口。仅当您接受将所有结果存储在集合中时,Jerry 的上述建议才有效。如果您的结果集很大并且您实际上不需要存储它们,这可能会导致高内存消耗。

一种肯定有效的解决方案是在单独的线程中启动访问者并开始等待条件变量的结果。当进行访问调用时,您将当前结果存储到共享临时位置,并使用另一个条件变量来等待下一个请求。在您自己等待之前,您可以向调用者(主)线程的条件变量发出信号。然后,实现迭代器接口的调用者可以返回存储在临时位置的值。在下一次迭代期间,它可以向访问者线程的条件变量发出信号,并自行等待下一个项目。不幸的是,如果您针对每个项目执行此操作,成本会有些高。您可以缓冲一些项目以提高性能。

我们真正需要的是一个额外的堆栈并在两个上下文之间交替。这种抽象是由协程提供的。在 C++ 中,boost::coroutine 提供了一个干净的实现。下面我提供了一个完整的示例,说明如何将访问者模式改编为迭代器模式。

#include <iostream>
#include <boost/bind.hpp>
#include <boost/coroutine/coroutine.hpp>

template<typename Data>
class Visitor
{
public:
  virtual ~Visitor() { }
  virtual bool visit(Data const & data) = 0;
};

template <typename Data>
class Visitable    
{
public:
    virtual ~Visitable() {}
    virtual void performVisits(Visitor<Data> & visitor) = 0;
};

// Assume we cannot change the code that appears above

template<typename Data>
class VisitableIterator : public Visitor<Data>
{
private:
    typedef boost::coroutines::coroutine<void()> coro_t;
public:
    VisitableIterator(Visitable<Data> & visitable)
        : valid_(true), visitable_(visitable)
    {
        coro_ = coro_t(boost::bind(&VisitableIterator::visitCoro, this, _1));
    }
    bool isValid() const
    {
        return valid_;
    }
    Data const & getData() const
    {
        return *data_;
    }
    void moveToNext()
    {
        if(valid_) 
            coro_();
    }
private:
    void visitCoro(coro_t::caller_type & ca)
    {
        ca_ = & ca;
        visitable_.performVisits(*static_cast<Visitor<Data> *>(this));
        valid_ = false;
    }
    bool visit(Data const & data)
    {
        data_ = &data;
        (*ca_)();
        return false;
    }
private:
    bool valid_;
    Data const * data_;
    coro_t coro_;
    coro_t::caller_type * ca_;
    Visitable<Data> & visitable_;
};

// Example use below

class Counter : public Visitable<int>
{
public:
    Counter(int start, int end)
        : start_(start), end_(end) {}
    void performVisits(Visitor<int> & visitor)
    {
        bool terminated = false;
        for (int current=start_; !terminated && current<=end_; ++current)
            terminated = visitor.visit(current);
    }
private:
    int start_;
    int end_;
};

class CounterVisitor : public Visitor<int>
{
public:
    bool visit(int const & data)
    {
        std::cerr << data << std::endl;
        return false; // not terminated
    }
};

int main(void)
{
    { // using a visitor
        Counter counter(1, 100);
        CounterVisitor visitor;
        counter.performVisits(visitor);
    }
    { // using an iterator
        Counter counter(1, 100);
        VisitableIterator<int> iter(static_cast<Visitable<int>&>(counter));
        for (; iter.isValid(); iter.moveToNext()) {
            int data = iter.getData();
            std::cerr << data << std::endl;
        }
    }
    return EXIT_SUCCESS;
}

I had this problem in a real-world setting with an R-tree implementation that provided a visitor interface, whereas I needed an iterator interface. The suggestion by Jerry above works only if you can accept storing all the results in a collection. That may result in high memory consumption if your result set is huge and you don't really need to store them.

One solution that will work for sure is to launch the visitor in a separate thread and start waiting on a conditional variable for the results. When a visit call is made, you store the current result into a shared temp location, and use another conditional variable to wait for the next request. You signal the caller (main) thread's conditional variable before you wait on your own. The caller, which is implementing the iterator interface can then return the value stored at the temp location. During the next iteration, it could signal the visitor thread's conditional variable, and wait on its own for the next item. Unfortunately, this is somewhat costly if you do it on a per-item basis. You can buffer some items to improve the performance.

What we really need is an extra stack and to alternate between two contexts. This abstraction is provided by coroutines. In C++, boost::coroutine provides a clean implementation. Below I include a full example of how visitor pattern can be adapted into an iterator pattern.

#include <iostream>
#include <boost/bind.hpp>
#include <boost/coroutine/coroutine.hpp>

template<typename Data>
class Visitor
{
public:
  virtual ~Visitor() { }
  virtual bool visit(Data const & data) = 0;
};

template <typename Data>
class Visitable    
{
public:
    virtual ~Visitable() {}
    virtual void performVisits(Visitor<Data> & visitor) = 0;
};

// Assume we cannot change the code that appears above

template<typename Data>
class VisitableIterator : public Visitor<Data>
{
private:
    typedef boost::coroutines::coroutine<void()> coro_t;
public:
    VisitableIterator(Visitable<Data> & visitable)
        : valid_(true), visitable_(visitable)
    {
        coro_ = coro_t(boost::bind(&VisitableIterator::visitCoro, this, _1));
    }
    bool isValid() const
    {
        return valid_;
    }
    Data const & getData() const
    {
        return *data_;
    }
    void moveToNext()
    {
        if(valid_) 
            coro_();
    }
private:
    void visitCoro(coro_t::caller_type & ca)
    {
        ca_ = & ca;
        visitable_.performVisits(*static_cast<Visitor<Data> *>(this));
        valid_ = false;
    }
    bool visit(Data const & data)
    {
        data_ = &data;
        (*ca_)();
        return false;
    }
private:
    bool valid_;
    Data const * data_;
    coro_t coro_;
    coro_t::caller_type * ca_;
    Visitable<Data> & visitable_;
};

// Example use below

class Counter : public Visitable<int>
{
public:
    Counter(int start, int end)
        : start_(start), end_(end) {}
    void performVisits(Visitor<int> & visitor)
    {
        bool terminated = false;
        for (int current=start_; !terminated && current<=end_; ++current)
            terminated = visitor.visit(current);
    }
private:
    int start_;
    int end_;
};

class CounterVisitor : public Visitor<int>
{
public:
    bool visit(int const & data)
    {
        std::cerr << data << std::endl;
        return false; // not terminated
    }
};

int main(void)
{
    { // using a visitor
        Counter counter(1, 100);
        CounterVisitor visitor;
        counter.performVisits(visitor);
    }
    { // using an iterator
        Counter counter(1, 100);
        VisitableIterator<int> iter(static_cast<Visitable<int>&>(counter));
        for (; iter.isValid(); iter.moveToNext()) {
            int data = iter.getData();
            std::cerr << data << std::endl;
        }
    }
    return EXIT_SUCCESS;
}
抱猫软卧 2024-11-04 07:55:26

在访问者实现中构建遍历逻辑确实不灵活。可以通过访问者组合器<来干净地分离遍历复合结构和访问。 /a> (还有其他论文,请随意谷歌搜索)。

这些关于同一主题的幻灯片您可能也会感兴趣。他们解释了如何获得干净的语法 à la boost::spirit 规则。

Building traversal logic in the visitors implementations is indeed not flexible. A usable way to cleanly separate traversing composite structures from visitation may be done via visitor combinators (there are other papers, feel free to google for them).

These slides about the same topic may also be of interest. They explain how to get clean syntax à la boost::spirit rules.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文