risorse | good unit tests
Questa parte (la diciasettesima; qui la prima, qui la seconda, qui la terza, qui la quarta, qui la quinta, qui la sesta, qui la settima, qui l'ottava, qui la nona, qui la decima, qui l'undicesima, qui la dodicesima, qui la tredicesima, qui la quattordicesima, qui la quindicesima e qui la sedicesima) risolve un piccolo problema riscontrato nell'uso degli enumerati con il compilatore GCC.
Un test che coinvolge degli enumerati convenzionali – non quelli strong-typed del C++11, per intenderci – quando compilato con GCC con il flag -Wsign-promo emette un warning fastidioso:
// test-gut.cpp ... enum Level { e_low, e_medium, e_high, }; int main() { ... Level l = e_low; CHECK(l != e_low); assert(lastFailure == "[error] l != e_low evaluates to 0 != 0"); CHECK(l == e_high); assert(lastFailure == "[error] l == e_high evaluates to 0 == 2"); return 0; } /* output: (GCC 5.2.0) * * In file included from test-gut.cpp:2:0: * gut.h: In instantiation of 'std::__cxx11::string gut::toString(const T&) [with T * = Level; std::__cxx11::string = std::__cxx11::basic_string<char>]': * gut.h:273:29: required from 'std::__cxx11::string gut::BinaryExpression<T, U>: * :toString() const [with T = Level; U = Level; std::__cxx11::string = std::__cxx1 * 1::basic_string<char>]' * test-gut.cpp:598:1: required from here * gut.h:595:26: warning: passing 'Level' chooses 'int' over 'long unsigned int' [- * Wsign-promo] * os << std::boolalpha << value; * ^ * ... */
Il problema è dovuto alla serializzazione di una costante enum in uno ostream, il cui overload resolution causa una promozione del tipo intero sottostante l'enumerato a signed integer. Le soluzioni disponibili sono molteplici, ad esempio:
Il modo più immediato di risolvere il problema è quello di implementare l'operatore di ridirezione, in modo da evitare la risoluzione che genera il warning:
enum Level { e_low, e_medium, e_high, }; std::ostream& operator<<(std::ostream& os, Level level) { os << static_cast<int>(level); return os; }
L'ambiguità può essere eliminata utilizzando i nuovi tipi enumerati introdotti nel C++11, che non sono implicitamente convertibili a intero:
enum class Level { e_low, e_medium, e_high, }; int main() { ... Level l = Level::e_low; CHECK(l != Level::e_low); assert(lastFailure == "[error] l != Level::e_low evaluates to {?} != {?}"); CHECK(l == Level::e_high); assert(lastFailure == "[error] l == Level::e_high evaluates to {?} == {?}"); return 0; }
L'assenza del cast implicito ha una ricaduta negativa sulla rappresentazione delle costanti dell'enumerato: al loro posto compare ora il simbolo {?}, ad indicare che la libreria non ha modo di mostrarne il valore in nessuna delle forme predefinite – prima usava l'intero sottostante. Per rendere più espliciti i messaggi d'errore si può sempre ricorrere all'operatore di ridirezione:
std::ostream& operator<<(std::ostream& os, Level level) { os << static_cast<int>(level); return os; } int main() { ... Level l = Level::e_low; CHECK(l != Level::e_low); assert(lastFailure == "[error] l != Level::e_low evaluates to{?}0 !={?}0"); CHECK(l == Level::e_high); assert(lastFailure == "[error] l == Level::e_high evaluates to{?}0 =={?}2"); return 0; }
Per rappresentazioni più specifiche si può sfruttare una x-macro:
#define LEVELS(lambda_) \ lambda_(low) \ lambda_(medium) \ lambda_(high) #define LEVEL_ID(name_) e_ ## name_, #define LEVEL_ENTRY(name_) { Level::e_ ## name_, "Level::e_" #name_ }, enum class Level { LEVELS(LEVEL_ID)e_low,e_medium,e_high,}; #include <map> static std::map<Level, std::string> level_names = { LEVELS(LEVEL_ENTRY) }; std::ostream& operator<<(std::ostream& os, Level level) { os <<static_cast<int>(level)level_names[level]; return os; } int main() { ... Level l = Level::e_low; CHECK(l != Level::e_low); assert(lastFailure == "[error] l != Level::e_low evaluates to0Level::e_low !=0Level::e_low"); CHECK(l == Level::e_high); assert(lastFailure == "[error] l == Level::e_high evaluates to0Level::e_low ==2Level::e_high"); retur 0; }
Per fare in modo che GCC non emetta il warning in oggetto, si agisce sul file gut.h: per le versioni di GCC precedenti la 4.6, tramite una direttiva pragma che indica che il file dev'essere trattato alla stregua di un file di inclusione di sistema, causando la soppressione di tutta una classe di messaggi diagnostici, compreso quello in esame; per le versioni successive, disabilitando selettivamente il warning indesiderato nella porzione di codice che lo causa:
// gut.h ... #if defined __GNUC__ && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 6)) #pragma GCC system_header #endif namespace gut { ... template <typename T> std::string toString(const T& value) { std::ostringstream os; #if defined __GNUC__ && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 6) || __GNUC__ > 4) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsign-promo" #endif os << std::boolalpha << value; #if defined __GNUC__ && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 6) || __GNUC__ > 4) #pragma GCC diagnostic pop #endif return os.str(); }
Questa tecnica risulta utile quando non è possibile modificare il tipo enumerato o non è possibile – o non è opportuno – definire l'operatore di ridirezione.
Pagina modificata il 16/11/2015