Pattern visitatore, sostituendo oggetti

7

Ho un programma che traduce un DSL in C ++, che utilizza un pattern Visitor nella rappresentazione intermedia.

Ho spesso bisogno di sostituire il nodo attualmente elaborato con uno di un tipo diverso (ad esempio, sostituendo il "tipo non risolto" con la definizione del tipo).

Qual è un buon modello per farlo?

Ho provato:

  1. memorizzando un puntatore nel punto in cui viene fatto riferimento all'oggetto corrente

    Questo è enormemente brutto, ma mantiene un modello di visitatore "standard", in cui il visitatore stesso mantiene tutto il suo stato come variabili membro e il metodo visit non restituisce un valore.

    La difficoltà qui è che questo è abbastanza incline all'errore - se dimentico di memorizzare il puntatore prima di scendere nell'albero, potrei sovrascrivere il riferimento sbagliato.

  2. restituire un oggetto sostitutivo dal metodo di visita

    Il successivo layer up è responsabile della sostituzione dell'oggetto corrente con l'oggetto appena restituito - la cancellazione è rappresentata dalla restituzione di un puntatore NULL e nessuna modifica viene rappresentata restituendo il vecchio oggetto.

    Questo è molto più facile da ottenere correttamente, perché posso ottenere la diagnostica se ho accidentalmente lasciato cadere un codice di ritorno, ma penso che sia ancora un po 'brutto.

Ci sono opzioni migliori?

    
posta Simon Richter 25.03.2016 - 00:17
fonte

1 risposta

1

È solo un dettaglio di implementazione che il metodo visit() di solito è vuoto quando descritto in C ++. Questo non è affatto una parte centrale del modello del visitatore. Nel libro "Design Patterns" di Gamma, Helm, Johnson, Vlissides, gli esempi di codice sono di solito forniti in C ++ e Smalltalk. Smalltalk è un linguaggio molto dinamico. Il codice di esempio per il pattern Visitor in Smalltalk in quel libro restituisce effettivamente dei valori! È un valutatore regex. Tradurrò il codice in JavaScript per renderlo più comprensibile.

The object structure (regular expressions) is made of four classes, and all of them have an accept() method that takes the visitor as an argument. In class SequenceExpression, the accept method is

function accept(aVisitor) {
  return aVisitor.visitSequence(this);
}

[…]

The ConcreteVisitor class is REMatchingVisitor. […] Its methods […] return the set of streams that the expression would match to identify the current state.

function visitSequence(sequenceExp) {
  this.inputState = sequenceExp.expression1.accept(this);
  return sequenceExp.expression2.accept(this);
}

...

function visitAlternation(alternateExp) {
  var originalState = this.inputState;
  var finalState = alternateExp.alternative1.accept(this);
  this.inputState = originalState;
  finalState.addAll(alternateExp.alternative2.accept(this));
  return finalState;
}

function visitLiteral(literalExp) {
  var finalState = new Set();
  this.inputState.foreach(function (stream) {
    var tStream = stream.copy();
    if (tStream.nextAvailable(literalExp.value.size) == literalExp.value)) {
      finalState.add(tStream);
    }
  });
  return finalState;
}

Quindi, perché non è fatto in C ++? Perché tutti i metodi visit() sono void? Perché i metodi virtuali accept() non saprebbero cosa restituire. Ogni visitatore può restituire un tipo diverso. Mentre il metodo accept deve semplicemente passare questo valore, il C ++ avrebbe bisogno del tipo preciso. Questo può essere espresso con modelli, ma i metodi virtuali non possono essere metodi basati su modelli. (Il punto di un metodo virtuale è che l'implementazione viene risolta solo in fase di runtime (binding in ritardo), mentre i template devono essere completamente valutati in fase di compilazione, poiché al momento della compilazione non sarebbe noto quale metodo sarebbe selezionato, il template non sarebbe essere invocato)

La soluzione usuale è quella di memorizzare il valore restituito in un campo istanza dell'oggetto visitatore. Questo è scomodo e richiede o un valore di ritorno costruibile di default o un puntatore indiretto. Sebbene questo sia semanticamente equivalente alla restituzione diretta di un valore, ciò rende l'utilizzo del visitatore così scomodo da creare spesso una funzione wrapper per l'esecuzione del visitatore:

class Visitor;

class Base {
public:
  virtual void accept(Visitor&) const = 0;
};

class A;
class B;

class Visitor {
public:
  virtual void visitA(A const&) = 0;
  virtual void visitB(B const&) = 0;
};

class A { … };
class B { … };

class ConcreteVisitor : public Visitor final {
  int mResult;
public:
  ConcreteVisitor() : mResult(0) {}
  void visitA(A const& a) override { mResult = 1; }
  void visitB(B const& a) override { mResult = 2; }
  int result() const { return mResult; }
};

int runConcreteVisitor(Base const& base) {
  ConcreteVisitor v;
  base.accept(v);
  return v.result();
}

Ogni visitatore si comporta in modo efficace come una funzione (polimorfica) sulla gerarchia di input specificata o come metodi di estensione. Inserendo ulteriori argomenti e restituendo valori in variabili membro, possiamo modellare firme di funzioni arbitrarie in C ++. (Gli argomenti delle funzioni diversi dall'elemento da visitare corrispondono agli argomenti del costruttore del visitatore).

Ma questa è solo una soluzione. Se tutti i tuoi visitatori avranno una firma efficace Expression visitor(Expression const&) , puoi scrivere tutti i tuoi metodi di accettazione come

 virtual Expression accept(Visitor& v) {
   return v.acceptAddition(*this);
 }

Per le operazioni AST, a volte questo è tutto ciò di cui hai bisogno. Ma una volta che hai diversi tipi di "ritorno" per i tuoi visitatori, dovrai duplicare tutti i metodi di accettazione / visita per l'altro tipo (in pratica, i modelli manuali) o dovrai utilizzare la soluzione alternativa per la variabile membro del visitatore. In questa luce, potrebbe essere sensato utilizzare la soluzione dall'inizio. Usando runVisitor(…) helper, questo diventa leggermente meno incline agli errori perché non devi ricordarti di recuperare il valore restituito, ne viene dato uno direttamente. Se il tuo visitatore è ricorsivo, questo significa anche che devi evitare lo stato mutevole non necessario direttamente nel tuo visitatore da quando viene creato un nuovo visitatore per ogni chiamata di accettazione / visita.

    
risposta data 26.03.2016 - 13:11
fonte

Leggi altre domande sui tag