Design preferibile della protezione per l'ambito in C ++

2

Recentemente, mi sono imbattuto in un problema riguardante la progettazione della protezione per ambito. Una guardia di ambito richiama un oggetto funzione fornito (di solito esegue procedure di pulizia) all'uscita dall'ambito di inclusione. Il design attuale è il seguente:

#define HHXX_ON_SCOPE_EXIT_F(...) ...
#define HHXX_ON_SCOPE_EXIT(...) HHXX_ON_SCOPE_EXIT_F([&]() { __VA_ARGS__ })

HHXX_ON_SCOPE_EXIT_F(...) esegue l'oggetto funzione come definito da __VA_ARGS__ all'uscita dall'ambito di chiusura. Ad esempio,

int i = 0;
{
  HHXX_ON_SCOPE_EXIT_F([&]() {
    assert(++i == 4);
  });
  HHXX_ON_SCOPE_EXIT_F([&]() {
    assert(++i == 3);
  });
  HHXX_ON_SCOPE_EXIT(
    assert(++i == 2);
  );
  HHXX_ON_SCOPE_EXIT(
    assert(++i == 1);
  );
}
assert(i == 4);

Anche l'implementazione è semplice:

#define HHXX_ON_SCOPE_EXIT_F(...)                                              \
  auto HHXX_UNIQUE_NAME(hhxx_scope_guard) =                                    \
    make_scope_guard(__VA_ARGS__)

#define HHXX_ON_SCOPE_EXIT(...) HHXX_ON_SCOPE_EXIT_F([&]() { __VA_ARGS__ })

template <typename F>
class scope_guard {
public:
  explicit scope_guard(F f)
      : f_(std::move(f)) {
    // nop
  }
  ~scope_guard() {
    f_();
  }

private:
  F f_;
};

template <typename F>
auto make_scope_guard(F f) {
  return scope_guard<F>(std::move(f));
}

Come puoi vedere, non fornisce un supporto integrato per un metodo per abbandonare la chiamata all'uscita dell'ambito. Fornire tale funzionalità, tuttavia, annulla la concisione e la semplicità del design attuale. Induce anche alcuni overhead delle prestazioni. Sostanzialmente, sono favorevole al mantenimento dell'attuale design e al rifiuto della proposta di funzionalità. Voglio ascoltare le opinioni di voi ragazzi, per essere sicuri di prendere una decisione giusta.

    
posta Lingxi 29.01.2016 - 11:33
fonte

2 risposte

3

Bene, ci sono diversi tipi di guardie di campo che potresti creare, a seconda delle funzioni che ti servono:

  1. Può essere disarmato? Quello è gratuito, qualcosa di simile deve comunque essere implementato. Non si può dipendere dal fatto che RVO venga sempre in soccorso.
  2. Si esegue solo se viene lanciata un'eccezione? Questo ha bisogno del C ++ 17, sfortunatamente.

In ogni caso, esiste già una classe che corrisponde alla fattura: std::unique_ptr .
In realtà, ha solo bisogno di una semplice funzione di convenienza per facilità d'uso.

Per semplicità ho usato la deduzione automatica del tipo di ritorno C ++ 14:

template<class F>
auto finally(F f) noexcept(noexcept(F(std::move(f)))) {
    auto x = [f = std::move(f)](void*){ f(); };
    return std::unique_ptr<void, decltype(x)>((void*)1, std::move(x));
}

auto guard = finally([&]{ final_action(); });

Con C ++ 17, si può limitare a eseguire solo quando viene lanciata un'eccezione:

template<class F>
auto on_throw(F f) noexcept(noexcept(F(std::move(f)))) {
    auto x = [f = std::move(f)](void* p){ if((int)p > std::uncaught_exceptions()) f(); };
    return std::unique_ptr<void, decltype(x)>(
        (void*)(std::uncaught_exceptions() + 1), std::move(x));
}

auto rollback = on_throw([&]{ do_rollback(); });

In entrambi i casi, il disarmo è facilmente eseguibile chiamando .release() .

    
risposta data 29.01.2016 - 14:11
fonte
4

Il tuo scope guard ha un comportamento interessante, e una rapida revisione del codice potrebbe trovare vari problemi o possibili problemi (usare la macro richiede più codice di quello che non usa la macro, la tua macro è difettosa perché è una macro e non è necessaria; la macro non può essere usata due volte sulla stessa linea, cosa che potrebbe accadere nelle macro, molti tipi non sono costruttibili nel movimento, molti tipi non sono copiabili, si fa una copia, quindi si sposta la copia, non si può dare un riferimento a una funzione alla guardia perché crea una copia, idealmente, la callback utilizzata sarebbe nothrow ; ...).

Hai visto correttamente che disabilitare la guardia può essere banalmente implementato da un utente. Un'implementazione che vuole essere minima e componibile non deve offrire funzionalità utili.

Tuttavia, le funzionalità di convenienza sono estremamente convenienti e fanno la differenza tra un'implementazione corretta e un'implementazione utile. Nota che se imposti la tua guardia in termini di std::function o in termini di std::unique_ptr , è banale supportare la disattivazione della funzione senza ulteriori complicazioni.

L'ultima volta che ho usato una funzione del genere era quando I fork() ed un processo e volevo che la guardia fosse eseguita nel processo genitore, ad es. una guardia per registrare un'uscita dell'oscilloscopio. Il logging è un uso glorioso delle guardie.

Tuttavia, le guardie sono estremamente utili in un'altra circostanza: se voglio solo gestire casi eccezionali, ad es. per ripristinare una transazione se qualcosa va storto:

{
  Transaction transaction {};
  scope_guard rollbackGuard([&](){ transaction.rollback(); });
  ... // do things that could go wrong
  rollbackGuard.disarm();
  transaction.commit();
}

(nota l'uso di un nome di guardia per rendere il codice più auto-documentante.) Gensyms fa schifo).

Questo in genere verrebbe scritto con un try-catch:

{
  Transaction transaction {};
  try { ... } // do things that could go wrong
  catch (...) { transaction.rollback(); throw; }
  transaction.commit();
}

Si prega di capire Transaction come simbolo per alcuni calcoli; se fosse un oggetto singolare, dovrebbe ovviamente gestire il rollback nel proprio distruttore, a meno che il commit non abbia avuto successo.

Il grande vantaggio delle guardie rispetto a try-catch-finally è che una guardia mi consente di dichiarare il codice di uscita vicino ai dati con cui interagisce, piuttosto che 100 righe più in basso in un catch . Se consenti alle tue guardie di essere licenziate, le tue guardie non solo supportano l'equivalente di try-finally, ma anche try-catch-with-rethrow. Ti suggerisco caldamente di seguire questa strada, poiché aggiunge maggiore praticità agli utenti, con un impatto minimo sulla tua implementazione.

    
risposta data 29.01.2016 - 13:03
fonte

Leggi altre domande sui tag