C++11 und die alternative Syntax für Funktionen
Ich hatte mehrere Jahre mit Turbo Pascal programmiert, bevor ich mich – zugegebenermaßen relativ spät – mit Programmiersprachen der C‑Familie ernsthaft auseinandergesetzt habe. Und von Pascal war ich es gewohnt, den Rückgabetyp hinter dem Funktionsnamen und den formalen Parametern anzugeben.
Fast 40 Jahre nach der Entstehung von C führt C++ nun eine alternative Syntax für die Funktionsdeklaration ein, bei der der Rückgabetyp von Vorne nach Hinten wandert. Die bisherige Syntax wird selbstverständlich weiter unterstützt. Aber ich bin davon überzeugt, dass viele der Sprachentwickler die neue Reihenfolge als einzig Richtige betrachten – wäre da nicht das Problem mit der Abwärtskompatibilität.
Ich kenne die Gründe für die neue Syntax nicht im Einzelnen. Aber viele Vorteile sind offensichtlich und werde ich im folgenden aufzeigen. Auf der Kehrseite kann ich lediglich die etwas gewöhnungsbedürftige Syntax benennen, die leider aufgrund der komplexen Sprachgrammatik notwendig geworden ist.
Syntax
Die neu eingeführte Syntax ist relativ einfach. Der Rückgabetyp wandert hinter die Liste der formalen Parameter und wird durch einen Pfeil abgetrennt. Zusätzlich muss vor den Funktionsnamen das Schlüsselwort auto
gestellt werden. Der folgende Ausschnitt zeigt die alte und neue Syntax im direkten Vergleich.
// old syntax
double pow(double base, int exponent);
// new syntax
auto pow(double base, int exponent) -> double;
Das Schlüsselwort auto
erscheint auf den ersten Blick an dieser Stelle ein wenig merkwürdig. Es ist notwendig um Mehrdeutigkeiten in der Grammatik auszuschließen. Die Syntax wäre vermutlich eingängiger, wenn man sich stattdessen für ein neues Schlüsselwort wie beispielsweise function
entschieden hätte. Allerdings waren die Sprachentwickler sehr darauf bedacht, die neue Sprachversion abwärtskompatibel zu gestalten. Und das bedeutet eben auch, dass keine neuen Schlüsselwörter eingeführt werden. Ich vermute, dass die Wahl schließlich auf auto
fiel, weil jedes andere existierende und mögliche Schlüsselwort noch verwirrender an dieser Stelle gewesen wäre.
Die neue Syntax beschränkt sich selbstverständlich nicht auf die Funktionsdeklarationen. Sie kann an allen Stellen verwendet werden, an denen Funktionstypen angegeben werden. Der folgende Ausschnitt enthält dazu einige Beispiele.
// example for a typedef
typedef auto predicate_t(int) -> bool;
// function accepting a pointer to a predicate function
void set_predicate(auto (*predicate)(int) -> bool);
// function returning a pointer to a predicate function
auto get_predicate() -> auto (*)(int) -> bool;
Übersichtlichkeit
Einen großen Vorteil der neuen Syntax sehe in in der verbesserten Lesbarkeit. Wenn mehrere Funktionsdeklarationen unmittelbar beieinander stehen, dann sind mit der neuen Syntax die Funktionsnamen für mich wesentlich einfacher zu erfassen. Und es sind meistens gerade die Funktionsnamen, die mich beim Lesen interessieren – und seltener die Rückgabetypen. Das folgende, einfache Beispiel zeigt meiner Meinung nach diesen Unterschied bereits sehr deutlich.
class person
{
// ...
public:
// old syntax
std::string get_first_name() const;
std::string get_last_name() const;
date get_birthday() const;
std::vector<std::string> get_nick_names() const;
// new syntax
auto get_first_name() const -> std::string;
auto get_last_name() const -> std::string;
auto get_birthday() const -> date;
auto get_nick_names() const -> std::vector<std::string>;
};
Dabei sind in diesem Beispiel die Rückgabetypen noch erstaunlich einfach. Noch deutlicher wird der Unterschied, wenn längere Bezeichner und generische Typen zum Einsatz kommen.
Das Problem der Lesbarkeit beschränkt sich übrigens nicht nur auf C++. Ich gehe allerdings nicht davon aus, dass Java und C# diesem Weg folgen werden. Denn diesen Sprachen liegt eine teilweise andere Philosophie zu Grunde. Anstatt die Sprache zu ändern werden dort Verbesserungen der Lesbarkeit eher den Entwicklungsumgebungen und -werkzeugen überlassen.
Redundanz und Schreibarbeit
Neben der besseren Übersichtlichkeit sehe ich den zweiten großen Vorteil der neuen Syntax in der reduzierten Redundanz und Schreibarbeit, wenn Funktionen getrennt von ihrer Deklaration definiert werden. Am einfachsten in das wiederum an einem Beispiel zu zeigen. Der folgende Ausschnitt zeigt eine einfache Klassendefinition mit zwei Funktionsdeklarationen. Die beiden Funktionen sind zueinander symmetrisch und verwenden einen innerhalb der Klasse deklarierten Typ einmal als Parameter- und einmal als Rückgabetyp.
namespace ex
{
class person
{
// ...
public:
enum class gender { male, female, other, };
void set_gender(gender value);
gender get_gender() const;
private:
gender _gender;
};
}
Werden die beiden Funktionen nun außerhalb ihrer Klasse definiert, so entsteht eine zwar gewohnte, aber dennoch kuriose Asymmetrie. Beim Parametertyp kann wie bei der Deklaration die Qualifizierung entfallen. Der Rückgabetyp muss hingegen voll qualifiziert werden, da an dieser Stelle im Code der Scope noch nicht bekannt ist.
void ex::person::set_gender(gender value)
{
_gender = value;
}
ex::person::gender ex::person::get_gender() const
{
return _gender;
}
Mit der alternativen Syntax für Rückgabetypen löst sich diese Asymmetrie elegant. Da bei der alternativen Syntax der Rückgabetyp hinter dem Funktionsnamen steht, ist der Scope der Funktion bekannt. Daher kann analog zu den Parametertypen die Qualifizierung – sowohl des Namensraums als auch der Klasse – entfallen.
auto ex::person::get_gender() const -> gender
{
return _gender;
}
Das spart bei langen Bezeichnern viel Schreibarbeit. Auch erleichtert es die (manuelle) Erzeugung der Definitionen aus den Deklarationen, da die Qualifizierung der Rückgabetypen nicht mehr angepasst werden muss.
Inferenz des Rückgabetyps
Für die neue Syntax habe ich schon mehrfach eine weitere Begründung gelesen, die ich aber hauptsächlich der Vollständigkeit wegen beschreibe. Denn ich kann mich nicht daran erinnern, dass ich diese Anwendungsmöglichkeit in der Vergangenheit vermisst habe. Am einfachsten lässt sie sich vermutlich am folgenden Beispiel motivieren.
ReturnType add(long lhs, unsigned rhs)
{
return lhs + rhs;
}
Die Funktion add
in diesem Beispiel soll sich genau so verhalten wie der interne Operator für die Addition. Und die Frage dabei ist, welchen Rückgabetyp die Funktion dafür haben muss.
Diese Aufgabe ist schwieriger als es zunächst aussieht. Denn dieser Typ lässt sich nicht direkt angeben, da er von den Größen von int
und long
abhängt, die selbst wiederum von der Implementierung und der Plattform abhängen. Mir fallen mehrere Lösungsmöglichkeiten für dieses Problem ein. Die einfachste Variante verwendet decltype
, bei dem es sich ebenfalls um eine Neuerung von C++11 handelt.
decltype(long{} + unsigned{}) add(long lhs, unsigned rhs)
{
return lhs + rhs;
}
Damit ist die Implementierung zwar korrekt, aber leider nicht schön anzusehen. Die Lesbarkeit verbessert sich deutlich, wenn der Rückgabetyp hinter der Parameterliste steht. Der große Vorteil dabei ist, dass die Funktionsparameter bei der Definition des Rückgabetype verwendet werden können. Das hört sich merkwürdig an, wird anhand des Beispiels aber klarer.
auto add(long lhs, unsigned rhs) -> decltype(lhs + rhs)
{
return lhs + rhs;
}
Das vorherige Beispiel ist sehr speziell und für die Praxis weitgehend irrelevant. Bei Template-Funktionen und der Meta-Programmierung hat dieser Mechanismus aber durchaus praktische Relevanz. Mir fällt es schwer dafür ein kleines und einleuchtendes Beispiel zu geben. Aber vielleicht kann die folgende Funktionsdeklaration ein Gefühl dafür vermitteln, dass es interessante Anwendungsfälle dafür gibt.
template <typename Callable, typename ...Args>
auto invoke_special(Callable&& callable, Args&&... args)
-> decltype(std::forward<Callable>(callable)(std::forward<Args>(args)...));
Fazit
In meinem Code verursacht diese Spracherweiterung den größten sichtbaren Unterschied zu altem Code. Das liegt einerseits daran, dass mich die Vorteile direkt überzeugt haben, und andererseits natürlich daran, dass die neue Syntax an sehr vielen Stellen Verwendung findet. Und ich kann jedem interessierten C++‑Entwickler wärmstens empfehlen, sich damit vertraut zu machen.
Das gilt natürlich zunächst hauptsächlich für neuen Code. Denn ein wilder Mix zwischen alter und neuer Syntax dient kaum der Lesbarkeit und Verständlichkeit. Und die Umstellung einer umfangreichen Code-Basis will wohl überlegt sein.