risorse | funzioni di lippincott

Funzioni di Lippincott

Introduzione

LE funzioni di Lippincott, così battezzate da Jon Kalb[3], catturano eccezioni C++ e le convertono in codici d'errore. Trovano uso nell'interfacciamento di codice C e C++, in particolare quando si vuole rendere disponibili in C delle funzionalità sviluppate in C++. La conversione è necessaria in quanto le eccezioni C++ innescano un undefined behaviour quando raggiungono il runtime del C:

…it's undefined behavior to let a C++ exception propagate through C code…[4]
…make sure you catch all possible exceptions in your C-callable C++ functions…[6]
…the C++ standard is somewhat vague about whether you can expect exceptions to behave properly…[7]

Cattura delle eccezioni alla barriera C/C++

La modalità usuale con cui una funzionalità C++ che solleva delle eccezioni viene pubblicata in C è la seguente:

#include <cassert>
#include <stdexcept>

void f(int i) {
  if (i < 0)
    throw std::out_of_range("negative value");
  else if (i % 2 == 1)
    throw std::invalid_argument("odd value");
  // do something with i...
}

// step 1: explicit capture

enum RESPONSE {
  OK,
  INVALID_ARGUMENT,
  OUT_OF_RANGE,
  UNKNOWN_ERROR,
};

extern "C" RESPONSE f_(int i) {
  try {
    f(i);
    return OK;
  } catch (const std::invalid_argument&) {
    return INVALID_ARGUMENT;
  } catch (const std::out_of_range&) {
    return OUT_OF_RANGE;
  } catch (...) {
    return UNKNOWN_ERROR;
  }
}

int main() {
  assert(f_(42) == OK);
  assert(f_(-1) == OUT_OF_RANGE);
  assert(f_(11) == INVALID_ARGUMENT);

  return 0;
}

Se le funzioni da pubblicare sono diverse, ognuna con la sua particolare lista di eccezioni da cattuare, è facile farsi “sfuggire” qualche tipo di eccezione, specie se il codice C++ è soggetto ad aggiornamenti, modifiche o refactoring:

// ...

void g(double a, double b) {
  if (a > b)
    throw std::invalid_argument("a greater than b");
  else if (a == 0)
    throw std::domain_error("a is zero");
  // do something with a and b...
}

// ...

enum RESPONSE {
  OK,
  INVALID_ARGUMENT,
  OUT_OF_RANGE,
  DOMAIN_ERROR,
  UNKNOWN_ERROR,
};

extern "C" RESPONSE g_(double a, double b) {
  try {
    g(a, b);
    return OK;
  } catch (const std::invalid_argument&) {
    return INVALID_ARGUMENT;
  } catch (const std::domain_error&) {
    return DOMAIN_ERROR;
  } catch (...) {
    return UNKNOWN_ERROR;
  }
}

int main() {
  assert(f_(42) == OK);
  assert(f_(-1) == OUT_OF_RANGE);
  assert(f_(11) == INVALID_ARGUMENT);

  assert(g_(1, 1) == OK);
  assert(g_(2, 1) == INVALID_ARGUMENT);
  assert(g_(0, 1) == DOMAIN_ERROR);

  return 0;
}

Conviene dunque centralizzare il codice di cattura e conversione delle eccezioni. Un modo per farlo è introducendo delle apposite macro:

// step 2: macro-based capture

#define BEGIN_C_BOUNDARY \
  try {

#define END_C_BOUNDARY \
    return OK; \
  } \
  catch (const std::invalid_argument&) { \
    return INVALID_ARGUMENT; \
  } catch (const std::out_of_range&) { \
    return OUT_OF_RANGE; \
  } catch (const std::domain_error&) { \
    return DOMAIN_ERROR; \
  } catch (...) { \
    return UNKNOWN_ERROR; \
  }

extern "C" RESPONSE f_macro(int i) {
BEGIN_C_BOUNDARY
  f(i);
END_C_BOUNDARY
}

extern "C" RESPONSE g_macro(double a, double b) {
BEGIN_C_BOUNDARY
  g(a, b);
END_C_BOUNDARY
}

int main() {
  assert(f_(42)   == OK);
  assert(f_(-1)   == OUT_OF_RANGE);
  assert(f_(11)   == INVALID_ARGUMENT);

  assert(g_(1, 1) == OK);
  assert(g_(2, 1) == INVALID_ARGUMENT);
  assert(g_(0, 1) == DOMAIN_ERROR);

  assert(f_macro(42)   == OK);
  assert(f_macro(-1)   == OUT_OF_RANGE);
  assert(f_macro(11)   == INVALID_ARGUMENT);

  assert(g_macro(1, 1) == OK);
  assert(g_macro(2, 1) == INVALID_ARGUMENT);
  assert(g_macro(0, 1) == DOMAIN_ERROR);

  return 0;
}

Un modo alternativo consiste nello sfruttare una funzione ausiliaria error_code_from_exception, la cosiddetta funzione di Lippincott:

// step 3. function-based capture

RESPONSE error_code_from_exception() noexcept {
  try {
    throw; // re-throw the current exception
  } catch (const std::invalid_argument&) {
    return INVALID_ARGUMENT;
  } catch (const std::out_of_range&) {
    return OUT_OF_RANGE;
  } catch (const std::domain_error&) {
    return DOMAIN_ERROR;
  } catch (...) {
    return UNKNOWN_ERROR;
  }
}

extern "C" RESPONSE f_lippincott(int i) {
  try {
    f(i);
    return OK;
  }
  catch (...) {
    return error_code_from_exception();
  }
}

extern "C" RESPONSE g_lippincott(double a, double b) {
  try {
    g(a, b);
    return OK;
  }
  catch (...) {
    return error_code_from_exception();
  }
}

int main() {
  // ...

  assert(f_lippincott(42) == OK);
  assert(f_lippincott(-1) == OUT_OF_RANGE);
  assert(f_lippincott(11) == INVALID_ARGUMENT);

  assert(g_lippincott(1, 1) == OK);
  assert(g_lippincott(2, 1) == INVALID_ARGUMENT);
  assert(g_lippincott(0, 1) == DOMAIN_ERROR);

  return 0;
}

error_code_from_exception è dichiarata noexcept per garantire che nessuna eccezione possa sfuggire alle funzioni *_lippincott. Se un'eccezione dovesse propagarsi al di fuori di error_code_from_exception, per effetto della dichiarazione noexcept il programma terminerebbe immediatamente — attraverso una chiamata a std::terminate(): in questo modo l'undefined behaviour è scongiurato.

Poiché error_code_from_exception forza il risollevamento dell'eccezione corrente, è necessario che tale eccezione esista; per questo motivo la funzione va richiamata esclusivamente all'interno di un blocco catch — una throw; al di fuori di un blocco catch causa una chiamata immediata a std::terminate().

#include <cassert>
#include <iostream>
#include <stdexcept>
// ...

int main() {
  // ...

  std::cout << "response: " << error_code_from_exception() << std::endl; // boom!

  return 0;
}

Se si intende usare error_code_from_exception anche in altri ambiti, la si può rendere più sicura verificando esplicitamente l'esistenza dell'eccezione prima di risollevare l'eccezione:

RESPONSE error_code_from_exception() noexcept {
  try {
    if (std::current_exception())
      throw; // re-throw the current exception
    else
      return OK;
  } catch (const std::invalid_argument&) {
    return INVALID_ARGUMENT;
  } catch (const std::out_of_range&) {
    return OUT_OF_RANGE;
  } catch (const std::domain_error&) {
    return DOMAIN_ERROR;
  } catch (...) {
    return UNKNOWN_ERROR;
  }
}
// ...

/* output:
 *
 * response: 0
 */

Funzioni Lippincott e lambda

In [5] viene utilizzata una tecnica piuttosto interessante, che consiste nel passare alla funzione di cattura e conversione delle eccezioni anche il frammento di codice C++, sotto forma di funzione lambda. Il codice di interfacciamento diventa così più compatto:

template <typename F>
RESPONSE lippincott(F&& f) noexcept {
  try {
    f();
    return OK;
  } catch (...) {
    return error_code_from_exception();
  }
}

extern "C" RESPONSE f_lippincott_lambda(int i) {
  return lippincott([&]{
    f(i);
  });
}

extern "C" RESPONSE g_lippincott_lambda(double a, double b) {
  return lippincott([&]{
    g(a, b);
  });
}
// ...

int main() {
  // ...

  assert(f_lippincott_lambda(42) == OK);
  assert(f_lippincott_lambda(-1) == OUT_OF_RANGE);
  assert(f_lippincott_lambda(11) == INVALID_ARGUMENT);

  assert(g_lippincott_lambda(1, 1) == OK);
  assert(g_lippincott_lambda(2, 1) == INVALID_ARGUMENT);
  assert(g_lippincott_lambda(0, 1) == DOMAIN_ERROR);

  return 0;
}

La forma generale della barriera C++/C diventa quindi:

extern "C" RESPONSE fun_name(args) {
  return lippincott([&]{
    // c++ code that may throw
  });
}

Zucchero sintattico

Nel suo intervento dal titolo “Error Handling in C++” — successivamente ribattezzato “Declarative Control Flow” — all'NDC di Oslo del 2014, Alexandrescu fa uso di un oggetto temporaneo e di una particolare forma dell'operator+ per specificare una funzione lambda riducendo al minimo il carico sintattico:

// original code
// ...
  return lippincott([&]{
    // c++ code that may throw
  });


// auxiliary object with a custom operator+ that
// takes a callable and wraps it with a `lippincott` call
struct ErrorCodeFromException {
  template <typename F>
  RESPONSE operator+(F&& f) {
    return lippincott(f);
  }
};

// modified code that instantiates a temporary ErrorCodeFromException
// and passes the C++ code to wrap through a call to the operator+ method
// ...
  return ErrorCodeFromException() + [&]{
    // c++ code that may throw
  };

L'uso dell'operator+ (o di un qualunque altro operatore binario) consente di eliminare la coppia di parentesi tonde. L'istanziazione dell'oggetto temporaneo e il preambolo della dichiarazione lambda (e, a seconda dei casi, pure il return) possono a questo punto essere nascosti all'interno di una macro:

#define ERROR_CODE_FROM_EXCEPTION \
  ErrorCodeFromException() + [&]

// final code
// ...
  return ERROR_CODE_FROM_EXCEPTION {
    // c++ code that may throw
  };

Il codice nella sua forma definitiva diventa quindi:

// ...

extern "C" RESPONSE f_lippincott_final(int i) {
  return ERROR_CODE_FROM_EXCEPTION {
    f(i);
  };
}

extern "C" RESPONSE g_lippincott_final(double a, double b) {
  return ERROR_CODE_FROM_EXCEPTION {
    g(a, b);
  };
}

int main() {
  // ...

  assert(f_lippincott_final(42) == OK);
  assert(f_lippincott_final(-1) == OUT_OF_RANGE);
  assert(f_lippincott_final(11) == INVALID_ARGUMENT);

  assert(g_lippincott_final(1, 1) == OK);
  assert(g_lippincott_final(2, 1) == INVALID_ARGUMENT);
  assert(g_lippincott_final(0, 1) == DOMAIN_ERROR);

  return 0;
}

Sorgenti

Riferimenti

  1. Alexandrescu, A. “Error Handling in C++”. NDC Oslo 2014. <http://vimeo.com/97329153>. Visitato il 26/08/2014.
  2. Guillemot, N. “C++ Secrets - Using a Lippincott Function for Centralized Exception Handling”. <http://cppsecrets.blogspot.it/2013/12/using-lippincott-function-for.html>. Visitato il 29/07/2014.
  3. Kalb, J. “Exception-Safe Coding in C++”. <http://exceptionsafecode.com/>, visitato il 29/07/2014.
  4. “Can C functions marked as extern c throw”. StackOverflow. <http://stackoverflow.com/questions/15845681/can-c-functions-marked-as-extern-c-throw>. Visitato il 29/07/2014.
  5. “Exception Boundaries”. MSDN. <http://blogs.msdn.com/b/vcblog/archive/2014/01/16/exception-boundaries.aspx>. Visitato il 29/07/2014.
  6. “How can I create a C++ function f(int,char,float) that is callable by my C code?”. C++ FQA Lite. <http://yosefk.com/c++fqa/mixing.html#fqa-32.6>. Visitato il 29/07/2014.
  7. “Mixing C and C++ code”. oracle.com. <http://www.oracle.com/technetwork/articles/servers-storage-dev/mixingcandcpluspluscode-305840.html>. Visitato il 29/07/2014.

Pagina modificata il 26/08/2014