Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Visitor pattern with smart pointers

I am trying to implement Oppen's algorithm in C++.

The basic routines in this algorithm (print and scan) dispatch on a token type. It seems natural to implement this dispatch using the visitor pattern. The problem is: the routines are nested and the arguments to print() are enqueued in a stack during scan(). In order to avoid any memory problems, I would like to use smart pointers for the task.

So my implementation looks like this:

class Text;
class Line;
class Open;
class Close;

class Visitor {
  /* Define virtual visit functions for concrete doc nodes:
   */
public:
  virtual void visit(const Text&) = 0;  
  virtual void visit(const Line&) = 0;  
  virtual void visit(const Open&) = 0;  
  virtual void visit(const Close&) = 0; 
};


class DocToken
{
protected:
  explicit DocToken() {}

  friend class Visitor;

public:
  virtual void accept(Visitor * visitor) const = 0;
};

class Text : public DocToken {
public:
  Text(std::string s) : text(s) {} 
  void accept(Visitor *visitor) const {
    visitor -> visit (*this);
  }
  std::string text;
};

class Open : public DocToken { /* .. */ }

/* .. */

class Scan : public Visitor {
  stream_t stream;
  /* ... */
public:
  void visit(const Open& x) {
    /* ... */ 
    stream.push_back(/* .. */ new Open() /* .. */);
    /* ... */ 
  }

  void visit(const Text& x) {
    /* ... */ 
    stream.push_back(/* .. */ new Text(x) /* .. */);
    /* ... */ 
  }
  /* .. */
}

As you can see, the Open token does not carry any data and can be constructed in place easily. The Text token does carry data (a std::string) and has to be copied in order to be pushed into the stream. The stream needs to consist of pointers due to the common abstract base class of Open and Text.

Since on the outside, there is a smart pointer to that text token, I'd like to avoid the copying and simply use the existing smart pointer. However, the accept method does not have access to that smart pointer.

Is there a way to implement a visitor pattern directly on smart-pointers? If not, how can I reduce the cost of copying the text token?

like image 471
choeger Avatar asked Sep 29 '16 08:09

choeger


1 Answers

Technically, You can do this using std::enable_shared_from_this. (Note Pete Kirkham's excellent comment to the question, though - shared pointers indicate ownership. This is applicable to visitors that might outlive their originating documents, e.g., an ad-hoc dictionary builder, which might live after the document has been closed. Where no ownership is involved, raw pointers are the way to go.)

Below is a simplified version of your code illustrating this.

Say we start with the usual visitor-pattern forward declarations and base class definitions.

#include <memory>
#include <vector>
#include <iostream>

struct token;

struct visitor;

struct token {
    virtual void accept(visitor &v) = 0;
};

struct text_token;
struct open_token;

When we define visitor, we make it accept std::shared_ptrs of the options:

struct visitor {
    virtual void accept(std::shared_ptr<text_token> p) = 0;
    virtual void accept(std::shared_ptr<open_token> p) = 0;
};

Now when we make concrete tokens, we:

  1. subclass std::enable_shared_from_this
  2. use shared_from_this to pass on the argument to accept

so the concrete tokens become:

struct text_token : public token, public std::enable_shared_from_this<text_token> {
    virtual void accept(visitor &v) override {
        std::shared_ptr<text_token> p{shared_from_this()};
        v.accept(p);
    }   
};

struct open_token : public token, public std::enable_shared_from_this<open_token> {
    virtual void accept(visitor &v) override {
        std::shared_ptr<open_token> p{shared_from_this()};
        v.accept(p);
    }   
};

The concrete visitor doesn't change by much:

struct scan : public visitor {
    virtual void accept(std::shared_ptr<text_token>) override {
        std::cout << "accepting text" << std::endl;
    }
    virtual void accept(std::shared_ptr<open_token>) override {
        std::cout << "accepting open" << std::endl;
    }   
};

Now we can define a range of std::shared_ptrs to tokens

int main() {
    std::vector<std::shared_ptr<token>> toks;
    toks.push_back(std::make_shared<text_token>());
    toks.push_back(std::make_shared<open_token>());

And call accept on them:

    scan s;
    for(auto p: toks)
       p->accept(s);
}

When run, it prints:

$ ./a.out 
accepting text
accepting open

Full Code

#include <memory>
#include <vector>
#include <iostream>

struct token;

struct visitor;

struct token {
    virtual void accept(visitor &v) = 0;
};

struct text_token;
struct open_token;

struct visitor {
    virtual void accept(std::shared_ptr<text_token> p) = 0;
    virtual void accept(std::shared_ptr<open_token> p) = 0;
};

struct text_token : public token, public std::enable_shared_from_this<text_token> {
    virtual void accept(visitor &v) override {
        std::shared_ptr<text_token> p{shared_from_this()};
        v.accept(p);
    }   
};

struct open_token : public token, public std::enable_shared_from_this<open_token> {
    virtual void accept(visitor &v) override {
        std::shared_ptr<open_token> p{shared_from_this()};
        v.accept(p);
    }   
};

struct scan : public visitor {
    virtual void accept(std::shared_ptr<text_token>) override {
        std::cout << "accepting text" << std::endl;
    }
    virtual void accept(std::shared_ptr<open_token>) override {
        std::cout << "accepting open" << std::endl;
    }   
};

int main() {
    std::vector<std::shared_ptr<token>> toks;
    toks.push_back(std::make_shared<text_token>());
    toks.push_back(std::make_shared<open_token>());

    scan s;
    for(auto p: toks)
       p->accept(s);
}
like image 188
Ami Tavory Avatar answered Nov 02 '22 08:11

Ami Tavory