risorse | good unit tests

Good Unit Tests /1 bis

Introduzione

La macro CHECK descritta nella prima parte di questo ciclo presenta, all'atto pratico, alcuni problemi; in particolare:

Il secondo punto, essendo quello che richiede uno sforzo maggiore per essere risolto, verrà affrontato per primo.

Signed vs. unsigned

Il primo test sul campo dell'infrastruttura di test, per quanto banale, ha da subito evidenziato un problema (ove si intende che gut.h contiene tutte le definizioni fin qui descritte):

#include "gut.h"

class recently_used_list {
public:
  bool empty() const {
    return true;
  }
  size_t size() const {
    return 0;
  }
};

int main() {
  recently_used_list anEmptyList;

  CHECK(anEmptyList.size() == 0);
  return 0;
}

/* output:
 *
 * ...\recently_used_list.cpp(16): warning C4389: '==' : signed/unsigned mismatch
 */

Il warning è dovuto al fatto che i termini dell'espressione sono confrontati tra loro così come sono stati catturati:

template<typename T>
class Term {
  //...
  template<typename U>
  bool operator==(const U& rhs) const {
    return Expression::logAndEvaluate(Equal<T, U>(lhs_, rhs));
  }

La segnalazione può può essere eliminata con una conversione preliminare dei dati. È necessario allora disaccoppiare le due attività di cattura e comparazione, per avere l'opportunità, una volta catturati i due termini, di convertire il termine signed in unsigned. Il primo passo da compiere consiste dunque nel separare cattura e confronto:

template<class E, typename T, typename U>
bool compare(const T& lhs, const U& rhs) {
  return Expression::logAndEvaluate(E(lhs, rhs));
}

template<typename T>
class Term {
  // ...
  template<typename U>
  bool operator==(const U& rhs) const {
    return compare<Equal<T, U>,T, U>(lhs_, rhs);
  }
  // ...
};

Ciò tuttavia non è sufficiente, poiché la funzione compare riceve un oggetto Expression già tipizzato su T, U: serve un ulteriore grado di indirezione, che istanzi l'oggetto Expression solo dopo che i tipi T e U sono stati identificati e la conversione attuata; a tal fine compare potrebbe ricevere un token che indica il tipo di confronto richiesto, anziché direttamente l'oggetto che lo implementa:

enum Operator {
  e_equal,
  // ...
};

template<typename T>
class Term {
  // ...
  template<typename U>
  bool operator==(const U& rhs) const {
    return compare<e_equal,T, U>(lhs_, rhs);
  }
  // ...
};

compare a sua volta usa Operator per discriminare il tipo Expression da istanziare:

template<Operator op, typename T, typename U>
bool compare(const T& lhs, const U& rhs) {
  return ExprFactory<T, U, op>::logAndEvaluate(lhs, rhs);
}

ExprFactory si occupa di istanziare l'oggetto Expression adatto, e ne invoca il metodo logAndEvaluate (che a questo punto da statico diventa di istanza):

struct Expression {
  // ...
  bool logAndEvaluate() {
    Expression::last = toString();
    return evaluate();
  }
  // ...
};

// ...

template<typename T, typename U, Operator op>
struct ExprFactory {
  static bool logAndEvaluate(const T& /*lhs*/, const U& /*rhs*/) {
    OPERATION_NOT_SUPPORTED dummy;
    return false;
  }
};

template<typename T, typename U>
struct ExprFactory<T, U, e_equal> {
  static bool logAndEvaluate(const T& lhs, const U& rhs) {
    return Equal<T, U>(lhs, rhs).logAndEvaluate();
  }
};

Siamo prossimi alla soluzione; quel che resta da fare, alla luce del fatto che il partial template specialization non è applicabile alle funzioni, è di sfruttare l'overloading per gestire i confronti sui tipi numerici signed/unsigned:

template<typename T>
class Term {
  // ...
  template<typename U>
  bool operator==(const U& rhs) const {
    return compare<e_equal>(lhs_, rhs);
  }
  // ...
};

template<Operator op>
bool compare(unsigned int lhs, const int rhs) {
  return compare<op>(lhs, static_cast<unsigned int>(rhs));
}

L'esempio riporta la specializzazione di compare per il caso unsigned int/int; le altre combinazioni sono gestite in modo analogo.

Equal_ e NotEqual_

Con l'introduzione della funzione globale compare viene meno la necessità delle gerarchie facenti capo alle classi Equal_ e NotEqual_ per la comparazione dei puntatori con NULL: ciò che si è ottenuto con la specializzazione parziale di template si può ora più semplicemente realizzare con l'overloading:


template<typename T, typename U>
struct Equal_Equal : public BinaryExpression<T, U> {
  Equal_Equal(const T& lhs, const U& rhs) : BinaryExpression<T, U>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ == this->rhs_; }
  virtual std::string getOpName() const { return "=="; }
};

template<typename T, typename U>
struct Equal : public Equal_<T, U> {
  Equal(const T& lhs, const U& rhs) : Equal_<T, U>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ == this->rhs_; }
};

template<typename T>
struct Equal<T*, int> : public Equal_<T*, int> {
  Equal(T* const& lhs, const int& rhs) : Equal_<T*, int>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ == reinterpret_cast<T*>(this->rhs_); }
};

template<typename T>
struct Equal<int, T*> : public Equal_<int, T*> {
  Equal(const int& lhs, T* const& rhs) : Equal_<int, T*>(lhs, rhs) { }
  virtual bool evaluate() const { return reinterpret_cast<T*>(this->lhs_) == this->rhs_; }
};

template<typename T, typename U>
struct NotEqual_NotEqual : public BinaryExpression<T, U> {
  NotEqual_NotEqual(const T& lhs, const U& rhs) : BinaryExpression<T, U>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ != this->rhs_; }
  virtual std::string getOpName() const { return "!="; }
};

template<typename T, typename U>
struct NotEqual : public NotEqual_<T, U> {
  NotEqual(const T& lhs, const U& rhs) : NotEqual_<T, U>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ != this->rhs_; }
};

template<typename T>
struct NotEqual<T*, int> : public NotEqual_<T*, int> {
  NotEqual(T* const& lhs, const int& rhs) : NotEqual_<T*, int>(lhs, rhs) { }
  virtual bool evaluate() const { return this->lhs_ != reinterpret_cast<T*>(this->rhs_); }
};

template<typename T>
struct NotEqual<int, T*> : public NotEqual_<int, T*> {
  NotEqual(const int& lhs, T* const& rhs) : NotEqual_<int, T*>(lhs, rhs) { }
  virtual bool evaluate() const { return reinterpret_cast<T*>(this->lhs_) != this->rhs_; }
};

template<Operator op, typename T>
bool compare(T* lhs, int rhs) {
  return ExprFactory<T*, T*, op>::logAndEvaluate(lhs, reinterpret_cast<T*>(rhs));
}

template<Operator op, typename T>
bool compare(int lhs, T* rhs) {
  return ExprFactory<T*, T*, op>::logAndEvaluate(reinterpret_cast<T*>(lhs), rhs);
}

Altri operatori

Con l'infrastruttura attuale, introdurre il supporto per un altro operatore si riduce a:

  1. aggiungere l'identificativo dell'operatore in Operators:
    enum Operator {
      e_equal,
      e_lessThan,
      // ...
    };
  2. definire l'operator in Term:
    template<typename T>
    class Term {
      // ...
      template<typename U>
      bool operator==(const U& rhs) const {
        return compare<e_equal>(lhs_, rhs);
      }
      template<typename U>
      bool operator<(const U& rhs) const {
        return compare<e_lessThan>(lhs_, rhs);
      }
      // ...
    };
  3. definire l'oggetto ExprFactory relativo:
    template<typename T, typename U>
    struct ExprFactory<T, U, e_lessThan> {
      static bool logAndEvaluate(const T& lhs, const U& rhs) {
      return LessThan<T, U>(lhs, rhs).logAndEvaluate();
      }
    };
    
  4. definire l'oggetto Expression che realizza il confronto vero e proprio:
    template<typename T, typename U>
    struct LessThan : public BinaryExpression<T, U> {
      LessThan(const T& lhs, const U& rhs) : BinaryExpression<T, U>(lhs, rhs) { }
      virtual bool evaluate() const { return this->lhs_ < this->rhs_; }
      virtual std::string getOpName() const { return "<"; }
    };
    

Conclusioni

Con le modifiche apportate, il programma d'esempio riportato all'inizio compila finalmente senza warning.

Codice sorgente

Pagina modificata il 03/10/2012