C++ Visitor pattern with smart pointers
Asked Answered
S

1

7

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?

Stook answered 29/9, 2016 at 8:38 Comment(8)
What is your definition of Visitor? (Also, I wouldn't worry too much about the copying until you have profiled to show it is a problem).Vlissingen
I added the implementation. Classical visitor pattern, AFAIK. And yeah, the optimization might not be worth it, but if it was, I still would not know how to do it properly. So I ask ;)Stook
"Since on the outside, there is a smart pointer to that text token" Are you storing each of the tokens in a shared_ptr? I.e., do you have a range of shared pointers (to the base class), and you pass the visitor to them all?Hadria
Yes, that is what I currently do.Stook
In what circumstance would the 'outside' smart pointer be destroyed while you are visiting the structure? It does seem that the issue would be solved taking holding on to the root for the duration of the visiting rather than managing each part in turn.Twain
@PeteKirkham That's an excellent point. I have to admit I didn't open the link up to now. The premise makes sense where a visitor might take ownership of some of the tokens (e.g., an ad-hoc dictionary builder, viable even after the document has been closed). If there's no ownership, raw pointers are the way to go.Hadria
@Pete Kirkham: What do you mean by "taking holding on to the root"?Stook
@Stook sorry, I was typing slower than I was thinking. 'would be solved by holding on to the root' (or 'would be solved by taking hold of the root' - I kind of ended up typing something half way between both thoughts)Twain
H
14

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);
}
Hadria answered 29/9, 2016 at 10:20 Comment(2)
In your main functions, why do you push_back a uniqe_ptr onto a vector of shared_ptrs? Or to put it another way, why not use make_shared? (Has the advantage that the shared_ptr control block is usually allocated in the same memory allocation as the actual object.) But otherwise, yes: This!Vlissingen
@MartinBonner Thanks for the comment - appreciated! Mental hiccup. will correct.Hadria

© 2022 - 2024 — McMap. All rights reserved.