risorse | scopeguard

ScopeGuard

Definizioni

Un'operazione si definisce exception-safe se lascia l'oggetto (o gli oggetti) sul quale ha agito in uno stato consistente quando termina avendo sollevato un'eccezione. Non conta lo stato in cui l'oggetto si trova (può per esempio trattarsi di un particolare stato d'errore che richiede una esplicita cancellazione), ciò che importa è che si tratti di uno stato ben definito – condizione necessaria per scrivere il codice di gestione dell'eccezione.

Un'operazione exception-safe può offrire le seguenti garanzie in fatto di eccezioni (classificazione di Abrahams[1]):

Nota: un livello di garanzia che può essere offerto da operazioni non exception-safe è la cosiddetta no leak guarantee, che assicura la corretta gestione delle risorse, senza tuttavia dire nulla circa lo stato finale dell'oggetto.

La garanzia basic assicura che l'oggetto, trovandosi in uno stato valido anche in seguito al sollevamento di un'eccezione, può ancora essere utilizzato. Tuttavia, il fatto che lo stato in cui si trova, per quanto valido, sia ignoto, a volte lo rende di fatto inutilizzabile:

std::vector<int> v;

for (size_t i = 0; i < items; i++)
{
  try
  {
    v.push_back(rand());
  }
  catch (...)
  {
    // ignore any exception
  }
}

// how many random numbers will v contain???

In alcune circostanze un'operazione fornisce garanzie diverse, in funzione di specifiche condizioni al contorno: la funzione di libreria vector<T>::erase, ad esempio, di base fornisce una garanzia basic, che diventa no-throw se il costruttore di copia e l'operatore di assegnamento del tipo T non sollevano eccezioni.

Implementazione

Scrivere codice exception-safe è particolarmente difficile. Gli strumenti che il C++ mette a disposizione per implementare operazioni exception-safe sono il costrutto try/catch e il supporto per l'idioma RAII – Resource Acquisition Is Initialization.

Le principali linee guida da seguire nella scrittura di codice exception-safe sono riassumibili nei seguenti punti:

Si consideri a titolo d'esempio la seguente funzione:

void SetName(const std::string& newName) {
  m_name = newName;
  // code that can throw an excpetion
}

Per rendere il codice exception-safe è necessario annullare l'effetto dell'assegnamento m_name = newName; una soluzione (ben poco elegante) prevede l'uso un blocco try/catch:

void Safe::SetName(const std::string& newName) {
  std::string oldName = m_name;
  m_name = newName;
  try {
      // code that can throw an excpetion
  } catch (...) {
    m_name = oldName;
  }
}

Il numero di righe di codice sono lievitate, offuscando la semantica dell'operazione. Una soluzione alternativa è l'uso dell'idioma RAII, sfruttando un oggetto che nel distruttore annulli l'effetto dell'assegnamento, a meno che non venga esplicitamente istruito diversamente:

class RevertableString
{
  std::string& m_string;
  std::string m_content;
  bool m_commit;
public:
  RevertableString(std::string& string)
    : m_string(string), m_content(string), m_commit(false) {
  }
  ~RevertableString() {
    if (!m_commit)
      m_string = m_content;
  }
  void Commit() {
    m_commit = true;
  }
};

// ...

void Safe::SetName(const std::string& newName) {
  RevertableString guard(m_name);
  m_name = newName;
  // code that can throw an excpetion
  guard.Commit();
}

Se il codice richiamato da SetName solleva un'eccezione, il distruttore di guard effettua il ripristino del valore originale di m_name; se l'esecuzione del metodo va a buon fine, la chiamata Commit disinnesca il distruttore di guard.

Per quanto interessante, questo approccio ha lo svantaggio di costringere il programmatore a definire un discreto numero di classi di supporto. Andrei Alexandrescu e Petru Marginean hanno generalizzato questa idea in ScopeGuard[2], un oggetto che, se richiesto, nel suo distruttore effettua una chiamata a metodo, funzione o functor.

Ripristino mediante chiamata a metodo d'oggetto

Si consideri la seguente definizione:

#include <stdexcept>
#include <string>

class Safe {
  std::string m_name;
protected:
  void SetName(const std::string& newName, bool fail) {
    m_name = newName;
    if (fail)
      throw std::runtime_error("error");
  }
public:
  const std::string& GetName() const {
    return m_name;
  }
  void SetNameSafely(const std::string& newName) {
    SetName(newName, false);
  }
  void SetNameAndFail(const std::string& newName) {
    SetName(newName, true);
  }
};

La classe non fornisce una garanzia strong, come dimostra il codice che segue:

#include <cassert>

int main(int argc, char* argv []) {
  Safe safe;
  assert(safe.GetName() == "");
  safe.SetNameSafely("buzz");
  assert(safe.GetName() == "buzz");

  try {
    safe.SetNameAndFail("fuzz");
    assert(false);
  } catch (...) {
  }

  assert(safe.GetName() == "buzz");
  return 0;
}

Volendo generalizzare l'idea alla base di RevertableString, nell'intento di annullare l'effetto di Safe::SetNameAndFail con una chiamata Safe::SetNameSafely (che è no-throw), si introduce la classe ScopeGuardForMethod1 (Method indica che la classe è predisposta alla chiamata di un metodo, 1 indica il numero di parametri del metodo):

template <class Object, typename Method, typename Param>
class ScopeGuardForMethod1 {
  Object& m_object;
  Method m_method;
  Param m_param;
  bool m_commit;
public:
  ScopeGuardForMethod1(Object& object, Method method, const Param& param)
    : m_object(object), m_method(method), m_param(param), m_commit(false) {
  }
  ~ScopeGuardForMethod1() {
    if (!m_commit)
      (m_object.*m_method)(m_param);
  }
  void Commit() {
    m_commit = true;
  }
};

Il metodo Safe::SetName può essere così riscritto:

  void SetName(const std::string& newName, bool fail) {
    ScopeGuardForMethod1<Safe, void (Safe::*)(const std::string&), std::string>
      guard (*this, &Safe::SetNameSafely, m_name);
    m_name = newName;
    if (fail)
      throw std::runtime_error("error");
    guard.Commit ();
  }

Per evitare di dichiarare esplicitamente il tipo di guard (risultando questo spesso tutt'altro che banale), si ricorre ad una caratteristica del linguaggio C++, per la quale quando un oggetto temporaneo viene associato ad una referenza (che deve necessariamente essere costante), la vita del temporaneo viene estesa a quello della referenza. L'idea dunque è quella di definire una classe base per ScopeGuardForMethod1, e tenere in vita il temporaneo per mezzo di una referenza costante alla sua classe base:

class ScopeGuardBase {
  // ...
};

template <class Object, typename Method, typename Param>
class ScopeGuardForMethod1 : public ScopeGuardBase {
  // ...

class Safe {
  // ...
  void SetName(const std::string& newName, bool fail) {
    const ScopeGuardBase& guard = ...

L'oggetto temporaneo si può ottenere come valore di ritorno di una funzione ausiliaria:

template <class Object, typename Method, typename Param>
ScopeGuardForMethod1<Object, Method, Param> CreateGuard(
  Object& object, Method method, const Param& param) {
  return ScopeGuardForMethod1<Object, Method, Param<(object, method, param);
}

guard può essere così istanziata più agevolmente:

class Safe {
  // ...
  void SetName(const std::string& newName, bool fail) {
    const ScopeGuardBase& guard = CreateGuard(*this, &Safe::SetNameSafely, m_name);

Con poco altro zucchero sintattico si giunge ad alla forma:

typdef const ScopeGuardBase& Guard;

class Safe {
  // ...
  void SetName(const std::string& newName, bool fail) {
    Guard guard = CreateGuard(*this, &Safe::SetNameSafely, m_name);

La rifattorizzazione appena compiuta, oltre a rendere più leggibile il codice, ha il vantaggio di isolare la gestione del flag commit nella nuova classe base ScopeGuardBase, oltre a unificare la definizione delle guardie, grazie alla deduzione automatica dei tipi e dell'overloading: indipendentemente dal numero e dal tipo di parametri, l'istanziazione di guard può avvenire sfruttando una generica chiamata CreateGuard. Di seguito è riportato il codice completo per la nuova versione di ScopeGuardForMethod1 (la generalizzazione ad un differente numero di parametri è banale); degni di nota il costruttore di copia della classe base, che si occupa di trasferire la responsabilità del ripristino finale, e il distruttore, non virtuale, che cattura le eventuali eccezioni sollevate dal codice di ripristino:

class ScopeGuardBase {
protected:
  mutable bool m_commit;
public:
  ScopeGuardBase()
    : m_commit(false) {
  }
  ScopeGuardBase(const ScopeGuardBase& guard)
    : m_commit(guard.m_commit) {
    guard.m_commit = true;
  }
  void Commit() const {
    m_commit = true;
  }
};

typedef const ScopeGuardBase& Guard;

template <class Object, typename Method, typename Param>
class ScopeGuardForMethod1 : public ScopeGuardBase {
  Object& m_object;
  Method m_method;
  Param m_param;
public:
  ScopeGuardForMethod1(Object& object, Method method, const Param& param)
    : m_object(object), m_method(method), m_param(param) {
  }
  ~ScopeGuardForMethod1() {
    if (!m_commit)
      try {
        (m_object.*m_method)(m_param);
      } catch (...) {
      }
  }
};

template <class Object, typename Method, typename Param>
ScopeGuardForMethod1<Object, Method, Param> CreateGuard(
  Object& object, Method method, const Param& param) {
  return ScopeGuardForMethod1<Object, Method, Param>(object, method, param);
}

Non c'è la necessità di dichiarare il distruttore virtual perché in gioco non ci sono metodi virtuali, ed essendo stato allocato un oggetto temporaneo di tipo ScopeGuardForMethod1, sarà esattamente il suo distruttore ad essere chiamato, non quello del tipo della referenza che lo ha mantenuto in vita.

Ripristino mediante chiamata a funzione

Si consideri il seguente esempio, che si interrompe in corrispondenza dell'ultima assert:

#include <cassert>
#include <stdexcept>

bool g_busy = false;

void Acquire() {
  g_busy = true;
}

void Release() {
  g_busy = false;
}

void UseResource(bool fail = false) {
  Acquire();
  if (fail)
    throw std::runtime_error("error");
  Release();
}

void UseResourceAndFail() {
  UseResource(true);
}

int main(int argc, char* argv[]) {
  assert (g_busy == false);
  UseResource();
  assert (g_busy == false);

  try {
    UseResourceAndFail();
    assert(false);
  } catch (...) {
  }

  assert(g_busy == false);
  return 0;
}

In questo caso, il ripristino delle condizioni precedenti alla chiamata UseResource si ottiene chiamando la funzione Release. È semplice, a partire da ScopeGuardForMethod1, definire una nuova classe che effettua una chiamata a funzione senza parametri:

template <typename Func>
class ScopeGuardForFunc0 : public ScopeGuardBase {
  Func m_func;
public:
  ScopeGuardForFunc0(const Func& func)
    : m_func (func) {
  }
  ~ScopeGuardForFunc0() {
    if (!m_commit)
      try {
        m_func();
      } catch (...) {
      }
  }
};

template <typename Func>
ScopeGuardForFunc0<Func> CreateGuard(Func func) {
  return ScopeGuardForFunc0<Func>(func);
}

// ...

void UseResource(bool fail = false) {
  Acquire();
  Guard guard = CreateGuard(Release);
  if (fail)
    throw std::runtime_error("error");
  Release();
  guard.Commit();
}

ScopeGuardForFunc0 supporta anche functor, come dimostra l'esempio seguente:

class Releaser {
public:
  void operator()() {
    Release();
  }
};

// ...

void UseResource(bool fail = false) {
  Acquire();
  Guard guard = CreateGuard(Releaser());
  if (fail)
    throw std::runtime_error("error");
  Release();
  guard.Commit();
}

Questo esempio si presta ad un'ulteriore considerazione: gli oggetti guard possono essere utilizzati come veri e propri deallocatori automatici, in pieno stile RAII, se si omette la chiamata Commit finale:

void UseResource(bool fail = false) {
  Acquire();
  Guard guard = CreateGuard(Release);
  if (fail)
    throw std::runtime_error("error");
  // guard will always call Release!
}

Passaggio di parametri per referenza

Un ultimo aspetto da considerare riguarda il passaggio di parametri per referenza; modificando il codice dell'esempio precedente, introducendo l'uso di una referenza, il rilascio della risorsa acquisita in UseResourceAndFail avviene in modo scorretto:

#include <cassert>
#include <stdexcept>

class ScopeGuardBase {
protected:
  mutable bool m_commit;
public:
  ScopeGuardBase()
    : m_commit(false) {
  }
  ScopeGuardBase(const ScopeGuardBase& guard)
    : m_commit(guard.m_commit) {
    guard.m_commit = true;
  }
  void Commit() const {
    m_commit = true;
  }
};

typedef const ScopeGuardBase& Guard;

template <typename Func, typename Param>
class ScopeGuardForFunc1 : public ScopeGuardBase {
  Func m_func;
  Param m_param;
public:
  ScopeGuardForFunc1(Func func, const Param& param)
    : m_func(func), m_param(param) {
  }
  ~ScopeGuardForFunc1() {
    if (!m_commit)
      try {
        m_func(m_param);
      } catch (...) {
      }
  }
};

template <typename Func, typename Param>
ScopeGuardForFunc1<Func, Param> CreateGuard(Func func, const Param& param) {
  return ScopeGuardForFunc1<Func, Param>(func, param);
}

void Acquire(bool& busy) {
  busy = true;
}

void Release(bool& busy) {
  busy = false;
}

void UseResource(bool& busy, bool fail = false) {
  Acquire(busy);
  Guard guard = CreateGuard(Release, busy);
  if (fail)
    throw std::runtime_error("error;");
  Release(busy);
  guard.Commit();
}

void UseResourceAndFail(bool& busy) {
  UseResource(busy, true);
}

int main(int argc, char* argv[]) {
  bool busy = false;
  UseResource(busy);
  assert (busy == false);

  try {
    UseResourceAndFail(busy);
    assert(false);
  } catch (...) {
  }

  assert(busy == false);
  return 0;
}

Il problema nasce dal fatto che ScopeGuardForFunc1 mantiene una copia dei parametri passati in fase di istanziazione. Il supporto per le referenze può tuttavia essere facilmente introdotto sfruttando un oggetto che renda la referenza copiabile:

template <typename T>
class Reference {
  T& m_ref;
public:
  Reference (T& ref) : m_ref (ref) { }
  operator T& () const { return m_ref; }
};

template <class T>
inline Reference<T> ByRef (T& t) {
  return Reference<T> (t);
}

ByRef è una funzione utile per trasformare una referenza in un oggetto sul posto:

void UseResource(bool& busy, bool fail = false) {
  Acquire(busy);
  Guard guard = CreateGuard(Release, ByRef(busy));
  if (fail)
    throw std::runtime_error("error;");
  Release(busy);
  guard.Commit();
}

Un'accortezza che consente di individuare i casi in cui si definisce una guard su una referenza non costante – e che dovrebbe perciò essere trasformata in un oggetto Reference – consiste nel definire const le variabili membro di ScopeGuardForXYZ; in questo modo, in assenza di una sola delle chiamate ByRef necessarie, il compilatore si rifiuterà di compilare la chiamata inserita nel distruttore:

template <typename Func, typename Param>
class ScopeGuardForFunc1 : public ScopeGuardBase {
  Func m_func;
  const Param m_param; // const!
  // ...

Riferimenti

  1. Abrahams, D. "Exception-Safety in Generic Components". boost.org. <http://www.boost.org/community/exception_safety.html>. Visitato il 4 Novembre 2011.
  2. Alexandrescu, A. and Marginean, P. "Generic: Change the Way You Write Exception-Safe Code – Forever". Dr.Dobb's. December 2000. <http://drdobbs.com/cpp/184403758>. Visitato il 5 Ottobre 2011.
  3. Stroustrup, B. "Standard-Library Exception Safety". AT&T Labs Research. <http://www2.research.att.com/~bs/3rd_safe.pdf>. Visitato il 5 Ottobre 2011.

Pagina modificata l'8/11/2011