atto_finale - follow-up

atto_finale - follow-up

L'ultima volta che ho scritto su final_act utilità, e sembra che io sia entrato in un'area più ampia di cui non ero a conoscenza. Continuiamo con l'argomento e cerchiamo di capire alcuni dei problemi che sono stati menzionati nei commenti.

Introduzione

Ricordiamo com'è successo l'ultima volta:

Voglio chiamare un codice di pulizia personalizzato alla fine dell'ambito e voglio essere sicuro che venga richiamato.

bool Scanner::scanNodes()
{
    // code...
    addExtraNodes();
    auto _ = finally([] { removeExtraNodes(); });

    // code...

    return true;
}

Ho usato finally() da GSL che funziona internamente su final_act oggetto.

La cosa più importante!

OK, lo so... ho fatto un errore di battitura nel titolo del mio post originale! :)
L'ho provato più volte, ho inviato newsletter con il nome proprio... ma il post era sbagliato :)

GSL -> Libreria di supporto per linee guida, non GLS -> Libreria di supporto per linee guida

Caso d'uso importante

L'ultima volta ho dimenticato di menzionare un caso enorme in cui tutti quei scope_exit /final_act potrebbero essere utilizzate cose.

Intendo:transazioni . Questo è un termine generale per tutte le azioni che dovrebbero essere ripristinate quando qualcosa fallisce. Se hai copiato il 95% di un file e hai ricevuto un errore, non puoi lasciare questo file eventualmente danneggiato; devi rimuoverlo e forse ricominciare. Se ti sei connesso a un database e vuoi scrivere dei record, presumi che sia atomico .Penso che questa idea fosse "nascosta" da qualche parte nei miei esempi, ma dovrebbe essere più esposta.

Quindi, ogni volta che hai a che fare con un codice che deve essere atomico e transazionale, tali costrutti di codice potrebbero essere utili. A volte puoi avvolgerlo in un RAII; spesso è necessario utilizzare codice esplicito.

Nessuna eccezione

Prima di tutto, la mia ipotesi iniziale era di usare final_act in un ambiente dove non ci sono molte eccezioni. Ad esempio, molto codice legacy non utilizza eccezioni. Anche le linee guida per la codifica di Google C++ non preferiscono le eccezioni (per ragioni pratiche). Questo è un presupposto forte, lo so, forse l'ho fatto automaticamente :)

Senza la gestione delle eccezioni in giro, dobbiamo occuparci solo dei resi anticipati. In quel contesto, final_act funziona come previsto.

Con eccezioni

OK... allora quali sono i problemi con le eccezioni? final_act funzionerà nella maggior parte dei casi, quindi non rilasciarlo ogni volta che hai un codice con eccezioni... ma dobbiamo esaminare attentamente alcune parti delicate qui.

Prima cosa:l'atto finale è noException

Come spiegato molte volte attraverso i commenti nel repository GSL (per esempio qui), altri problemi

E da Final_act può portare alla chiusura del programma se l'atto finale genera un'eccezione:

In altre parole dovresti scrivere il codice che verrà chiamato con gli stessi presupposti dell'altro codice distruttore... quindi non gettare nulla lì. Potrebbe essere una piccola limitazione quando vuoi chiamare un codice "normale", non solo alcune cose di pulizia (d'altra parte potrebbe essere un cattivo design dopotutto?).

Ho appena notato un'ottima spiegazione del perché i distruttori non dovrebbero lanciare:

daisocpp.org/faq

Lancio da ctor o copy ctor

C'è un bug di vecchia data nell'implementazione attuale:

il lancio dei costruttori di copia e spostamento fa sì che final_act non esegua l'azione · Problema n. 283 ·Microsoft/GSL

Come aggirare il bug?

Stiamo esaminando questo codice:

explicit final_act(F f) noexcept 
: f_(std::move(f))
, invoke_(true)
{
}

final_act(final_act&& other) noexcept 
: f_(std::move(other.f_))
, invoke_(other.invoke_)
{
    other.invoke_ = false;
}

E soprattutto quelli f_(std::move(other.f_)) chiamate.

Il problema si verificherà se solleviamo un'eccezione dal move/copyconstructor. Come vedo questo, può succedere solo con il codice di spostamento personalizzato che abbiamo per l'oggetto richiamabile. Dovremmo essere al sicuro quando utilizziamo onlylambdas come in:

auto _ = finally([] { removeExtraNodes(); });

Poiché lambdas (aggiornamento:senza parametri) avrà un codice predefinito che non verrà lanciato.

Quindi forse non è un limite importante?

aggiornamento: Mi è sfuggita una cosa. Dai un'occhiata all'esempio fornito nel commento atr/cpp. Un'eccezione può anche essere generata da un costruttore copy/move da qualche argomento dell'oggetto lambda (poiché i lambda sono "internamente" rappresentati come oggetti functor e i loro parametri sono membri di thatfunctor). Tuttavia, questo è probabilmente un caso piuttosto raro.

Tuttavia, se prevedi di utilizzare alcuni functor richiamabili avanzati/personalizzati, con un codice di spostamento speciale, potrebbe essere utile prendere qualcosa di diverso da final_act .

Altre soluzioni

Ad essere onesto, ho anche supposto che da final_act è proposto in CoreGuidelines, quindi è la scelta migliore che abbiamo in Modern C++! Ma a quanto pare abbiamo altre possibilità:

Il discorso

Prima di tutto guarda questo:

CppCon 2015:Andrei Alexandrescu “Flusso di controllo dichiarativo”

La carta

E leggi questo:

PDF, P0052R3 - Scope Guard generico e wrapper RAII per StandardLibrary

Approssimativamente, il piano è di avere (C++20?) un set di strumenti:

  • std::scope_exit
  • std::scope_success
  • std::scope_fail

questo presuppone uncaught_exceptions() restituisce int non solo bool.

follia/ScopeGuard.h

C'è già un codice funzionante

follia/ScopeGuard.h -master

Lingua D

In D abbiamo un supporto integrato per tali strutture:

scope(exit) removeExtraNodes();

vedi qui per alcuni esempi Dlang:ExceptionSafety

Copia elisione

Il codice esistente ora funziona e non si basa sull'eliminazione della copia garantita che avremo in C++ 17. Per supportare questo devono introdurre quello speciale bool parametro.

Vedi la discussione in Final_act copy/move semantics è sbagliato

Riepilogo

Come appare final_act è una semplice utility che dovrebbe funzionare bene nel caso in cui il codice di uscita non generi eccezioni (e inoltre non viene generato dai costruttori di copia/sposta!). Tuttavia, se hai bisogno di alcune soluzioni più avanzate, potresti voler attendere il generalestd::scope_exit/_success/_fail utilità.

Uno dei casi d'uso più importanti è ogni volta che abbiamo bisogno di un approccio transazionale con alcune azioni. Quando è necessario chiamare un codice di pulizia dopo che è riuscito o non è riuscito.

Opinione sul meta-blogging:il bello del blog è che spesso scrivi su un argomento e scopri (per te stesso) aree completamente nuove. In questo modo il blog è un ottimo modo per imparare le cose!

A proposito:come compito puoi scrivere una macro FINALLY che racchiude la creazione della variabile automatica e si assicura che abbiamo un nome diverso per quella variabile, in modo che tu possa avere diversi blocchi finali in una funzione/ambito.