risorse | good unit tests

Good Unit Tests /17

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.

Introduzione

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:

Definire l'operatore di ridirezione operator<< per il tipo enumerato

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;
}

Ricorrere ad una scoped enumeration

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 to 0Level::e_low != 0Level::e_low");
  CHECK(l == Level::e_high);
  assert(lastFailure ==
    "[error] l == Level::e_high evaluates to 0Level::e_low == 2Level::e_high");

  retur 0;
}

Inibire esplicitamente il warning

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.

Codice sorgente

Riferimenti

  1. “Are C++ enums signed or unsigned?”, StackOverflow — <http://stackoverflow.com/questions/159034/are-c-enums-signed-or-unsigned>, visitato il 16/11/2015.
  2. “Options Controlling C++ Dialect”, Using the GNU Compiler Collection (GCC) — <https://gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/C_002b_002b-Dialect-Options.html>, visitato il 16/11/2015.
  3. “TestAssert.h”, CppUnit project page — <http://people.freedesktop.org/~mmohrhard/cppunit/_test_assert_8h_source.html>, visitato il 16/11/2015.
  4. “What is the type of an enumeration such as enum Color? Is it of type int?”, C++ Super-FAQ — <https://isocpp.org/wiki/faq/newbie#enumeration-is-its-own-type>, visitato il 16/11/2015.

Pagina modificata il 16/11/2015