04.01.2014
Version: 2
Inhaltsverzeichnis
1. Regel: Verwenden Sie kein "bool" in Schnittstellen
Okay, ich gebe zu - diese Regel ist sehr krass und provozierend - aber das soll sie auch sein. Denn es ist mehr als ein kleines Stück Wahrheit an ihr. Schauen wir uns mal ein kleines Beispiel an - eine einfache Funktion "set_visible", die einen GUI Check-Button sichtbar bzw. unsichtbar schaltet.
class check_button
{
public:
...
void set_visible(bool);
...
};
check_button cb;
...
cb.set_visible(true);
...
cb.set_visible(false);
Schaun wir uns ein zweites Beispiel an. Hier werden zwei Check-Buttons erzeugt, jeweils mit Titel und den Informationen ob der Check-Button "checked" bzw. "unchecked", "grayed" bzw. "non-grayed", "enabled" bzw. "disabled" und "visible" bzw. "invisible" ist.
class check_button
{
public:
explicit check_button(
const std::string& title,
bool checked = false,
bool grayed = false,
bool enable = true,
bool visible = true
);
...
};
check_button cb1("Regeln aktiv", true, false, false, true);
check_button cb2("Direkt auswerten", false, false, true, false);
Bei einem Bool-Parameter mag sich ein Funktions-Aufruf ja noch ganz gut lesen lassen, bei mehreren Bool-Parametern oder Konstruktoren oder Operatoren sind die True's und False's nur noch verwirrend. Und auch bei einem einzelnem Bool-Parameter ist die Bedeutung nicht immer klar, z.B.:
container.insert(object, true);
- Vielleicht soll vor dem Einfügen gecheckt werden, ob das "object" schon im Container ist, und dann nicht eingefügt werden?
- Oder wenn das "object" schon im Container ist, dann soll das alte "object" gegen das neue getauscht werden? Oder vielleicht auch das Gegenteil?
- Oder wenn der Container zu klein für das "object" ist, soll eine Exception geworfen werden statt einer Fehler-Code-Rückgabe? Oder umgekehrt?
- Oder es soll sortiert eingefügt werden, und bei "false" würde das neue "object" einfach hinten angefügt werden?
- Oder, oder, oder,...
Nachdem wir nun gesehen haben, dass Bool-Parameter wirklich nicht die beste Wahl sind, stellt sich die Frage: Was nehmen wir statt dessen?
1.1 Enum- statt Bool-Parameter
Sinnvoll wäre ein Argument, dass selbsterklärend ist, daher seine Bedeutung im Namen trägt - also z.B. Konstanten. Und da es immer nur ein begrenzte Anzahl an möglichen Werten gibt (bei Bool-Parametern genau 2), bieten sich Enums als Argumente an. Wir werden später sehen, dass wir damit noch weitere Vorteile gegenüber Bool-Parametern gewinnen - aber bleiben wir erstmal bei dem Thema "Lesbarkeit". Schreiben wir beide Beispiele mal auf Enums um.
enum enable_state { enabled, disabled };
enum visible_state { visible, invisible };
enum check_state { checked, unchecked };
enum gray_state { grayed, ungrayed };
class check_button
{
public:
explicit check_button(
const std::string& title,
check_state = unchecked,
gray_state = ungrayed,
enable_state = enabled,
visible_state = visible
);
...
void set_visible(visible_state);
...
};
check_button cb1("Regeln aktiv", checked, ungrayed, disabled, visible);
check_button cb2("Direkt auswerten", unchecked, ungrayed, enabled, invisible);
...
cb2.set_visible(visible);
2. Vorteile
Aber Enum- statt Bool-Parameter bieten viele Vorteile:- Lesbarkeit - siehe vorheriges Kapitel
- Typ-Sicherheit
- Änder- und Erweiterbarkeit
Hier wollen wir jetzt erstmal mit den einfachen Vorteilen von Enum- statt Bool-Schnittstellen fortfahren.
2.1 Typsicherheit
Enum- statt Bool-Parameter bieten noch mehr Vorteile. Einer davon ist die Typsicherheit. Da jeder Parameter eindeutig typisiert ist, kann es keine fehlerhaften Zuordnungen geben.Da wollte jemand die Checkbox "enabled" und "invisible" erstellen, hat sich dann aber in der Reihenfolge der Parameter 4 und 5 vertan. Dieser Fehler fällt bei Bool-Parametern erst zur Laufzeit auf - falls gut getestet wird.
check_button cb("Regeln aktiv", true, false, false, true);
^ ^ Parameter falsch rum
check_button cb("Regeln aktiv", checked, ungrayed, invisible, enabled);
^ ^ Compiler-Fehler
2.2 Änder- und Erweiterbarkeit
Ein zusätzlicher Nachteil von Bool-Parametern ist, dass er auf die zwei Werte "true" und "false" eingeschränkt ist. Nun werden Sie vielleicht sagen, dass das kein Nachteil ist, sondern ein absichtliches und gewünschtes Feature eines boolschen Typs. Ja, das stimmt schon - aber dieses Feature kann in zukünftigen Situation zum Handikap werden.Stellen Sie sich vor, dass sich die GUI Klassen-Bibliothek weiterentwickelt, und Check-Buttons nun neben "sichtbar" und "unsichtbar" auch noch "transparent" sein können. Mit den Bool-Parametern stehen wir jetzt auf dem Schlauch, da ein "bool" eben keinen dritten Wert unterstützen kann. Spätestens jetzt müssen wir den Typ wechseln - sinnvollerweise natürlich zu einem Enum.
class check_button
{
public:
explicit check_button(
const std::string& title,
bool checked = false,
bool grayed = false,
bool enable = true,
visible_state = visible // Schnittstellen-Aenderung
);
...
};
Hätten wir statt des Bool-Parameter einen Enum genommen, wären wir jetzt fein raus. Es würde sich ja keine Schnittstelle ändern. Unser Enum hätte nur einen neuen weiteren Enum-Wert - und das wär's.
enum visible_state { visible, invisible, transparent }; // Neuer Wert
class check_button
{
...
void set_visible(visible_state); // Keine Aenderung
...
};
cb.set_visible(visible); // Funktioniert weiterhin
2.3 Parameter fallen weg
Eine andere Situation ist, wenn sich Parameter nicht ändern, sondern Alte wegfallen oder Neue hinzukommen. Auch hier sollte der Compiler daraus resultierende Inkompatibilitäten finden und anmeckern. Bei Bool-Parametern ist dies nicht zwingend der Fall - jedenfalls nicht wenn die Bool-Parameter hinten in der Parameterliste stehen und mit Default-Argumenten abgedeckt sind.Nehmen wir das Beispiel von oben:
class check_button
{
public:
explicit check_button(
const std::string& title,
bool checked = false,
bool grayed = false,
bool enable = true,
bool visible = true
);
...
};
check_button cb("Regeln aktiv", true, false); // Erzeugt Check-Button mit:
// - checked
// - not grayed
// - enabled
// - visible
class check_button
{
public:
explicit check_button(
const std::string& title,
bool checked = false,
// Kein "grayed" Parameter mehr
bool enable = true,
bool visible = true
);
...
};
check_button cb("Regeln aktiv", true, false); // Erzeugt Check-Button mit:
// - checked
// - disabled (vorher enabled)
// - visible
Mit Enum-Parametern wäre dies nicht passiert. Nun hätte der Programmierer eine Fehlermeldung bekommen, da die Argumente nicht mehr zur Parameter-Liste passen.
class check_button
{
public:
explicit check_button(
const std::string& title,
check_state = unchecked,
// Kein "grayed" Parameter mehr
enable_state = enabled,
visible_state = visible
);
};
check_button cb("Regeln aktiv", checked, ungrayed);
^ Compiler-Fehler - "enable_state" Argument erwartet
2.4 Neue Parameter kommen hinzu
Ähnlich ist das Ergebnis, wenn mitten in die Parameter-Liste ein weiterer Bool-Parameter eingefügt wird. Ich denke, da das Szenario sehr ähnlich dem vorherigen ist, reicht hier eine reduzierte Diskussion.Wir wollen in unseren Check-Button Konstruktor einen neuen zweiten Parameter aufnehmen, der angibt, ob das GUI-Element direkt nach der Konstruktion den Focus erhält. Mit Bool-Parametern führt dies natürlich wieder zu Problemen.
class check_button
{
public:
explicit check_button(
const std::string& title,
bool focus = true, // Neuer Parameter
bool checked = false,
bool grayed = false,
bool enable = true,
bool visible = true
);
...
};
check_button cb("Regeln aktiv", true, false); // Erzeugt nun Check-Button mit:
// - focus
// - unchecked (vorher checked)
// - not grayed
// - enabled
// - visible
check_button cb("Regeln aktiv", checked, ungrayed);
^ Compiler-Fehler - "focus_state" Argument erwartet
Aber sind Sie absolut sicher, dass das wirklich nie passiert? Ich nicht! Parameter-Listen sind doch meist semantisch sortiert - z.B. zusammengehörige bzw. ähnliche Parameter stehen zusammen, oder je "unwichtiger" ein Parameter ist, umso weiter hinten steht er. Wenn ein hinten angefügter Parameter solche "ästhetischen" Gesichtspunkte stark verletzt - keine Ahnung, was dann passiert. Mit Enum-Parametern ist man jedenfalls auf der sicheren Seite.
3. Schluss-Bemerkungen
-
Diese Regel gilt nicht nur für Funktions-Parameter, sondern auf für Funktions-Rückgaben
Selbst wenn sich die Beispiele alle auf Funktions-Parameter bezogen - die Regel gilt natürlich für alle Schnittstellen-Anteile, d.h. auch für Funktions-Rückgaben. Funktions-Rückgaben sind zwar etwas unkritischer, da es hier ja in C++ keine Änderungen der Rückgabe-Anzahl gibt. Aber erweiterbar sind boolsche Typen auch hier nicht, und Abfragen lassen sich zum Teil nicht gut lesbar formulieren. -
Status-Abfragen
Wieso sind boolsche Abfrage-Funktionen (sogenannte "Prädikate") nicht gut lesbar?
Und wie fragt man Objekt-Status wie "visible" oder "invisible" (also Enum-Werte) denn am geschicktesten ab?
Diese - und andere Fragen - beantwortet ein anderer Artikel von mir über Status-Abfragen[1]. -
Dies war kein Plädoyer gegen den Typen "bool"
Dieser Artikel ist nur ein Plädoyer gegen Bool-Typ-Vorkommen in Funktions-Schnittstellen, nicht gegen den Typen "bool" an sich. Ganz im Gegenteil habe ich mich sehr gefreut, dass C++ gegenüber C endlich einen Bool-Typ eingeführt hat. Und es gibt viele Stellen, an denen er sehr viel Sinn macht und auch von mir exzessiv genutzt wird. Hier ging es rein um die Vermeidung von Bool-Typen in Funktions-Schnittstellen - mehr nicht. Also lautet die Regel für C++:
Nutze für Funktions-Schnittstellen besser Enum- statt Bool-Parameter! -
Lohnt sich der Aufwand wirklich?
Ich meine - selbst wenn ein Enum in C++ nicht viel Arbeit ist, so ist ein Bool-Parameter immer noch weniger Arbeit.
Wenn Sie nur ein "Hallo Welt" schreiben, das Sie danach wegwerfen - dann lohnt sich der Aufwand sicher nicht. Umgekehrt sollte ein langlebiges Projekt oder eine gar eine Bibliothek les- und wartbar implementiert sein, da es hierbei während der Lebensdauer viele Leser und Nutzer geben wird. Bei welcher Größe und Lebensdauer eines Programm der Break-Even liegt, ist sicher ein Stück subjektives Empfinden.
Lassen Sie sich aber gesagt sein: Ich habe diesen Artikel vor der Veröffentlichung mehreren Bekannten zum Lesen gegeben. Und zwei konnten mir von solchen Problemen aus dem letzten halben Jahr berichten. Einmal musste ein Bool-Parameter, der ganz sicher nicht mehr Zustände haben kann, dann doch auf einen dritten Zustand erweitert werden. Ein anderes Mal wurde wirklich ein Bool-Parameter mitten in andere Bool-Parameter in die Funktions-Signatur eingefügt - mit fatalen Folgen: es compilierte alles nach wenigen Änderungen, aber nichts lief mehr.
Bilden Sie sich also Ihr eigenes Urteil.
4. Links
5. 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:- Alexander Heckner
- Klaus Wittlich
- Ralph Habermann
- Robert Wittek
- Sven Johannsen
6. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 20.10.2013
- Initiale Version
-
Version 2
- 04.01.2014
- Link auf den Artikel über "Status-Abfragen in C++" ergänzt