Wilkening-Online Logo

Status Abfragen in C++



Von Detlef Wilkening
04.01.2014
Version: 1

1. Einleitung

Im Artikel "Verwenden Sie keine Bool-Typen in Schnittstellen"[1] haben wir gelernt in Schnittstellen besser Enums als Bool-Typen einzusetzen. Neben der besseren Lesbarkeit sind Enums typsicherer und änderungsfreundlicher als Bool-Typen.

Während sich Bool-Status aber einfach mit tpyischen "set_state(state)" und "is_state()" Funktionen setzen und abfragen lassen, wird dies bei Enums schnell unschön. Dieser Artikel soll sich dieser Frage widmen: Wie fragt man einen Status sinnvoll ab? Wie sollte eine Schnittstelle für Enum-Werte aussehen?

Hinweis - wenn Sie den anderen Artikel nicht kennen, oder seinen Inhalt vergessen haben - lesen Sie ihn vorher noch einmal. Ansonsten ist dieser Artikel manchmal schwer nachzuvollziehen, da er sich immer wieder auf den Argumentationen und Beispielen des anderen Artikels abstützt.

2. Abfrage-Funktionen

Im vorherigen Artikel haben wir Abfrage-Funktionen auf Objekt-Zustände komplett ignoriert. Bei Bool-Parametern ist dies ja auch kein Problem, wie das folgende Beispiel zur Abfrage der Sichtbarkeit eines Check-Buttons zeigt.

class check_button
{
public:
   ...
   bool is_visible() const;
   ...
};


check_button cb;
...
if (cb.is_visible())      // Wenn der Check-Button sichtbar ist, dann...
   ...
...
if (!cb.is_visible())     // Wenn der Check-Button unsichtbar ist, dann...
   ...

Wie machen wir die Abfrage nun mit Enum-Parametern? Hier haben wir drei Möglichkeiten: Schauen wir uns mal alle drei Möglichkeiten an.

3 Rückgabe des Enum-Werts

Das ist sicher die offensichtlichste Lösung, die man auch in der Praxis an vielen Stellen wiederfindet. Die Abfrage-Funktion gibt den Enum-Wert zurück, und man muss ihn selber gegen die Enum-Werte vergleichen.

enum visible_state { visible, invisible };

class check_button
{
public:
   ...
   visible_state get_visible() const;
   ...
};


check_button cb;
...
if (cb.get_visible()==visible)       // Wenn der Check-Button sichtbar ist, dann...
   ...
...
if (cb.get_visible()==invisible)     // Wenn der Check-Button unsichtbar ist, dann...
   ...

Schlecht ist diese Lösung sicher nicht, denn sie hat gegenüber einer Bool-Funktion eine Menge Vorteile: Nachteile dieser Lösung: Stellen wir uns einen Enum mit vier Werten vor, z.B. für die Ausrichtung ("Alignment") eines Text-Blocks mit "links-bündig", "rechts-bündig", "zentriert" und "Blocksatz". Und eine dazu passende Abfrage-Funktion "get_alignment" in einer Klasse "paragraph".

enum alignment { left, right, center, block };

class paragraph
{
public:
   ...
   alignment get_alignment() const;
   ...
};

Jetzt wollen wir etwas machen, wenn die Ausrichtung nicht zentriert oder Blocksatz ist. So sieht der Code dann aus.

paragraph para;
...
if (para.get_alignment()!=center && para.get_alignment()!=block)
   ...

Der ein oder andere Programmierer würde den Code mit einer lokalen Variable implementieren - aber viel anders ist auch das dann nicht. Da ist es wohl mehr Geschmacks-Sache, welcher Code einem besser gefällt - bzw. ein anderer Artikel der die Vor- und Nachteile der beiden Lösungen vergleicht.

paragraph para;
...
alignment a = para.get_alignment();
if (a!=center && a!= block)
   ...

Mit einem Bool-Parameter müssen wir diese Lösung eigentlich gar nicht vergleichen, denn dies hier kann mit einem Bool-Parameter gar nicht ausgedrückt werden. Trotzdem ist es ein Teil der Diskussion, denn die Schnittstellen einer Klassen-Bibliothek sollten homogen aufgebaut sein, daher möglichst ähnlich. Wenn ich also Bool-Parameter durch Enum-Parameter ersetze, und dann Abfrage-Funktionen zur Verfügung stelle die den aktuellen Enum-Wert zurückgeben, dann sollte ich diesen Stil auch für Enums mit mehr als einem Wert durchhalten. Und für diese Anforderung ist dieser Stil eben nicht optimal.

4 Spezielle Abfrage-Funktionen

Ab und zu findet man auch einen anderen Stil für Abfrage-Funktionen, bei dem für jeden Zustand spezielle Abfrage-Funktionen zur Verfügung gestellt werden. Schauen wir uns das am Beispiel des sichtbaren bzw. unsichtbaren Check-Buttons mal an.

class check_button
{
public:
   ...
   bool is_visible() const;
   ...
};

Damit ist der nutzende Code natürlich schön einfach und gut lesbar - ich persönlich finde nur den Not-Operator "!" nicht so optimal. Aber es geht ja gleich noch weiter.

check_button cb;
...
if (cb.is_visible())             // Wenn der Check-Button sichtbar ist, dann...
   ...
...
if (!cb.is_visible())            // Wenn der Check-Button unsichtbar ist, dann...
   ...

Spätestens bei mehr als zwei möglichen Zuständen - daher wenn wir eh mit einem Bool-Parameter nicht weiterkommen - müssen wir für jeden Zustand eine Abfrage-Funktion einbauen. Ich persönlich bevorzuge diese Option auch schon für zwei Zuständen, da man dann den Not-Operator "!" los ist.

class check_button
{
public:
   ...
   bool is_visible() const;
   bool is_invisible() const;
   bool is_transparent() const;
   ...
};


check_button cb;
...

if (cb.is_visible())
   ...

if (cb.is_invisible())           // Das ist doch besser lesbar als !is_visible(), oder?
   ...

if (cb.is_transparent())
   ...

Oder auch.

paragraph para;
...
if (!para.is_center() && !para.is_block())
   ...

Bei unserem Test-Ausrichtungs-Beispiel kommt leider wieder der Not-Operator "!" ins Spiel. Denken Sie daran, die Abfrage "para.is_left() && para.is_right()" enthält zwar keinen Not-Operator "!" und verhält sich im Augenblick auch gleich - ist aber nicht identisch. Sie würde ein anderes Ergebnis liefern wenn es irgendwann mal eine weitere Text-Ausrichtung geben würde.

Vorteile Nachteile Man könnte natürlich für die Teilmengen wiederum spezielle Abfrage-Funktionen anbieten wie z.B. "is_not_center_or_block()" - aber das lohnt sich nur für sehr häufige Abfragen auf einzelne spezielle Kombinationen. Dann halte ich das durchaus für sinnvoll, aber nicht als eine allgemeine Lösung. Das erzeugt dank der kombinatorischen Explosion viel zu viele Funktionen.

4.1 Parametrisierbare Abfragen

Das letzte Argument mit "parametrisierbaren Abfragen" ist neu. Was ist damit gemeint? Nicht immer ist der Quelltext so einfach, dass man auf z.B. Sichtbarkeit abfragt und dann eben das oder das macht. Manchmal kommt der erwartet Status von außen, und nur bei Gleichheit oder Ungleichheit muss etwas gemacht werden. Schauen wir uns folgende Funktion an, die auf dem Enum-Rückgabe-Stil beruht, und bei Ungleichheit eine Warnung ausgibt.

void check_visible_state(const check_button& cb, visible_state vs)
{
   if (vs != cb.get_visible())
   {
      cout << "Check-Button Visible-Status ist nicht okay." << endl;
   }
}

Lassen Sie uns das gleiche Verhalten mal mit speziellen Abfrage-Funktionen implementieren.

void check_visible_state(const check_button& cb, visible_state vs)
{
   bool equal;
   switch (vs)
   {
   case visible:
      equal = cb.is_visible();
      break;
   case invisible:
      equal = cb.is_invisible();
      break;
   case transparent:
      equal = cb.is_transparent();
      break;
   }
   if (!equal)
   {
      cout << "Check-Button Visible-Status ist nicht okay." << endl;
   }
}

Übersichtlich ist dieser Quelltext nicht - ganz im Gegenteil. Hinzu kommt noch, dass er ein Wartungs-Alptraum ist. Denn ein neuer Enum-Wert würde den Quelltext ohne Compiler-Fehler brechen, und zur Laufzeit zu falschem Verhalten führen. Das mindeste was hier noch hingehört ist eine Assertion, die aber leider halbwegs vollständige Tests voraussetzt...

void check_visible_state(const check_button& cb, visible_state vs)
{
   bool equal;
   switch (vs)
   {
   case visible:
      equal = cb.is_visible();
      break;
   case invisible:
      equal = cb.is_invisible();
      break;
   case transparent:
      equal = cb.is_transparent();
      break;
   default:
      assert(false);
   }
   if (!equal)
   {
      cout << "Check-Button Visible-Status ist nicht okay." << endl;
   }
}

Außerdem erzeugt dieser Quelltext schnell noch eine zweite Diskussion, auf die ich hier nicht detailliert aber wenigstens kurz eingehen will. Die lokale Variable "equal" wird nicht initialisiert und daher ist ihre Vorbelegung undefiniert. Solange ein Case-Zweig zuschlägt ist das egal, aber was ist im Falle eines neuen Enum-Werts? Im Debug-Modus wird eine Assertion ausgelöst, aber im Release-Modus haben wir echtes undefiniertes Verhalten. Das ist ganz schlecht, denn es macht z.B. eine Fehler-Reproduzierbarkeit sehr schwierig.

Hinzu kommt noch, dass manche Compiler den Ausführungs-Pfad ohne Initialisierung von "equal" erkennen und eine Warnung ausgeben. Wirklich gute Compiler - gibt es die? - würden hier sogar nur dann warnen, wenn es den Default-Fall wirklich gibt, daher ein Enum-Wert vorhanden ist, der durch keinen Case-Zweig abgedeckt wird. (Anmerkung: ich werde das vielleicht mal mit verschiedenen Compilern testen und den Artikel dann updaten). Das könnte man dann als Wartungs-Hinweis nutzen - man verläßt sich dann aber sehr auf spezielles Compiler-Verhalten, und wehe man wechselt den Compiler, man ändert die Warnungs-Einstellungen, jemand ignoriert die Warnungen, oder oder oder.

Sinnvoll ist also wohl eine Initialisierung von "equal" in jedem Fall - auch wenn dann der mögliche Vorteil der Compiler-Warnungen verloren geht. Eine Vorinitialisierung direkt bei der Definition von "equal" wird dem echten C-Programmierer aber nicht gefallen, denn das hieße in den Normal-Fällen ja eine überflüssige Initialisierung. Also muss "equal" auch im Default-Zweig gesetzt werden. Bleibt nur noch die Frage, was denn ein sinnvoller Default-Wert ist? Dies muss man wohl Kontext-Abhängig entscheiden - aber ein ungutes Gefühl bleibt.

void check_visible_state(const check_button& cb, visible_state vs)
{
   bool equal;
   switch (vs)
   {
   case visible:
      equal = cb.is_visible();
      break;
   case invisible:
      equal = cb.is_invisible();
      break;
   case transparent:
      equal = cb.is_transparent();
      break;
   default:
      assert(false);
      equal = false;     // Fuer ein definiertes Fehler-Verhalten im Release-Modus
   }
   if (!equal)
   {
      cout << "Check-Button Visible-Status ist nicht okay." << endl;
   }
}

Hoffen wir nur, dass jetzt kein Compiler den toten Code im Debug-Modus im Default-Zweig anwarnt, und auch kein Compiler aufgrund mangelhafter Ausführungspfad-Analyse immer noch eine uninitialisierte Variable meldet. Dann müßte man nur für die Warnungs-Freiheit einen unoptimalen Code schreiben, und das müßte man kommentieren, damit einem später kein Review hier schlechten Code vorhält, und und und... Eine Büchse der Pandorra.

Wir sehen, dass obwohl spezielle Abfrage-Funktionen einige Vorteile gegenüber der Rückgabe des Enum-Werts haben, sie auch ihre Nachteile haben. Eine richtig gute Lösung scheint es nicht zu geben. Vielleicht sollte man beide Stile parallel anbieten, und dann je nach Situation mal die einen Funktionen und mal die anderen nutzen. Aber dann hätten wir ja wieder den Nachteil von viel zu großen Schnittstellen und viel zu viel Wartungs-Aufwand bei Änderungen. Was machen wir bloß?

5 Zwischenfazit & Kriterien-Katalog

Bevor wir uns eine weitere Lösungs-Möglichkeit anschauen, wollen wir nochmal alle Kriterien zusammenfassen, die wir im Laufe des alten und des bisherigen Artikels genutzt haben, um die möglichen Implementierungen zu bewerten:
  1. Lesbarer Quelltext bei allen Arten von Funktionen, d.h. z.B. Settern und Konstruktoren.
  2. Lesbarer Quelltext bei Abfrage-Funktionen, sowohl wenn ein einzelner Wert, eine Negation, eine Kombination oder eine negierte Kombination abgefragt wird.
  3. Hart bzgl. Schnittstellen-Änderungen, d.h. am Besten funktioniert alles weiter, aber wenn nicht so muss dies durch einen Compiler-Fehler gemeldet werden.
  4. Unproblematisch wenn neue Enum-Werte hinzukommen bzw. welche wegfallen - d.h. am Besten funktioniert alles weiter, aber wenn nicht so muss dies durch einen Compiler-Fehler gemeldet werden.
  5. Kleine Schnittstellen, d.h. wenige Funktionen.
  6. Parametrisierbare Abfragen sollen einfach möglich sein.

6 Angabe des abzufragenden Werts

Die beiden bisherigen Lösungen sind zwar sehr verbreitet, kranken letztlich aber an der nicht konsequenten Umsetzung der objektorientierten Ideen der "Kapselung" und "Lass-das-Objekt-für-dich-arbeiten". Stroustrup und viele andere haben immer wieder die Ansicht vertreten, dass Getter-Funktionen schlecht sind und kein OO-Design darstellen - und bislang sind unsere Abfrage-Funktionen nichts anderes als einfachste Getter-Funktionen.

Wenn wir die eigentliche Arbeit in die Abfrage-Funktionen integrieren würden (daher in die betroffenen Objekte), dann müßte der nutzende Quelltext doch einfach und wartungsfreundlich werden, oder? Probieren wir es mal aus.

enum alignment { left, right, center, block };

class paragraph
{
public:
   paragraph(alignment);                          // (*)
   ...
   bool is_alignment(alignment) const;            // (**)
   void set_alignment(alignment);                 // (***)
   ...
   void print(output_device&, alignment) const;   // (****)
};


paragraph pa(left);                    // (*)

if (pa.is_alignment(left))             // (**)
   ...

pa.set_alignment(right);               // (***)

pa.print(printer, block);              // (****)

6.1 Kriterium "Lesbarer Quelltext"

Checken wir am Beispiel, ob normale Funktionen gut lesbar sind:

6.2 Kriterium "Abfragen jeglicher Art"

Für diese Thema müssen wir etwas weiter gehen. Hierzu sollte Ihnen bekannt sein, dass der von C++ erlaubte Enum Werte-Bereich nicht nur die angegebenen Enum-Werte umfaßt, sondern auch ihre Kombinationen - näheres hierzu finden Sie z.B. in meinem Artikel über Enums[2].

Damit sollte klar sein - spätestens nach dem Lesen meines Enums-Artikels - das wir hier die Enum-Werte anders definieren müssen und auch den Oder-Operator "|" für die Enums überladen sollten - das Operator-Überladung für Enums funktioniert, und wie man das macht, das wird in meinem Artikel über Operator-Überladung[3] beschrieben. Damit können wir ganz einfach abfragen können, ob z.B. ein Kapitel zentriert oder mit Blocksatz formatiert ist.

enum alignment { left=1, right=2, center=4, block=8 };

inline alignment operator|(alignment lhs, alignment rhs)
{
   return static_cast<alignment>(lhs | rhs);
}

class paragraph
{
public:
   ...
   bool is_alignment(alignment) const;
   ...
};


paragraph pa;

if (pa.is_alignment(block | center))
{
   ...
}

Das sieht doch gut aus, was? Aber wie ist es mit negierten Abfragen, d.h. wenn wir z.B. wissen wollen, ob das Kapitel weder zentriert noch als Blocksatz formatiert ist? Dazu sollten wir noch den Not-Operator "!" und den Und-Operator "&" überladen - und schon ist auch das kein Problem mehr.

inline alignment operator!(alignment a)
{
   // Hier sollte besser eine Konstante stehen
   return static_cast<alignment>(15-a);
}

inline alignment operator&(alignment lhs, alignment rhs)
{
   return static_cast<alignment>(lhs & rhs);
}


paragraph pa;

if (pa.is_alignment(!block & !center))            // Nun ist die Abfrage so
{
   ...
}

if (pa.is_alignment(!(block | center)))           // oder so moeglich
{
   ...
}

Der ein oder andere mag jetzt einwenden, dass wir hiermit unsere Enum-Schnittstellen etwas aufweichen, da nun folgendes möglich wäre:

paragraph pa(left | right);         // Was soll das denn meinen?

Aber auch hiergegen kann man sich mit dem Compiler sichern. Man muss nur zwei Enum-Typen einführen - einer für einfache Werte, und einer für verknüpfte Werte, z.B. so:

enum alignment { left=1, right=2, center=4, block=8 };
enum alignment_union { alignment_values=15 };

Und dann muss man die Operatoren und Funktionen nur noch so typisieren, wie man es möchte - und im schlimmsten Fall muss man einige Funktionen doppelt implementieren.

6.3 Kriterium "Hart bzgl. Schnittstellen-Änderungen"

Wie wir oben schon gelernt haben, sind alle Schnittstellen durch die Verwendung von Enums hart gegen Änderungen - also z.B. wenn sich die Reihenfolge der Parameter ändert oder mitten in der Parameterliste Parameter wegfallen oder hinzukommen. Da Enums echte C++ Typen darstellen, die nicht implizit ineinander konvertiert werden, führt jede Schnittstellen Änderung zu einem Compiler-Fehler.

6.4 Kriterium "Änderung der Enum-Werte"

Entfernen, Hinzufügen oder Ändern von Enum-Werten ist überhaupt kein Problem - solange ich die Implementierungen der Operatoren mit anpasse. Verwendung nun nicht mehr vorhandener Enum-Konstanten führt zu Compiler-Fehlern. Und die Verwendung aller noch vorhandenen Enum-Konstanten führt zu keiner Änderung der Semantik - auch nicht bei z.B. negierten Gruppen-Abfragen.

6.5 Kriterium "Wenige Funktionen"

Da es nicht für jeden Enum-Wert eine Funktion gibt, sondern nur einen Setter und Getter für den Enum-Typ, ist die Schnittstelle schön klein und wartungsfreundlich. Selbst bei der Verwendung eines extra Union-Enum-Typs wächst die Schnittstelle nur um eine Funktion - und bedenken Sie bitte: diese Funktion bietet eine Funktionalität an, die die anderen Lösungen gar nicht unterstützt haben.

6.6 Kriterium "Parametrisierbare Abfragen"

Bleiben zu guter Letzt die parametriesierbaren Abfragen, und auch hier geht die Geschichte natürlich gut aus. Parametrisierbare Anfragen sind ganz primitiv und wartungsfreundlich möglich.

void check_visible_state(const check_button& cb, visible_state vs)
{
   if (cb.is_visible(!vs))
   {
      cout << "Check-Button Visible-Status ist nicht okay." << endl;
   }
}

7. Bewertung der Lösungs-Möglichkeiten

Bevor wir zu einem letzten Schmankerl kommen, lassen Sie uns die vorgestellten Lösungen noch mal bewerten.

Kriterium Bool + Setter/Getter Enums + Setter/Getter Enums + Abfrage-Fkt. Enums + Operatoren + Angabe-Fkt.
Konstruktor Deklaration lesbar - + + +
Konstruktor Aufruf lesbar - + + +
Setter Deklaration lesbar + + + +
Setter Aufruf lesbar + + + +
Getter Deklaration lesbar + + + +
Getter Aufruf lesbar + + + +
Funktions Deklaration lesbar - + + +
Funktions Aufruf lesbar - + + +
Negierte Abfragen + o o +
Gruppen Abfragen o o - +
Schnittstellen problemlos änderbar - + + +
Enum-Werte problemlos änderbar - + + +
Minimales Funktions-Set o + - +
Parametrisierbare Abfragen + - - +

Klar sollte sein, dass ein Bool-Parameter, trotz seiner Einfachheit, hier wohl die schlechteste Wahl ist. Schon ein einfacher Enum bringt eine Menge an Lesbarkeit und Sicherheit. Wird dann noch geschickt mit den Enum-Werten und Operatoren gearbeitet, kann man wirklich schönen Quelltext bekommen.

8. Schmankerl

So richtig perfekt ist der Quelltext aber immer noch nicht, denn im Prinzip enthält er doppelten Code. Zur Zeit tragen die Funktions-Namen und die Enum-Konstanten nämlich überlappene Informations-Anteile - im folgenden Beispiel die Visible-Information.

button b;
b.set_visible(visible);
if (b.is_visible(visible)) ...

Das zu ändern ist in C++ gar kein Problem. Da Enums eigenständige Typen sind, kann man sie zum Überladen nutzen. Viel schöner ist also folgendes:

class button
{
public:
   ...
   void set(visible_state);
   bool is(visible_state);
   ...
};


button b;
b.set(visible);
if (b.is(visible)) ...

Wenn man diesen Weg einschlägt sollte man aber seine Enum-Konstanten Namen mit einer gewissen Sorgfalt wählen. Z.B. einfach "left" für eine Ausrichtungs-Konstante fände ich jetzt nicht mehr okay - besser wäre da z.B.:

enum alignment 
{ 
   left_alignment   = 1,
   right_alignment  = 2,
   center_alignment = 4,
   block alignment  = 8
};

...

if(para.is(block_alignment)) ...

Der ein oder andere mag diesen Quelltext immer noch nicht optimal finden, denn korrekterweise müßte es nicht "button.set(visible)" heißen, sondern "button.make(visible)" - und nicht "paragraph.is(block_alignment)", sondern "paragraph.has(block_alignment)".

Wer es jetzt ganz genau nimmt, könnte sogar auf die Idee kommen, dass "make(button, visible) bzw. "has(paragraph, block_alignment)" noch besser sein könnten.

Egal, was einem besser gefällt - all das ist in C++ doch gar kein Problem...

8.1 Gefälligere Element-Funktionen

Für die erste Idee implementieren Sie einfach zwei Member-Templates "make" und "has" die inline auf "set" und "is" abgebildet werden.

class button
{
public:
   ...
   template<class T> inline void make(T t) { set(t); }
   template<class T> inline bool has(T t) { is(t); }
   ...
};


button b;
b.make(visible);
if (b.has(focus)) ...

Falls Sie diese Idee so gut finden, dass Sie das als normales Feature all Ihrer betroffenen Klassen haben wollen - dann sollten Sie dies einmal wiederverwendbar implementieren. Hierzu gibt es je nach gewünschtem Effekt mehrere Möglichkeiten - z.B. die Funktionen in eine Basis-Klasse legen, das Überladen von Funktions-Templates, oder die Nutzung des Barton-Nackman Tricks[4]. Welche Lösung für Sie und Ihre Wünsche die Beste ist, ist nicht Teil dieses Artikels.

8.2 Freie Funktionen

Item 44 des empfehlenswerten Buchs "C++ Coding Standards"[5] von Herb Sutter und Andrei Alexandresu beschreibt, dass freie Funktionen (d.h. Non-Member-Functions) den Element-Funktionen vorzuziehen sind, um die Schnittstelle von Klassen nicht unnötig zu überfluten. In diesem Sinne wären vielleicht auch hier die freien Funktionen vorzuziehen - ganz unabhängig von der vielleicht noch schöneren Lesbarkeit. Die Umsetzung ist auch hier wieder ganz einfach - einfach fünf Template-Funktionen implementieren.

template<class T, class E> inline void set(E& e, T t)
{
   e.set(t);
}

template<class T, class E> inline void make(E& e, T t)
{
   e.set(t);
}

template<class T, class E> inline bool is(const E& e, T t)
{
   return e.is(t);
}

template<class T, class E> inline bool has(const E& e, T t)
{
   return e.is(t);
}


button b;
make(b, visible);
if (is(b, visible)) ...

paragraph para;
make(para, left_alignment);
if (has(para, left_alignment | right_alignment)) ...

8.3 Man kann's auch übertreiben

Dem ein oder anderen mag das immer noch nicht lesbar genug sein, denn die Klammern und Kommas stören ja wirklich... Man könnte natürlich noch mehr Operator-Überladung und Templates nutzen, um folgende Dinge zu ermöglichen - aber mir geht das ehrlich gesagt zu weit.

make-button-visible;            // Operator - ueberladen fuer "make", "button" und "visible"
make-button-visible+enabled;    // Und noch zusaetzlich Operator + ueberladen

Auch das Überladen der Vorzeichen-Operatoren "+" und "-", um das An- bzw. Ausschalten zu steuern, stiftet in meinen Augen mehr Verwirrung als das es hilft.

make(button, +visible);
make(button, -visible);                   // statt make(button, invisible)

make(button, visible+enabled-checked);

Ne - das ist mir klar zu viel. Das ist für mich nicht mehr les- und wartbar, sondern nur noch ausgefallen. Und ausgefallener Code ist selten guter Code.

Man kann viel machen in C++ - man muss es aber nicht, und manches sollte man nicht.

9. Fazit

Nachdem wir gelernt hatten, keine Bool-Typen in Schnittstellen einzusetzen, haben wir heute gesehen, wie man sinnvoll den Status von Objekten abfragt. Einfache Bool-Funktionen kommen natürlich gar nicht in Frage - das wären ja ein Bool-Typ in der Schnittstelle. Aber auch einfche Enum-Funktionen sind erstmal nicht viel besser. Spezielle Abfrage Funktionen können den Quelltext lesbarer machen, blähen die Klassen aber extrem auf. Die Lösung sind die Nutzung von Enums mit Operator-Überladung und einfachen Funktionen - typischerweise dann als freie Funktionen ausgelegt. Damit erreicht man lesbaren, typsicheren, änderungs-freundlichen und kurzen Quelltext. Was will man mehr?

10. Links & Literatur

Hier die Links zu den speziellen Themen aus diesem Artikel:
  1. Mein Artikel zum Thema "Verwenden Sie keine Bool-Typen in Schnittstellen":

  2. Mein Artikel über Enums in C++:
    • Noch nicht veröffentlicht

  3. Mein Artikel zum Thema "C++ Operator-Überladung von A bis (fast) Z":

  4. Barton-Nackman Trick

  5. Buch:
    • C++ Coding Standards
    • Von Herb Sutter & Andrei Alexandresu
    • 101 Rules, Guidelines, and Best Practices
    • Sprache: English
    • Verlag: Pearson
    • ISBN: 978-8131706138

11. Danksagung

Bedanken möchte ich mich bei folgenden Personen (in alphabetischer Reihenfolge), die diesen Artikel gegengelesen und mit vielen kleinen und großen Hinweisen dafür gesorgt haben, dass er besser ist als am Anfang:

12. Versions-Historie

Die Versions-Historie dieses Artikels:
Schlagwörter: