Quando dovrei davvero usare noexcept?


509

La noexceptparola chiave può essere applicata in modo appropriato a molte firme di funzioni, ma non sono sicuro di quando dovrei considerare di usarlo nella pratica. Sulla base di ciò che ho letto finora, l'aggiunta dell'ultimo minuto noexceptsembra affrontare alcuni problemi importanti che sorgono quando si lanciano costruttori di mosse. Tuttavia, non sono ancora in grado di fornire risposte soddisfacenti ad alcune domande pratiche che mi hanno portato a leggere di più noexceptin primo luogo.

  1. Ci sono molti esempi di funzioni che conosco non verranno mai lanciate, ma per le quali il compilatore non può determinarlo da solo. Devo aggiungere noexceptalla dichiarazione di funzione in tutti questi casi?

    Dover pensare se devo aggiungere o meno noexceptdopo ogni dichiarazione di funzione ridurrebbe notevolmente la produttività del programmatore (e francamente, sarebbe un dolore nel culo). Per quali situazioni dovrei essere più attento all'uso noexcepte per quali situazioni posso cavarmela con l'implicito noexcept(false)?

  2. Quando posso realisticamente aspettarmi di osservare un miglioramento delle prestazioni dopo l'uso noexcept? In particolare, fornire un esempio di codice per il quale un compilatore C ++ è in grado di generare un codice macchina migliore dopo l'aggiunta di noexcept.

    Personalmente, mi preoccupo per noexceptla maggiore libertà fornita al compilatore di applicare in modo sicuro alcuni tipi di ottimizzazioni. I compilatori moderni ne traggono vantaggio noexceptin questo modo? In caso contrario, posso aspettarmi che alcuni di loro lo facciano nel prossimo futuro?


40
Il codice che usa move_if_nothrow(o whatchamacallit) vedrà un miglioramento delle prestazioni se c'è un mosse di movimento senza eccezioni.
R. Martinho Fernandes,


5
Lo è move_if_noexcept.
Nikos,

Risposte:


180

Penso che sia troppo presto per dare una risposta "best practice" per questo in quanto non c'è stato abbastanza tempo per usarlo nella pratica. Se questo fosse stato chiesto degli indicatori di lancio subito dopo la loro uscita, le risposte sarebbero molto diverse da adesso.

Dover pensare se devo aggiungere o meno noexceptdopo ogni dichiarazione di funzione ridurrebbe notevolmente la produttività del programmatore (e francamente, sarebbe una seccatura).

Bene, allora usalo quando è ovvio che la funzione non verrà mai lanciata.

Quando posso realisticamente aspettarmi di osservare un miglioramento delle prestazioni dopo l'uso noexcept? [...] Personalmente, mi preoccupo per noexceptla maggiore libertà fornita al compilatore di applicare in modo sicuro determinati tipi di ottimizzazioni.

Sembra che i maggiori guadagni di ottimizzazione derivino dalle ottimizzazioni degli utenti, non da quelle del compilatore a causa della possibilità di verificarle noexcepte sovraccaricarle. La maggior parte dei compilatori segue un metodo di gestione delle eccezioni senza penalità se non si lancia, quindi dubito che cambierebbe molto (o niente) a livello di codice macchina del codice, sebbene forse riduca le dimensioni binarie rimuovendo il codice di gestione.

L'uso noexceptnei quattro principali (costruttori, assegnazione, non distruttori come già sono noexcept) probabilmente causerà i migliori miglioramenti poiché i noexceptcontrolli sono "comuni" nel codice modello come nei stdcontenitori. Ad esempio, std::vectornon utilizzerà la mossa della tua classe a meno che non sia contrassegnata noexcept(o altrimenti il ​​compilatore non può dedurla in altro modo).


6
Penso che il std::terminatetrucco obbedisca ancora al modello Zero-Cost. Cioè, è proprio così che l'intervallo di istruzioni all'interno delle noexceptfunzioni è mappato per chiamare std::terminatese throwviene utilizzato al posto dello svolgitore dello stack. Pertanto, dubito che abbia un sovraccarico maggiore rispetto al normale monitoraggio delle eccezioni.
Matthieu M.,

4
@Klaim Vedere questo: stackoverflow.com/a/10128180/964135 realtà deve solo essere non-lancio, ma le noexceptgaranzie questo.
Pubby

3
"Se viene noexceptlanciata una funzione che std::terminateviene chiamata che sembra implicherebbe una piccola quantità di sovraccarico" ... No, questo dovrebbe essere implementato non generando tabelle di eccezioni per tale funzione, che il dispatcher di eccezioni dovrebbe catturare e poi salvare.
Potatoswatter

8
La gestione delle eccezioni di @Pubby C ++ viene generalmente eseguita senza sovraccarico, ad eccezione delle tabelle di salto che mappano potenzialmente il lancio degli indirizzi del sito di chiamata ai punti di ingresso del gestore. La rimozione di tali tabelle è la più vicina possibile alla rimozione completa della gestione delle eccezioni. L'unica differenza è la dimensione del file eseguibile. Probabilmente non vale la pena menzionare nulla.
Potatoswatter

26
"Bene, allora usalo quando è ovvio che la funzione non verrà mai lanciata." Non sono d'accordo. noexceptè parte della funzione di interfaccia ; non dovresti aggiungerlo solo perché l' implementazione attuale non getta. Non sono sicuro della risposta giusta a questa domanda, ma sono abbastanza sicuro che il modo in cui la tua funzione si comporta oggi non ha nulla a che fare con essa ...
Nemo

133

Come continuo a ripetere in questi giorni: prima la semantica .

Aggiungendo noexcept, noexcept(true)ed noexcept(false)è prima di tutto sulla semantica. Per inciso condiziona solo una serie di possibili ottimizzazioni.

Come programmatore che legge il codice, la presenza di noexceptè simile a quella di const: mi aiuta a capire meglio cosa può o non può accadere. Pertanto, vale la pena dedicare un po 'di tempo a pensare se si sa se la funzione verrà lanciata. Per un promemoria, può essere lanciato qualsiasi tipo di allocazione dinamica della memoria.


Bene, ora passiamo alle possibili ottimizzazioni.

Le ottimizzazioni più ovvie vengono effettivamente eseguite nelle librerie. C ++ 11 fornisce una serie di tratti che consente di sapere se una funzione è noexcepto meno e l'implementazione della Libreria standard stessa utilizzerà tali tratti per favorire le noexceptoperazioni sugli oggetti definiti dall'utente che manipolano, se possibile. Come spostare la semantica .

Il compilatore può solo eliminare un po 'di grasso (forse) dai dati di gestione delle eccezioni, perché deve tenere conto del fatto che potresti aver mentito. Se viene lanciata una funzione contrassegnata noexcept, std::terminateviene chiamata.

Queste semantiche sono state scelte per due motivi:

  • beneficiando immediatamente noexceptanche quando le dipendenze non lo utilizzano già (compatibilità con le versioni precedenti)
  • consentendo la specifica di noexceptquando si chiamano funzioni che teoricamente possono essere lanciate, ma che non sono previste per gli argomenti dati

2
Forse sono ingenuo, ma immagino che una funzione che invoca solo noexceptfunzioni non debba fare nulla di speciale, perché eventuali eccezioni che potrebbero sorgere si innescano terminateprima di arrivare a questo livello. Ciò differisce notevolmente dal dover affrontare e propagare bad_allocun'eccezione.

8
Sì, è possibile definire noexcept in modo come suggerisci, ma sarebbe una funzionalità davvero inutilizzabile. Molte funzioni possono essere attivate se determinate condizioni non sono mantenute e non è possibile chiamarle anche se si conoscono le condizioni. Ad esempio qualsiasi funzione che può generare std :: invalid_argument.
tr3w,

3
@MatthieuM. Un po 'in ritardo per una risposta, ma comunque. Le funzioni contrassegnate noexcept possono chiamare altre funzioni che possono essere lanciate, la promessa è che questa funzione non emetterà un'eccezione, cioè dovranno solo gestire l'eccezione da soli!
Honf, il

4
Ho votato a fondo questa risposta molto tempo fa, ma dopo aver letto e pensato un po 'di più, ho un commento / domanda. "Sposta la semantica" è l' unico esempio che abbia mai visto qualcuno dare dove noexceptè chiaramente utile / una buona idea. Sto iniziando a pensare di spostare la costruzione, spostare l'assegnazione e scambiare sono gli unici casi che ci sono ... Conosci altri?
Nemo il

5
@Nemo: nella libreria Standard, è forse l'unico, tuttavia mostra un principio che può essere riutilizzato altrove. Un'operazione di spostamento è un'operazione che mette temporaneamente uno stato nel "limbo" e solo quando lo è noexceptqualcuno può utilizzarlo con sicurezza su un dato a cui è possibile accedere in seguito. Ho potuto vedere questa idea usata altrove, ma la libreria Standard è piuttosto sottile in C ++ e viene utilizzata solo per ottimizzare copie di elementi che penso.
Matthieu M.,

77

Questo in realtà fa una differenza (potenzialmente) enorme per l'ottimizzatore nel compilatore. I compilatori hanno effettivamente avuto questa funzione per anni tramite l'istruzione empty throw () dopo una definizione di funzione, nonché estensioni di proprietà. Posso assicurarti che i compilatori moderni sfruttano questa conoscenza per generare codice migliore.

Quasi ogni ottimizzazione nel compilatore utilizza qualcosa chiamato "diagramma di flusso" di una funzione per ragionare su ciò che è legale. Un diagramma di flusso è costituito da quelli che vengono generalmente chiamati "blocchi" della funzione (aree di codice che hanno una singola entrata e una singola uscita) e bordi tra i blocchi per indicare dove può passare il flusso. Noexcept modifica il diagramma di flusso.

Hai chiesto un esempio specifico. Considera questo codice:

void foo(int x) {
    try {
        bar();
        x = 5;
        // Other stuff which doesn't modify x, but might throw
    } catch(...) {
        // Don't modify x
    }

    baz(x); // Or other statement using x
}

Il diagramma di flusso per questa funzione è diverso se barè etichettato noexcept(non è possibile che l'esecuzione salti tra la fine di bare l'istruzione catch). Se etichettato come noexcept, il compilatore è certo che il valore di x è 5 durante la funzione baz - si dice che il blocco x = 5 "domina" il blocco baz (x) senza il bordo bar()dall'istruzione catch.

Può quindi fare qualcosa chiamato "propagazione costante" per generare codice più efficiente. Qui se baz è inline, le istruzioni che usano x potrebbero contenere anche costanti e quindi quella che era una valutazione di runtime può essere trasformata in una valutazione in fase di compilazione, ecc.

Comunque, la risposta breve: noexceptconsente al compilatore di generare un diagramma di flusso più stretto e il diagramma di flusso viene utilizzato per ragionare su tutti i tipi di ottimizzazioni del compilatore comuni. Per un compilatore, le annotazioni degli utenti di questa natura sono fantastiche. Il compilatore proverà a capire queste cose, ma di solito non ci riesce (la funzione in questione potrebbe trovarsi in un altro file oggetto non visibile al compilatore o usare in modo transitorio alcune funzioni che non sono visibili), oppure quando lo fa, c'è qualche banale eccezione che potrebbe essere lanciata di cui non sei nemmeno a conoscenza, quindi non può implicitamente etichettarla come noexcept(allocando la memoria potrebbe generare bad_alloc, per esempio).


3
Questo in realtà fa la differenza nella pratica? L'esempio è inventato perché nulla prima x = 5può lanciare. Se quella parte del tryblocco avesse uno scopo, il ragionamento non sarebbe valido.
Potatoswatter

7
Direi che fa davvero la differenza nell'ottimizzazione delle funzioni che contengono blocchi try / catch. L'esempio che ho dato, sebbene inventato, non è esaustivo. Il punto più grande è che noexcept (come l'istruzione throw () prima di esso) aiuta la compilazione a generare un diagramma di flusso più piccolo (meno bordi, meno blocchi) che è una parte fondamentale di molte ottimizzazioni che poi compie.
Terry Mahaffey,

Come può il compilatore riconoscere che il codice può generare eccezioni? L'accesso all'array è considerato una possibile eccezione?
Tomas Kubes,

3
@ qub1n Se il compilatore può vedere il corpo della funzione può cercare throwistruzioni esplicite o altre cose del genere newche possono essere lanciate. Se il compilatore non riesce a vedere il corpo, deve fare affidamento sulla presenza o l'assenza di noexcept. L'accesso normale all'array non genera in genere eccezioni (C ++ non ha il controllo dei limiti), quindi no, l'accesso all'array non indurrebbe da solo il compilatore a pensare che una funzione generi eccezioni. (L'accesso non vincolato è UB, non un'eccezione garantita.)
cdhowie,

@cdhowie " deve fare affidamento sulla presenza o assenza di noexcept " o sulla presenza di throw()C ++ pre-noexcept
curiousguy,

57

noexceptpuò migliorare notevolmente le prestazioni di alcune operazioni. Ciò non avviene a livello di generazione del codice macchina da parte del compilatore, ma selezionando l'algoritmo più efficace: come altri hanno già detto, si effettua questa selezione utilizzando la funzione std::move_if_noexcept. Ad esempio, la crescita di std::vector(ad esempio, quando chiamiamo reserve) deve fornire una forte garanzia di sicurezza delle eccezioni. Se sa che Til costruttore di mosse non viene lanciato, può semplicemente spostare ogni elemento. Altrimenti deve copiare tutti Ti messaggi. Questo è stato descritto in dettaglio in questo post .


4
Addendum: ciò significa che se si definiscono costruttori di spostamenti o si aggiungono operatori di assegnazione di spostamenti noexcept(se applicabile)! Le funzioni membro di spostamento implicitamente definite sono state noexceptaggiunte automaticamente ad esse (se applicabile).
mucaho,

33

Quando posso realisticamente, tranne osservare un miglioramento delle prestazioni dopo l'uso noexcept? In particolare, fornisci un esempio di codice per il quale un compilatore C ++ è in grado di generare un codice macchina migliore dopo l'aggiunta di noexcept.

Uhm, mai? Non è mai un momento? Mai.

noexceptè per le ottimizzazioni delle prestazioni del compilatore allo stesso modo constdelle ottimizzazioni delle prestazioni del compilatore. Cioè, quasi mai.

noexceptviene utilizzato principalmente per consentire a "voi" di rilevare in fase di compilazione se una funzione può generare un'eccezione. Ricorda: la maggior parte dei compilatori non emette codice speciale per le eccezioni a meno che non generi effettivamente qualcosa. Quindi noexceptnon si tratta di dare suggerimenti al compilatore su come ottimizzare una funzione, ma piuttosto di darti suggerimenti su come usare una funzione.

I modelli come move_if_noexceptrileveranno se il costruttore di mosse è definito con noexcepte restituiranno un const&invece di un &&tipo se non lo è. È un modo per dire di muoversi se è molto sicuro farlo.

In generale, dovresti usarlo noexceptquando pensi che sarà effettivamente utile farlo. Alcuni codici avranno percorsi diversi se is_nothrow_constructibleè vero per quel tipo. Se stai usando un codice che lo farà, sentiti libero con noexcepti costruttori appropriati.

In breve: usalo per spostare costruttori e costrutti simili, ma non pensare di dover impazzire con esso.


13
In senso move_if_noexceptstretto , non restituirà una copia, restituirà un riferimento valore costante anziché un riferimento valore. In generale, ciò farà sì che il chiamante effettui una copia anziché uno spostamento, ma move_if_noexceptnon esegua la copia. Altrimenti, ottima spiegazione.
Jonathan Wakely,

12
+1 Jonathan. Il ridimensionamento di un vettore, ad esempio, sposta gli oggetti invece di copiarli se il costruttore di spostamenti è noexcept. Quindi "mai" non è vero.
mfontanini,

4
Voglio dire, il compilatore sarà generare codice migliore in quella situazione. OP richiede un esempio per il quale il compilatore è in grado di generare un'applicazione più ottimizzata. Questo sembra essere il caso (anche se non è un'ottimizzazione del compilatore ).
mfontanini,

7
@mfontanini: il compilatore genera solo un codice migliore perché il compilatore è costretto a compilare un codepath diverso . Funziona solo perché std::vectorè scritto per forzare il compilatore a compilare codice diverso . Non si tratta del compilatore che rileva qualcosa; si tratta di codice utente che rileva qualcosa.
Nicol Bolas,

3
Il fatto è che non riesco a trovare "l'ottimizzazione del compilatore" nella citazione all'inizio della tua risposta. Come ha detto @ChristianRau, il compilatore genera un codice più efficiente, non importa quale sia l'origine di tale ottimizzazione. Dopotutto, il compilatore sta generando un codice più efficiente, no? PS: Non ho mai detto che fosse un'ottimizzazione del compilatore, ho anche detto "Non è un'ottimizzazione del compilatore".
mfontanini,

22

Con le parole di Bjarne ( The C ++ Programming Language, 4th Edition , page 366):

Laddove la risoluzione sia una risposta accettabile, un'eccezione non rilevata la raggiungerà perché si trasforma in una chiamata di terminate () (§13.5.2.5). Inoltre, uno noexceptspecificatore (§13.5.1.1) può rendere esplicito quel desiderio.

I sistemi di tolleranza agli errori di successo sono multilivello. Ogni livello affronta quanti più errori possibile senza essere troppo contorto e lascia il resto a livelli più alti. Le eccezioni supportano tale visualizzazione. Inoltre, terminate()supporta questa visione fornendo una via di fuga se il meccanismo di gestione delle eccezioni stesso è corrotto o se è stato usato in modo incompleto, lasciando quindi le eccezioni non rilevate. Allo stesso modo, noexceptfornisce una semplice via di fuga per errori in cui il tentativo di recupero sembra impossibile.

double compute(double x) noexcept;     {
    string s = "Courtney and Anya";
    vector<double> tmp(10);
    // ...
}

Il costruttore di vettori potrebbe non riuscire ad acquisire memoria per i suoi dieci doppi e lanciare a std::bad_alloc. In tal caso, il programma termina. Termina incondizionatamente invocando std::terminate()(§30.4.1.3). Non invoca i distruttori dalle funzioni di chiamata. È definito dall'implementazione se vengono invocati i distruttori dagli ambiti tra il throwe il noexcept(es. Per s in compute ()). Il programma sta per terminare, quindi non dovremmo comunque dipendere da alcun oggetto. Aggiungendo uno noexceptspecificatore, indichiamo che il nostro codice non è stato scritto per far fronte a un lancio.


2
Hai una fonte per questa citazione?
Anton Golov,

5
@AntonGolov "Il linguaggio di programmazione C ++, 4a edizione" pag. 366
Rusty Shackleford,

1
Mi sembra che in realtà dovrei aggiungere noexceptogni volta, tranne che voglio esplicitamente occuparmi delle eccezioni. Siamo reali, la maggior parte delle eccezioni sono così improbabili e / o così fatali che il salvataggio è difficilmente ragionevole o possibile. Ad esempio nell'esempio citato, se l'allocazione non riesce, l'applicazione difficilmente sarà in grado di continuare a funzionare correttamente.
Neonit,

21
  1. Ci sono molti esempi di funzioni che conosco non verranno mai lanciate, ma per le quali il compilatore non può determinarlo da solo. Devo aggiungere noexcept alla dichiarazione di funzione in tutti questi casi?

noexceptè complicato, poiché fa parte dell'interfaccia delle funzioni. In particolare, se si sta scrivendo una libreria, il codice client può dipendere dalla noexceptproprietà. Può essere difficile modificarlo in un secondo momento, poiché potresti rompere il codice esistente. Potrebbe essere meno preoccupante quando si implementa il codice utilizzato solo dalla propria applicazione.

Se hai una funzione che non può essere lanciata, chiediti se rimarrà tale noexcepto limiterebbe le future implementazioni? Ad esempio, potresti voler introdurre il controllo degli errori di argomenti illegali lanciando eccezioni (ad esempio, per i test unitari), oppure potresti dipendere da un altro codice di libreria che potrebbe modificare le sue specifiche di eccezione. In tal caso, è più sicuro essere conservatori e omettere noexcept.

D'altra parte, se si è certi che la funzione non debba mai essere lanciata ed è corretto che faccia parte delle specifiche, è necessario dichiararla noexcept. Tuttavia, tieni presente che il compilatore non sarà in grado di rilevare violazioni noexceptse l'implementazione cambia.

  1. Per quali situazioni dovrei essere più attento all'uso di noexcept e per quali situazioni posso cavarmela con il noexcept implicito (falso)?

Esistono quattro classi di funzioni su cui dovresti concentrarti perché probabilmente avranno il maggiore impatto:

  1. operazioni di spostamento (sposta operatore di assegnazione e sposta costruttori)
  2. operazioni di scambio
  3. deallocatori di memoria (eliminazione operatore, eliminazione operatore [])
  4. distruttori (anche se questi sono implicitamente a noexcept(true)meno che tu non li crei noexcept(false))

Queste funzioni dovrebbero essere generalmente noexcepted è molto probabile che le implementazioni della libreria possano utilizzare la noexceptproprietà. Ad esempio, è std::vectorpossibile utilizzare operazioni di movimento senza lancio senza sacrificare forti garanzie di eccezione. Altrimenti, dovrà ricorrere agli elementi di copia (come in C ++ 98).

Questo tipo di ottimizzazione è a livello algoritmico e non si basa su ottimizzazioni del compilatore. Può avere un impatto significativo, soprattutto se gli elementi sono costosi da copiare.

  1. Quando posso realisticamente aspettarmi di osservare un miglioramento delle prestazioni dopo aver usato noexcept? In particolare, fornisci un esempio di codice per il quale un compilatore C ++ è in grado di generare un codice macchina migliore dopo l'aggiunta di noexcept.

Il vantaggio di noexceptnon specificare alcuna eccezione o throw()è che lo standard consente ai compilatori una maggiore libertà quando si tratta di impilare lo svolgimento. Anche nel throw()caso, il compilatore deve svolgere completamente lo stack (e deve farlo nell'esatto ordine inverso delle costruzioni di oggetti).

Nel noexceptcaso, d'altra parte, non è necessario farlo. Non è richiesto che lo stack debba essere svolto (ma al compilatore è ancora consentito farlo). Questa libertà consente un'ulteriore ottimizzazione del codice in quanto riduce il sovraccarico di essere sempre in grado di svolgere lo stack.

La domanda correlata su noexcept, svolgimento della pila e prestazioni va in maggiori dettagli sull'overhead quando è richiesto lo svolgimento della pila.

Raccomando anche il libro di Scott Meyers "Effective Modern C ++", "Articolo 14: Dichiarare le funzioni tranne se non emetteranno eccezioni" per ulteriori letture.


Sarebbe comunque molto più sensato se le eccezioni fossero implementate in C ++ come in Java, dove si contrassegna un metodo che potrebbe generare una throwsparola chiave anziché un noexceptnegativo. Non riesco proprio a ottenere alcune delle scelte di design C ++ ...
doc

Lo chiamarono noexceptperché throwera già stato preso. In poche parole throwpuò essere usato quasi come dici tu, tranne per il fatto che ne ha compromesso il design in modo che diventasse quasi inutile - anche dannoso. Ma ora ne siamo bloccati poiché rimuoverlo sarebbe un cambiamento decisivo con pochi benefici. Quindi noexceptè fondamentalmente throw_v2.
AnorZaken,

Come thrownon è utile?
curioso

@curiousguy lo stesso "lancio" (per il lancio di eccezioni) è utile, ma "lancio" come identificatore di eccezione è stato deprecato e in C ++ 17 addirittura rimosso. Per ragioni per le quali identificatori di eccezioni non sono utili, vedere questa domanda: stackoverflow.com/questions/88573/...
Philipp Claßen

1
@ PhilippClaßen L'identificatore di throw()eccezione non ha fornito la stessa garanzia di nothrow?
curioso

17

Ci sono molti esempi di funzioni che conosco non verranno mai lanciate, ma per le quali il compilatore non può determinarlo da solo. Devo aggiungere noexcept alla dichiarazione di funzione in tutti questi casi?

Quando dici "So che [non] non lanceranno mai", intendi esaminando l'implementazione della funzione, sai che la funzione non verrà lanciata. Penso che questo approccio sia capovolto.

È meglio considerare se una funzione può generare eccezioni come parte del progetto della funzione: importante quanto l'elenco degli argomenti e se un metodo è un mutatore (... const). Dichiarare che "questa funzione non genera eccezioni" è un vincolo per l'implementazione. Ometterlo non significa che la funzione potrebbe generare eccezioni; significa che la versione corrente della funzione e tutte le versioni future potrebbero generare eccezioni. È un vincolo che rende più difficile l'implementazione. Ma alcuni metodi devono avere il vincolo per essere praticamente utili; soprattutto, in modo che possano essere chiamati dai distruttori, ma anche per l'implementazione del codice "roll-back" in metodi che forniscono la forte garanzia di eccezione.


Questa è di gran lunga la risposta migliore. Stai offrendo una garanzia agli utenti del tuo metodo, che è un altro modo per dire che stai vincolando l'implementazione per sempre (senza cambiare il cambiamento). Grazie per la prospettiva illuminante.
AnorZaken,

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.