È 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.