Aumenta un'eccezione generata con alcune informazioni contestuali

2

L'applicazione di questa domanda è fondamentalmente un traspolatore che contiene molta logica. Il transpiler è scritto in C ++ (che non dovrebbe essere molto rilevante per questa domanda), e traspone un linguaggio speciale (che abbiamo sviluppato) in una lingua diversa.

Per la gestione degli errori (errori di input non validi e errori interni del transpiler), utilizzo le eccezioni. Per mantenere la logica del traspolatore il più semplice possibile, l'input viene convalidato molto tardi, cioè nella maggior parte dei casi non avviene alcuna convalida separata. Ciò significa che la logica di elaborazione genera eccezioni. Alcune funzioni di utilità sono utilizzate globalmente sul codice base e anche quelle possono generare eccezioni.

Ciò significa che le eccezioni sono potenzialmente gettate in una funzione di utilità, in cui non sappiamo da dove proviene l'input. Questo è un problema, perché - simile al modo in cui il compilatore ti dice in quale riga del file si è verificato l'errore di analisi - vorrei dire all'utente del traspolatore in cui è stato generato l'errore. Per mantenere le cose semplici, questo non deve necessariamente essere uno "stack" o "catena" di contesti. Ma le eccezioni dovrebbero contenere almeno alcune informazioni contestuali , quale parte dell'input non è valida e perché, in modo tale che la parte rilevante dell'input possa essere corretta e / o un IDE possa navigare verso la fonte dell'errore .

Per le eccezioni, attualmente utilizzo una sola classe Error che trasporta tutte le informazioni come membri e vengono utilizzate funzioni statiche per crearle a seconda di quale errore si è verificato, ad esempio:

throw Error::internalError("This should not have happened...");

o un caso più interessante (pseudo codice, questo non è C ++):

var x = ...;
if (!x.isInteger())
    throw Error::notAnInteger(x);

Ora vorrei aumentare quell'eccezione (l'oggetto Error ) con una "specifica di contesto / posizione". Ad esempio, nel mio linguaggio transpiled, ho il concetto di variabili, che hanno un valore predefinito. Consideriamo una variabile che ha un ID interno memorizzato come numero intero e un valore che possiamo recuperare. Ora vogliamo assicurarci che il valore sia un intero (di nuovo, pseudo codice):

var variableID = ...;
var value = getValueOfVariable(variableID);
if (!value.isInteger())
    throw Error::notAnInteger(value);

Consideriamo ora il caso in cui variableID = 42, value = "foo" . Quando l'eccezione viene lanciata e presentata all'utente, sa solo che "foo" non è un numero intero, ma a lei non viene detto che questo valore proviene dalla variabile 42 .

Ecco perché voglio dire al Error che stiamo cercando di analizzare la variabile il cui ID è variableID .

Vorrei presentarvi tre idee che mi sono venute in mente, ma non posso decidere quale sia lo stile migliore.

La prima idea consiste nell'aggiungere ulteriori funzioni statiche, ciascuna per un tipo di tale contesto. Nell'esempio sopra, invece di

    throw Error::notAnInteger(value);

Allora faccio

    throw Error::variableNotAnInteger(value, variableID);

e all'interno di quella funzione memorizzo le informazioni che la fonte dell'errore era "variabile con ID variableID ". Tuttavia, con un numero sempre maggiore di tipi di contesti (non solo analizza il valore di una variabile che può portare a errori "non interi"), il numero di funzioni aumenta rapidamente.

Una seconda idea era di introdurre una classe Location , che ha lo scopo di descrivere l'origine / contesto dell'errore e di passarla insieme agli altri parametri a queste funzioni statiche:

    throw Error::notAnInteger(value, Location::variable(variableID));

Tuttavia, ciò significa che tutte le funzioni di utilità (ad esempio una funzione che ha solo lo scopo di analizzare un valore, indipendentemente da dove proviene il valore), dovrebbero essere aumentate con l'oggetto contestuale Location , in ordine a Costruisci in modo appropriato l'oggetto Error da gettare. Ciò tuttavia rende il codice base un po 'brutto, in particolare per la logica nidificata e ulteriormente nidificata.

Una terza idea è quella di aggiungere la posizione dopo che l'eccezione è stata lanciata . La funzione di utilità genera un'eccezione e il chiamante recupera l'eccezione, aggiunge la posizione appropriata e quindi genera nuovamente l'eccezione aumentata. Questo ha lo svantaggio che quando mi dimentico da qualche parte nel codice di base per aggiungere una tale posizione, l'errore non ne porta uno. (Al contrario, la seconda idea garantisce che venga passato un oggetto posizione.)

Questo apparirebbe (ancora, pseudo codice):

function parseValue(value, type) {
    ...
    if (type == "integer") {
        if (!value.isInteger()) {
            // Throw an exception without a location / context information:
            throw Error::notAnInteger(value);
        }
        return value.toInteger();
    }
    ...
}

//-----------------------------------------

var variableID = ...;
var value = getValueOfVariable(variableID);
var type = getTypeOfVariable(variableID);
try {
    var parsedValue = parseValue(value, type);
} catch (Error e) {
    // Add the location of the error, and rethrow:
    e.setLocation(Location::variable(variableID));
    throw e;
}

Quale dei metodi precedenti può essere considerato un buon stile? C'è anche una quarta opzione che dovrei prendere in considerazione? Com'è fatto tipicamente in trans / / compilatori?

    
posta leemes 30.05.2017 - 10:36
fonte

2 risposte

1

Utilizzare il backtrace è l'opzione migliore. C'è una proposta per incorporare le informazioni di backtrace nell'eccezione (si veda qui ), ma finora non è stato fatto nulla. Per ottenere le informazioni sul backtrace, puoi fare come spiegato in questa risposta .

Se utilizzi eccezioni boost, puoi utilizzare la loro diagnostica .

    
risposta data 30.05.2017 - 11:23
fonte
0

Prima di tutto, non sono uno sviluppatore C ++ esperto, il mio background è principalmente Java, quindi forse non tutte le idee funzioneranno per C ++ ...

Preferisco principalmente il tuo terzo approccio, con qualche aggiustamento.

Lascia che le tue eccezioni vengano create senza informazioni di contesto e aggiungi il contesto in un secondo momento. (Oppure, se una combinazione di tipo di errore e contesto viene trovata ripetutamente, crea un metodo di convenienza per quella combinazione, se preferisci.)

Penso che possano esistere diversi tipi di contesto: ID variabili, numeri di linea, ... e probabilmente una singola eccezione può avere più contesti, ad es. sia un ID variabile che un numero di riga (o più numeri di linea se la tua lingua ha, ad esempio, blocchi annidati o pile di chiamate).

Nell'esempio 'variableID', è un ID variabile che si desidera aggiungere come contesto ed è disponibile nel punto in cui viene creata l'eccezione. Quindi creo l'eccezione, quindi aggiungo il contesto ID variabile e infine lo lancio.

Se, in un metodo che si occupa della riga di origine N, ricevi un'eccezione da qualunque metodo tu chiami, rilevi quell'eccezione, aggiungi il numero di riga N e rethrow. Oppure, se si trattava di un tipo di eccezione estraneo, non del tuo errore, prima avvolgi questa eccezione nel tuo errore, quindi aggiungi il numero di riga N e gira.

    
risposta data 26.11.2017 - 13:20
fonte

Leggi altre domande sui tag