allgemein
objekt-orientierter entwurf
programmierung
qualitätssicherung / testen

bugTeaser  #002: Die Makrofalle 
Schwierigkeitsgrad  mittel 
Lösung veröffentlicht am   18.02.2005  

Beobachtbare Symptome

Testfälle schlagen zwar wie erwartet fehl, weil EXPECTED und ACTUAL-Werte sich unterscheiden. Die entsprechende Log-Meldung weist aber beide Werte als gleich aus.

Erklärung

Wie Stephen Dewhurst treffend bemerkt, ist das Preprocessing die wohl gefährlichste Phase bei der Übersetzung in C++. Denn der Präprozessor nimmt lediglich textuelle Ersetzungen vor und ist blind für die vielen Subtilitäten von C++.

Das Makro im vorliegenden Beispiel tut nichts besonders Schwieriges und enthält dennoch zwei gravierende Fehler. Das zeigt sehr eindrücklich, wie gefährlich der Umgang mit dem Präprozessor ist. Der zweite Fehler war ursprünglich nicht einmal demjenigen selbst bewusst, der den bugTeaser eingereicht hat.

Der erste Fehler betrifft den Umstand, dass der Präprozessor wie gesagt nur textuelle Ersetzungen vornimmt. Handelt es sich bei EXPECTED und ACTUAL um Funktionsaufrufe, so werden diese bei der Makrosubstitution im Code auch mehrfach eingesetzt und somit immer, wenn sich ACTUAL und EXPECTED im Ergebnis unterscheiden, zweimal ausgeführt.

Scott Meyers behandelt dieses Problem in Lektion 1 von Effektiv C++ Programmieren:

"Eine [...] missbräuchliche Verwendung von #define ist die Implementierung von Makros, die wie Funktionen aussehen, aber nicht die Kosten eines Funktionsaufrufes mit sich herumschleppen. Das beste Beispiel ist die Berechnung des Maximus zweier Werte:

#define max(a,b) ((a) > (b) ? (a) : (b))

Allein dieses kleine Makro hat bereits soviele Nachteile, dass es sich nicht einmal lohnt, darüber nachzudenken. Da sind Sie ja noch besser dran, wenn Sie im Feierabendverkehr im Stau stehen."

Und dann gibt uns Scott Meyers auch gleich noch den Hinweis auf den zweiten Fehler im vorliegenden bugTeaser:

"Wann immer Sie ein derartiges Makro schreiben, dürfen Sie nicht vergessen, alle Argumente im Makrorumpf einzuklammern, sonst geraten Sie ganz schnell in Schwierigkeiten, wenn jemand Ihr Makro mit einem Ausdruck aufruft. Aber selbst wenn Sie diesen Punkt beachten, können immer noch die verrücktesten Dinge passieren:

int a = 5, b = 0;
max(++a, b);     // a wird zweimal inkrementiert
max(++a, b+10);  // a wird nur einmal inkrementiert

Hier haben Sie eine Situation, wo das, was mit a innerhalb von max passiert, davon abhängig ist, womit a verglichen wird!"

(Scott Meyers Effektiv C++ Programmieren , Lektion 1, S.34)

Die Lösung

Mit der folgenden Lösung kann besser verhindert werden, dass die Makroparameter mehr als einmal ausgwertet werden. Ausserdem werden wir so auch das Problem mit den Klammernlos. Und da es sich bei testAssert() um eine Template-Funktion handelt, kann sich diese an alle möglichen Parameter-Typen anpassen:

#define B_ASSERT(EXPECTED,ACTUAL)
testAssert(EXPECTED,ACTUAL,__FILE__,__LINE__)

template
void BSysTestContext::testAssert( 
       const T1& i_raExpectedObj,
       const T2& i_raActualObj,
       const char* i_pcFilePath,
       int i_nLineNumber) throw
(BSysTestErrorMsg*)
{
  incrementAssertionCount();

  if (isEqual(i_raExpectedObj,i_raActualObj))
  {
    return;
  }
  testError(BSysString()
  <<"EXPECTED: "
  <<i_raExpectedObj<<"\nACTUAL "
  <<i_raActualObj,i_pcFilePath,i_nLineNumber);

}


Über diesen bugTeaser diskutieren.

Druckbare Version