Uso idiomatico delle eccezioni in C ++


16

Gli stati FAQ delle eccezioni isocpp.org

Non usare il lancio per indicare un errore di codifica nell'uso di una funzione. Utilizzare assert o altro meccanismo per inviare il processo in un debugger o per arrestare il processo in modo anomalo e raccogliere il dump di arresto anomalo per lo sviluppatore.

D'altra parte la libreria standard definisce std :: logic_error e tutti i suoi derivati, che mi sembrano come se dovessero gestire, oltre ad altre cose, errori di programmazione. Passare una stringa vuota a std :: stof (genererà invalid_argument) non è un errore di programmazione? Passare una stringa che contiene caratteri diversi da '1' / '0' a std :: bitset (genererà invalid_argument) non è un errore di programmazione? Chiamare std :: bitset :: set con un indice non valido (genererà out_of_range) non è un errore di programmazione? Se questi non lo sono, allora qual è un errore di programmazione per il quale verrebbe verificato? Il costruttore basato su stringhe std :: bitset esiste solo dal C ++ 11, quindi avrebbe dovuto essere progettato tenendo in considerazione l'uso idiomatico delle eccezioni. D'altra parte ho avuto persone che mi dicono che la logica_errore non dovrebbe praticamente essere usata affatto.

Un'altra regola che si presenta frequentemente con le eccezioni è "utilizzare le eccezioni solo in circostanze eccezionali". Ma come può una funzione di biblioteca sapere quali circostanze sono eccezionali? Per alcuni programmi, non riuscire ad aprire un file potrebbe essere eccezionale. Per altri, non essere in grado di allocare memoria potrebbe non essere eccezionale. E ci sono centinaia di casi nel mezzo. Non riesci a creare un socket? Impossibile connettersi o scrivere dati su un socket o un file? Impossibile analizzare l'input? Potrebbe essere eccezionale, potrebbe non esserlo. La funzione in sé sicuramente non può in genere conoscerla, non ha idea in quale tipo di contesto venga chiamata.

Quindi, come dovrei decidere se dovrei usare le eccezioni o no per una particolare funzione? Mi sembra che l'unico modo effettivamente coerente sia usarli per la gestione di tutti gli errori, o per niente. E se sto usando la libreria standard, quella scelta è stata fatta per me.


6
Devi leggere attentamente la voce FAQ . Si applica solo agli errori di codifica, non ai dati non validi, alla dereferenziazione di un oggetto null o a qualsiasi cosa abbia a che fare con la cattiveria generale del runtime. In generale, le affermazioni riguardano l'identificazione di cose che non dovrebbero mai accadere. Per tutto il resto, ci sono eccezioni, codici di errore e così via.
Robert Harvey,

1
@RobertHarvey che la definizione ha ancora lo stesso problema - se qualcosa può essere risolto senza intervento umano o meno è noto solo ai livelli superiori di un programma.
cooky451

1
Ti stai impiccando alla legalistica. Valuta i pro e i contro e decidi. Inoltre, l'ultimo paragrafo della tua domanda ... Non lo considero affatto evidente. Il tuo pensiero è molto bianco e nero, quando la verità è probabilmente più vicina ad alcune sfumature di grigio.
Robert Harvey,

4
Hai provato a fare qualche ricerca prima di porre questa domanda? Gli errori C ++ nella gestione degli idiomi sono quasi certamente discussi in dettagli nauseabondi sul web. Un riferimento a una voce FAQ non fa una buona ricerca. Dopo aver fatto le tue ricerche, dovrai ancora prendere una decisione. Non farmi iniziare su come apparentemente le nostre scuole di programmazione stanno creando robot di codifica del modello software senza cervello che non sanno pensare da soli.
Robert Harvey,

2
Il che dà credito alla mia teoria secondo cui tale regola potrebbe non esistere realmente. Ho invitato alcune persone di The C ++ Lounge a vedere se possono rispondere alla tua domanda, anche se ogni volta che ci vado, il loro consiglio è "Smetti di usare il C ++, ti frangerà il cervello". Quindi prendi i loro consigli a tuo rischio e pericolo.
Robert Harvey,

Risposte:


15

Innanzitutto, mi sento in dovere di sottolineare che i std::exceptionsuoi figli sono stati progettati molto tempo fa. Ci sono un certo numero di parti che probabilmente (quasi certamente) sarebbero diverse se fossero progettate oggi.

Non fraintendetemi: ci sono parti del design che hanno funzionato abbastanza bene e sono esempi piuttosto buoni di come progettare una gerarchia di eccezioni per C ++ (ad esempio, il fatto che, a differenza della maggior parte delle altre classi, condividano tutti un radice comune).

In particolare logic_error, abbiamo un po 'di enigma. Da un lato, se hai una scelta ragionevole in merito, il consiglio che hai citato è giusto: è generalmente meglio fallire il più velocemente e rumorosamente possibile in modo da poter eseguire il debug e correggere.

Nel bene e nel male, tuttavia, è difficile definire la libreria standard in base a ciò che si dovrebbe fare in genere . Se li definisse uscire dal programma (ad es., Chiamare abort()) quando veniva dato un input errato, sarebbe quello che è sempre successo per quella circostanza - e in realtà ci sono alcune circostanze in cui questa probabilmente non è davvero la cosa giusta da fare , almeno nel codice distribuito.

Ciò si applicherebbe nel codice con requisiti (almeno soft) in tempo reale e una penalità minima per un output errato. Ad esempio, considera un programma di chat. Se sta decodificando alcuni dati vocali e ottiene un input errato, è probabile che un utente sarà molto più felice di vivere con un millisecondo di statico nell'output rispetto a un programma che si spegne completamente. Allo stesso modo quando si esegue la riproduzione video, potrebbe essere più accettabile vivere producendo valori errati per alcuni pixel per un fotogramma o due rispetto al fatto che il programma si chiuda in modo sommario perché il flusso di input è stato danneggiato.

Per quanto riguarda se utilizzare le eccezioni per segnalare determinati tipi di errori: hai ragione - la stessa operazione potrebbe essere considerata un'eccezione o meno, a seconda di come viene utilizzata.

D'altra parte, hai anche torto: l'uso della libreria standard non (necessariamente) forza questa decisione su di te. Nel caso di apertura di un file, normalmente useresti un iostream. Gli Iostreams non sono esattamente l'ultimo o il più grande progetto, ma in questo caso fanno le cose bene: ti permettono di impostare una modalità di errore, quindi puoi controllare se non riesci ad aprire un file con il risultato di generare un'eccezione o meno. Quindi, se hai un file che è veramente necessario per la tua applicazione e non aprirlo significa che devi prendere alcune misure correttive, puoi farlo lanciare un'eccezione se non riesce ad aprire quel file. Per la maggior parte dei file, che proverai ad aprire, se non esistono o non sono accessibili, falliranno (questo è il valore predefinito).

Quanto a come decidi: non penso che ci sia una risposta facile. Nel bene e nel male, le "circostanze eccezionali" non sono sempre facili da misurare. Mentre ci sono certamente casi che sono facili da decidere devono essere [non] eccezionali, ci sono (e probabilmente lo saranno sempre) casi in cui è aperto alla domanda o richiede una conoscenza del contesto che è al di fuori del dominio della funzione in questione. Per casi del genere, può valere la pena considerare almeno un design approssimativamente simile a questa parte degli iostreams, in cui l'utente può decidere se il fallimento comporta l'eccezione o meno. In alternativa, è del tutto possibile avere due serie separate di funzioni (o classi, ecc.), Una delle quali genererà eccezioni per indicare il fallimento, l'altra delle quali utilizza altri mezzi. Se segui quel percorso,


9

Il costruttore basato su stringhe std :: bitset esiste solo dal C ++ 11, quindi avrebbe dovuto essere progettato tenendo in considerazione l'uso idiomatico delle eccezioni. D'altra parte ho avuto persone che mi dicevano che la logica_errore non dovrebbe praticamente essere usata affatto.

Potresti non crederci, ma, beh, diversi programmatori C ++ non sono d'accordo. Ecco perché le FAQ dicono una cosa, ma la libreria standard non è d'accordo.

Le FAQ suggeriscono il crash perché sarà più facile eseguire il debug. Se si arresta in modo anomalo e si verifica un dump principale, si avrà lo stato esatto dell'applicazione. Se lanci un'eccezione perderai molto di quello stato.

La libreria standard prende la teoria secondo cui dare al programmatore la capacità di catturare e gestire l'errore è più importante della debuggabilità.

Potrebbe essere eccezionale, potrebbe non esserlo. La funzione in sé sicuramente non può in genere conoscerla, non ha idea in quale tipo di contesto venga chiamata.

L'idea qui è che se la tua funzione non sa se la situazione è eccezionale, non dovrebbe generare un'eccezione. Dovrebbe restituire uno stato di errore tramite qualche altro meccanismo. Una volta raggiunto un punto nel programma in cui sa che lo stato è eccezionale, allora dovrebbe generare l'eccezione.

Ma questo ha il suo problema. Se viene restituito uno stato di errore da una funzione, potresti non ricordarti di controllarlo e l'errore passerà silenziosamente. Questo porta alcune persone ad abbandonare le eccezioni sono regole eccezionali a favore del lancio di eccezioni per qualsiasi tipo di stato di errore.

Nel complesso, il punto chiave è che persone diverse hanno idee diverse su quando lanciare eccezioni. Non troverai una singola idea coerente. Anche se alcune persone affermeranno dogmaticamente che questo o quello è il modo giusto di gestire le eccezioni, non esiste una teoria concordata.

Puoi generare eccezioni:

  1. Mai
  2. Ovunque
  3. Solo sugli errori del programmatore
  4. Mai errori del programmatore
  5. Solo durante guasti non di routine (eccezionali)

e trova qualcuno su Internet che sia d'accordo con te. Dovrai adottare lo stile che funziona per te.


Vale forse la pena notare che il suggerimento di utilizzare le eccezioni solo quando le circostanze sono veramente eccezionali è stato ampiamente promosso da persone che insegnano le lingue in cui le eccezioni hanno scarse prestazioni. C ++ non è una di quelle lingue.
Jules l'

1
@Jules - ora che (performance) merita sicuramente una risposta in cui si esegue il backup del reclamo. Le prestazioni delle eccezioni C ++ sono certamente un problema, forse più, forse meno che altrove, ma affermare che "C ++ non è uno di quei linguaggi [dove le eccezioni hanno scarse prestazioni]" è certamente discutibile.
Martin Ba,

1
@MartinBa - rispetto, per esempio, a Java, le prestazioni delle eccezioni C ++ sono ordini di grandezza più veloci. I benchmark suggeriscono che le prestazioni del lancio di un'eccezione di 1 livello sono circa 50 volte più lente rispetto alla gestione di un valore di ritorno in C ++, rispetto a più di 1000 volte più lente in Java. I consigli scritti per Java in questo caso non dovrebbero essere applicati al C ++ senza pensarci troppo perché c'è più di un ordine di differenza di grandezza nelle prestazioni tra i due. Forse avrei dovuto scrivere "prestazioni estremamente scadenti" anziché "prestazioni scadenti".
Jules,

1
@Jules - grazie per questi numeri. (tutte le fonti?) io posso credere loro, perché Java (e C #) necessità di catturare la traccia dello stack, che certamente sembra come esso potrebbe essere molto costoso. Penso ancora che la tua risposta iniziale sia un po 'fuorviante, perché anche un rallentamento di 50x è piuttosto pesante, penso, esp. in un linguaggio orientato alle prestazioni come C ++.
Martin Ba,

2

Sono state scritte molte altre buone risposte, voglio solo aggiungere un breve punto.

La risposta tradizionale, specialmente quando è stata scritta la FAQ ISO C ++, confronta principalmente "eccezione C ++" e "codice di ritorno in stile C". Una terza opzione, "restituisce un tipo di valore composito, ad es. A structo union, o al giorno d'oggi, boost::varianto (proposta) std::expected, non è considerata.

Prima di C ++ 11 l'opzione "return a composite type" era generalmente molto debole. Perché, non vi era alcuna semantica di spostamento, quindi copiare e uscire da una struttura era potenzialmente molto costoso. A quel punto, era estremamente importante che la lingua modellasse il codice verso RVO per ottenere le migliori prestazioni. Le eccezioni erano come un modo semplice per restituire efficacemente un tipo composito, quando altrimenti sarebbe abbastanza difficile.

IMO, dopo C ++ 11, questa opzione "restituisce un'unione discriminata", simile al linguaggio Result<T, E>usato oggi in Rust, dovrebbe essere favorita più spesso nel codice C ++. A volte è davvero uno stile più semplice e più conveniente per indicare errori. Con le eccezioni, esiste sempre una sorta di possibilità che funzioni che non sono mai state lanciate prima potrebbero iniziare improvvisamente a lanciare dopo un refactor e i programmatori non sempre documentano così bene queste cose. Quando l'errore viene indicato come parte del valore restituito in un'unione discriminata, riduce notevolmente la possibilità che il programmatore ignori semplicemente il codice di errore, che è la solita critica alla gestione degli errori in stile C.

Di solito Result<T, E>funziona come un boost opzionale. È possibile verificare, utilizzando operator bool, se si tratta di un valore o di un errore. E poi usa dire operator *per accedere al valore, o qualche altra funzione "get". Di solito tale accesso è deselezionato, per la velocità. Ma puoi farlo in modo che in una build di debug, l'accesso venga verificato e un'asserzione assicuri che esista effettivamente un valore e non un errore. In questo modo chiunque non verifichi correttamente gli errori otterrà un'affermazione difficile piuttosto che qualche problema più insidioso.

Un ulteriore vantaggio è che, a differenza delle eccezioni in cui, se non viene catturato, fa impazzire la pila di una distanza arbitraria, con questo stile, quando una funzione inizia a segnalare un errore che non aveva prima, non è possibile compilare a meno che il il codice viene modificato per gestirlo. Questo rende i problemi più forti: il tradizionale problema dell '"eccezione non rilevata" diventa più un errore di compilazione che un errore di runtime.

Sono diventato un grande fan di questo stile. Di solito, oggigiorno uso questa o delle eccezioni. Ma provo a limitare le eccezioni ai problemi principali. Per qualcosa come un errore di analisi, provo ad expected<T>esempio a tornare . Cose del genere std::stoie boost::lexical_castche generano un'eccezione C ++ in caso di un problema relativamente minore "la stringa non può essere convertita in numero" mi sembra oggi di gusto molto scarso.


1
std::expectedè ancora una proposta non accettata, giusto?
Martin Ba,

Hai ragione, immagino che non sia ancora stato accettato. Ma ci sono diverse implementazioni open source che fluttuano in giro, e ho immaginato il mio un paio di volte. È meno complicato di fare un tipo di variante poiché ci sono solo due stati possibili. Le principali considerazioni di progettazione sono: quale interfaccia esatta vuoi e vuoi che sia come l'atteso <T> di Andrescu in cui l'oggetto di errore dovrebbe in realtà essere un exception_ptr, o vuoi solo usare un tipo di struttura o qualcosa del genere? come quello.
Chris Beck,

Il discorso di Andrei Alexandrescu è qui: channel9.msdn.com/Shows/Going+Deep/… Mostra in dettaglio come costruire una classe come questa e quali considerazioni potresti avere.
Chris Beck,

La proposta [[nodiscard]] attributesarà utile per questo approccio alla gestione degli errori poiché garantisce che non si ignori semplicemente il risultato dell'errore per errore.
CodesInChaos,

- Sì, conoscevo i discorsi di AA. Ho trovato il design piuttosto strano dato che per decomprimerlo ( except_ptr) hai dovuto lanciare un'eccezione internamente. Personalmente penso che un tale strumento dovrebbe funzionare in modo completamente indipendente dalle esecuzioni. Solo un'osservazione.
Martin Ba,

1

Questo è un problema altamente soggettivo, in quanto fa parte del design. E poiché il design è fondamentalmente arte, preferisco discutere di queste cose piuttosto che di dibattito (non sto dicendo che stai discutendo).

Per me, i casi eccezionali sono di due tipi: quelli che si occupano di risorse e quelli che si occupano di operazioni critiche. Ciò che può essere considerato critico dipende dal problema attuale e, in molti casi, dal punto di vista del programmatore.

La mancata acquisizione delle risorse è il candidato principale per il lancio di eccezioni. La risorsa può essere memoria, file, connessione di rete o qualsiasi altra cosa in base al problema e alla piattaforma. Ora, il mancato rilascio di una risorsa garantisce un'eccezione? Bene, dipende di nuovo. Non ho fatto nulla in cui il rilascio della memoria non è riuscito, quindi non sono sicuro di quello scenario. Tuttavia, l'eliminazione dei file come parte del rilascio delle risorse può non riuscire e per me ha avuto esito negativo e tale errore è in genere collegato ad altri processi che lo hanno tenuto aperto in un'applicazione multi-processo. Immagino che altre risorse potrebbero fallire durante il rilascio come un file, e di solito è un difetto di progettazione che causa questo problema, quindi correggerlo sarebbe meglio del lancio di eccezioni.

Quindi arriva l'aggiornamento delle risorse. Questo punto è, almeno per me, strettamente correlato all'aspetto delle operazioni critiche dell'applicazione. Immagina una Employeeclasse con una funzione UpdateDetails(std::string&)che modifica i dettagli in base alla stringa separata da virgola fornita. Simile al rilascio di memoria insufficiente, trovo difficile immaginare che l'assegnazione dei valori delle variabili membro non riesca a causa della mia mancanza di esperienza in tali domini in cui potrebbero verificarsi. Tuttavia, UpdateDetailsAndUpdateFile(std::string&)ci si aspetta che una funzione come quella indicata dal nome fallisca. Questo è ciò che chiamo operazione critica.

Ora, devi vedere se la cosiddetta operazione critica merita di generare un'eccezione. Voglio dire, l'aggiornamento del file sta avvenendo alla fine, come nel distruttore, o è semplicemente una chiamata paranoica fatta dopo ogni aggiornamento? Esiste un meccanismo di fallback che scrive regolarmente oggetti non scritti? Quello che sto dicendo è che devi valutare la criticità dell'operazione.

Ovviamente, ci sono molte operazioni critiche che non sono legate alle risorse. Se UpdateDetails()vengono forniti dati errati, non verranno aggiornati i dettagli e l'errore deve essere reso noto, quindi si genererebbe un'eccezione qui. Ma immagina una funzione come GiveRaise(). Ora, se il suddetto dipendente è fortunato ad avere un capo con i capelli a punta e non ottiene un aumento (in termini di programmazione, il valore di una variabile impedisce che ciò accada), la funzione è sostanzialmente fallita. Vuoi lanciare un'eccezione qui? Quello che sto dicendo è che devi valutare la necessità di un'eccezione.

Per me, la coerenza è in termini di approccio progettuale rispetto all'usabilità delle mie lezioni. Quello che voglio dire è che non penso in termini di "tutte le funzioni Get devono fare questo e tutte le funzioni Update devono farlo", ma vedo se una particolare funzione fa appello a una certa idea nel mio approccio. A prima vista, le classi potrebbero apparire in qualche modo "casuali", ma ogni volta che gli utenti (principalmente colleghi di altri team) invitano o chiedono informazioni, spiegherò e sembrano soddisfatti.

Vedo molte persone che in sostanza sostituiscono i valori restituiti con eccezioni perché usano C ++ e non C, e ciò dà una "bella separazione della gestione degli errori" ecc. E mi esorta a smettere di "mescolare" le lingue, ecc. persone così.


1

In primo luogo, come altri hanno affermato, le cose non sono così chiare in C ++, IMHO principalmente perché i requisiti e le restrizioni sono in qualche modo più vari in C ++ rispetto ad altre lingue, esp. C # e Java, che hanno problemi di eccezione "simili".

Esporrò sull'esempio std :: stof:

passare una stringa vuota a std :: stof (genererà invalid_argument) non un errore di programmazione

Il contratto di base , a mio modo di vedere, di questa funzione è che tenta di convertire il suo argomento in un float e qualsiasi errore nel farlo viene segnalato da un'eccezione. Entrambe le possibili eccezioni derivano logic_errorma non nel senso di errore del programmatore, ma nel senso di "l'input non può, mai, essere convertito in un float".

Qui, si potrebbe dire che logic_errorviene usato a per indicare che, dato quell'input (di runtime), è sempre un errore tentare di convertirlo - ma è compito della funzione determinarlo e dirlo (tramite eccezione).

Nota a margine: in quella vista, a runtime_error potrebbe essere visto come qualcosa che, dato lo stesso input per una funzione, potrebbe teoricamente avere successo per diverse esecuzioni. (ad es. un'operazione sul file, accesso al DB, ecc.)

Nota a margine: la libreria di regex C ++ ha scelto di derivarne l'errore runtime_errorsebbene ci siano casi in cui potrebbe essere classificata come qui (modello regex non valido).

Questo dimostra, IMHO, che il raggruppamento logic_o l' runtime_errore è piuttosto confuso in C ++ e non aiuta molto nel caso generale (*) - se hai bisogno di gestire errori specifici, probabilmente devi catturare più in basso dei due.

(*): Questo non vuol dire che un singolo pezzo di codice non deve essere coerente, ma se si gettano runtime_o logic_o custom_quarantina non è davvero così importante, credo.


Per commentare entrambi stofe bitset:

Entrambe le funzioni prendono le stringhe come argomento, e in entrambi i casi è:

  • non banale controllare se il chiamante è valido se una determinata stringa è valida (ad esempio, nel caso peggiore dovresti replicare la logica della funzione; nel caso di bitset, non è immediatamente chiaro se la stringa vuota è valida, quindi lascia che il ctor decida)
  • È già responsabilità della funzione "analizzare" la stringa, quindi deve già convalidare la stringa, quindi ha senso che segnala un errore per "utilizzare" la stringa in modo uniforme (e in entrambi i casi si tratta di un'eccezione) .

La regola che si presenta frequentemente con eccezioni è "usa eccezioni solo in circostanze eccezionali". Ma come può una funzione di biblioteca sapere quali circostanze sono eccezionali?

Questa affermazione ha, IMHO, due radici:

Prestazioni : se una funzione viene chiamata in un percorso critico e il caso "eccezionale" non è eccezionale, vale a dire che una quantità significativa di passaggi implicherà il lancio di un'eccezione, quindi pagare ogni volta per il meccanismo di svolgimento delle eccezioni non ha senso e potrebbe essere troppo lento.

Frazione di gestione degli errori : Se una funzione viene richiamata e l'eccezione viene immediatamente catturato e processato, allora c'è poco senso un'eccezione, la gestione degli errori sarà più dettagliato con il catchche con un if.

Esempio:

float readOrDefault;
try {
  readOrDefault = stof(...);
} catch(std::exception&) {
  // discard execption, just use default value
  readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}

Ecco dove entrano in gioco funzioni come TryParsevs Parse.: Una versione per quando il codice locale si aspetta che la stringa analizzata sia valida, una versione quando il codice locale presuppone che ci si aspetta effettivamente (cioè non eccezionale) che l'analisi fallisca.

In effetti, stofè solo (definito come) un wrapper strtof, quindi se non vuoi eccezioni, usa quello.


Quindi, come dovrei decidere se dovrei usare le eccezioni o no per una particolare funzione?

IMHO, hai due casi:

  • Funzione simile a "libreria" (riutilizzata spesso in contesti diversi): praticamente non puoi decidere. Forse fornire entrambe le versioni, forse una che riporta un errore e una a capo che converte l'errore restituito in un'eccezione.

  • Funzione "Applicazione" (specifica per un BLOB di codice applicazione, può essere riutilizzata, ma è vincolata dallo stile di gestione degli errori delle app, ecc.): Qui, dovrebbe essere spesso abbastanza chiara. Se i percorsi di codice che chiamano le funzioni gestiscono le eccezioni in modo sano e utile, utilizzare le eccezioni per segnalare qualsiasi errore (ma vedere di seguito) . Se il codice dell'applicazione è più facile da leggere e scrivere per uno stile di ritorno dell'errore, utilizzarlo in ogni caso.

Ovviamente ci saranno posti in mezzo: basta usare ciò di cui hai bisogno e ricordare YAGNI.


Infine, penso che dovrei tornare alla dichiarazione FAQ,

Non usare il lancio per indicare un errore di codifica nell'uso di una funzione. Usa assert o altro meccanismo per inviare il processo in un debugger o per arrestarlo in modo anomalo ...

Sottoscrivo questo per tutti gli errori che indicano chiaramente che qualcosa è gravemente incasinato o che il codice chiamante chiaramente non sapeva cosa stesse facendo.

Ma quando questo è appropriato è spesso altamente specifico per l'applicazione, quindi vedi sopra il dominio della libreria rispetto al dominio dell'applicazione.

Questo ricade sulla domanda su se e come convalidare le precondizioni di chiamata , ma non entrerò in quello, risposta già troppo a lungo :-)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.