Forum: PC-Programmierung Trailing return types in C++


von Wilhelm M. (wimalopaan)


Angehängte Dateien:

Lesenswert?

In C++ sind seit C++11 (und bspw. gcc-4.4) trailing return-types 
enthalten. Sie wurden auch u.a. notwendig wegen lamda-expressions.
1
auto foo() -> int;
2
3
auto foo() -> int {
4
   return 42;
5
}

In meinen persönlichen Gebrauch sind sie jedoch bis auf wenige Ausnahmen 
(s.u.) nicht eingezogen. Es gab nach Erscheinen von C++11 auch einige 
Stimmen, die diese neue Form als alleinige propagierten, aber als 
alleinige Syntax hat sie sich überhaupt nicht durchgesetzt. Es gibt 
natürlich auch Stimmen dafür (wie etwa Phil Nash). Bei mir und in meiner 
"Blase" herrscht aber überwiegend Zurückhaltung.

Ich sehe m.E. folgende sinnvolle / zwingende Einsatzzwecke:

1) Lambda-Expressions mit divergierenden RT bei mehreren return-stmts
2) Trennung von Deklaration und Definition von Funktionstemplates
3) SFINAE-Ugliness
4) Funktionszeiger-Ugliness

1) und 2) sind ggf. eine zwingende Sache.

3) und 4) sind optionale Einsatzzwecke, die ich ganz prima finde. Wobei 
ich SFINAE seit Verfügbarkeit von Constraints / Concepts versuche 
dadurch zu ersetzen. Zudem sind die Meldungen bei Constraints besser.

Bleibt 4) und das finde ich, ist ein echter Gewinn:
1
int test1(){
2
    return 42;
3
}
4
double test2(){
5
    return 42;
6
}
7
auto test3() ->int(*)() {
8
    return test1;
9
}
10
// unleserlich
11
int (*foo1())() {
12
    return test1;
13
}
14
// (*) wesentlich besser zu lesen
15
// foo2() -> Zeiger auf Funktion double()
16
auto foo2() -> double(*)() {
17
    return test2;
18
}
19
// (*) wesentlich besser zu lesen
20
// foo3() -> Zeiger auf Funktion, die Zeiger auf Funktion int() liefert
21
auto foo3() -> auto(*)() -> int(*)() {
22
    return test3;
23
}

Alle Beispiele zu den o.g. Zwecken sind in der angehängten Datei.

: Verschoben durch Moderator
von Peter (Gast)


Lesenswert?

Ich persönlich nutze es auch wenn überhaupt nur für 1) und 2), also wenn 
es unbedingt notwendig ist.

Bei 4) finde ich dedizierte Typedefs/alias besser, da im Produktivcode 
der Typ des Function pointers erfahrungsgemäß mehrmals vorkommt, z.B. 
bei Callbacks oder im Testcode. Über einen eigenen Namen kann man einen 
Callback besser zuordnen und braucht bei Bedarf nur das typedef/alias zu 
ändern.

Beitrag #6098063 wurde vom Autor gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb im Beitrag #6098063:
> Wobei "auto" wie das 3. Rad am Wagen daherkommt.

Das stimmt.
Man könnte es aber auch als function-introducer lesen ...

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Johann L. schrieb im Beitrag #6098063:
>> Wobei "auto" wie das 3. Rad am Wagen daherkommt.
>
> Das stimmt.
> Man könnte es aber auch als function-introducer lesen ...

Das könnte man vielleicht so sehen, wenn es denn nur für Funktionen 
eingesetzt wird. Aber ich nutze es eher für Variablen.

von Bernd K. (prof7bit)


Lesenswert?

Alles halbe Sachen!

Besser alte Zöpfe ganz abschneiden, komplett und konsequent auf 
Pascal-Grammatik (postfix type) umstellen und schon werden auch die 
Funktionszeiger wieder entknotet und lesbar (von links nach rechts 
parsbar). Da würdet ihr die Sprache nicht wiedererkennen, so elegant wär 
die dann. Predige ich schon seit über 35 Jahren. Moderne Sprachen kommen 
zum Glück langsam wieder auf diesen guten alten Trichter.

von Carl D. (jcw2)


Lesenswert?

Anmerkung an TO und/oder Moderation/Admin:
Die Endung .cc wird vom Forum nicht als "C"-artig erkannt und kommt 
deshalb als Text zum Leser. Gab es früher auch bei .ino und wurde damals 
unbürokratisch eingebaut.
Alternativ beim Hochladen bei .c oder .cpp oder einfach .C bleiben.

von mh (Gast)


Lesenswert?

Ich benutze sie immer. Im Gegensatz zu der gestern diskutierten brace 
initialization gibt es keine Sonderfälle und Ausnahmen an die man denken 
muss und an einigen Stellen sind trailing return types die einzige Wahl. 
Es wird also alles einfacher und weniger komplex, wenn man sie überall 
verwendet.

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Ich benutze sie immer.

Das ist interessant.
Machst Du / Ihr das in Deiner Firma übergeifend. Oder ist das nur im 
privaten Hobbyeinsatz?

von Programmierer (Gast)


Lesenswert?

Bernd K. schrieb:
> Besser alte Zöpfe ganz abschneiden,

Kann man bei C++ nicht. Die Abwärts-kompatibilität ist essentiell und 
wird nur in ganz kleinen Aspekten gebrochen, die leicht zu beheben sind. 
C++ ohne Abwärts-kompatibilität ist Rust.

von mh (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Machst Du / Ihr das in Deiner Firma übergeifend. Oder ist das nur im
> privaten Hobbyeinsatz?

Da meine Firma gerade nur aus mir selbst besteht, ist es Firma 
übergreifend ;-).

von Wilhelm M. (wimalopaan)


Lesenswert?

mh schrieb:
> Da meine Firma gerade nur aus mir selbst besteht, ist es Firma
> übergreifend ;-).

Ok ;-)

Andererseits kenne ich wirklich keine Unternehmung oder Projekt mit mehr 
als einem Mitglied, wo das konsequent umgesetzt wird.

Ist ja auch eher ein Library-Writer-Feature, was die meisten Anwender ja 
nicht tun.

von Johannes S. (Gast)


Lesenswert?

als wieder in C++ Einsteiger musste ich erstmal nachsehen worum es geht, 
hier ist das gut und einfach erklärt: 
https://www.nosid.org/cxx-trailing-return-types.html
Bisher habe ich allerdings auch mit typedefs gearbeitet um 
Funktionspointer zu entschärfen, zB wenn ich eine Map mit 
Handlerfunktionen habe (wie beim Einwand von Peter (Gast)). Aber 
prinzipiel finde ich die Syntax gut.

von Vlad T. (vlad_tepesch)


Lesenswert?

Wilhelm M. schrieb:
> auto test3() ->int(*)() {
>     return test1;
> }

> // (*) wesentlich besser zu lesen
> // foo3() -> Zeiger auf Funktion, die Zeiger auf Funktion int() liefert
> auto foo3() -> auto(*)() -> int(*)() {
>     return test3;
> }

gibts hier überhaupt einen grund am ende den Rückgabewert anzugeben und 
ihn nicht komplett automatisch bestimmen zu lassen?
1
int test1(){
2
    return 42;
3
}
4
double test2(){
5
    return 42;
6
}
7
auto test3() {
8
    return test1;
9
}
10
11
// (*) wesentlich besser zu lesen
12
// foo2() -> Zeiger auf Funktion double()
13
auto foo2() {
14
    return test2;
15
}
16
// (*) wesentlich besser zu lesen
17
// foo3() -> Zeiger auf Funktion, die Zeiger auf Funktion int() liefert
18
auto foo3(){
19
    return test3;
20
}

: Bearbeitet durch User
von Programmierer (Gast)


Lesenswert?

Vlad T. schrieb:
> gibts hier überhaupt einen grund am ende den Rückgabewert anzugeben und
> ihn nicht komplett automatisch bestimmen zu lassen

Wenn man mehrere return hat klappt das ggf nicht, und wenn der 
Rückgabetyp von template-Parametern an die Funktion abhängt kann das die 
Kompilation verlangsamen. Außerdem ist es nicht sehr übersichtlich, den 
Typ im Body suchen zu müssen.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wenn alter mit neuem C++-Code gemischt wird (was bei größeren Projekten
praktisch immer der Fall ist) und ich die Wahl habe, verwende ich die
alte Syntax, damit der Gesamtcode nicht in Kauderwelsch ausartet¹.
Außerdem schließe ich mich diesbezüglich Peters Meinung an:

Peter schrieb:
> Bei 4) finde ich dedizierte Typedefs/alias besser, da im Produktivcode
> der Typ des Function pointers erfahrungsgemäß mehrmals vorkommt, z.B.
> bei Callbacks oder im Testcode. Über einen eigenen Namen kann man einen
> Callback besser zuordnen und braucht bei Bedarf nur das typedef/alias zu
> ändern.

Damit stellt auch die etwas verschlungene Typsyntax von Funktionszeigern
kein Problem dar.

Dass man für den Return-Typ in Lambda-Ausdrücken eine andere Syntax
braucht, ist nicht besonders schön, aber auch nicht so tragisch, da die
Lambda-Ausdrücke ohnehin schon eine spezielle Syntax haben. Man könnte
allenfalls die Lambdas komplett ablehnen, was ich aber nicht tue, weil
damit endlich ein wichtiges Feature eingeführt wurde, das eigentlich
schon lange vor C++11 überfällig war.


Rust verwendet ja eine ganz ähnliche Syntax für Funktionsdeklarationen:

C++:
1
auto myfunc(int n) -> double { ... }

Rust:
1
fn myfunc(n: i32) -> f64 { ... }

Die Unterschiede:

- In Rust gibt es nur die trailing Syntax, also keinen Kauderwelsch.

- Rust verwendet auch für Variablendeklarationen die trailing Syntax
  (mit einem ":" statt des "->" wegen der etwas anderen Bedeutung).
  Deswegen passen die trailing Return-Types gut zur restlichen Syntax
  von Rust, während sie in C++ sie eher wie ein Fremdkörper wirken.

- Während die Deklaration in C++ mit einem "auto" beginnt, das ja
  eigentlich ein Platzhalter für einen Typ ist, aber – wie du oben
  schriebst – auch als Function-Introducer gelesen werden kann,
  verwendet Rust einen echten Function-Introducer "fn", mit dem jede
  Funktionsdeklaration eingeleitet wird.


Bernd K. schrieb:
> Besser alte Zöpfe ganz abschneiden,

Das wird in C++ nicht mehr geschehen. Deswegen habe ich mir im
Nachbarthread ein C++2 gewünscht, das zu C++ ähnlich, aber nicht
abwärtskompatibel ist und alle bekannten Unstimmigkeiten beiseite
schafft.

Programmierer schrieb:
> C++ ohne Abwärts-kompatibilität ist Rust.

Rust unterscheidet sich von C++ schon sehr stark, hat aber IMHO von
allen derzeitigen Programmiersprachen das größte Potential, irgendwann
C++ zu ersetzen.

Bernd K. schrieb:
> komplett und konsequent auf Pascal-Grammatik (postfix type) umstellen

Ich persönlich finde die Typsyntax, wie sie von Ritchie geschaffen
wurde, gar nicht so schlimm. Sie ist zwar etwas gewöhnungsbedürftig,
aber in sich konsistent. Das änderte sich erst mit der Einführung von

- const (ANSI-C)
- Referenzen (C++)
- Rvalue-Referenzen (C++11)
- trailing Return-Types (C++11)

> und schon werden auch die Funktionszeiger wieder entknotet und lesbar
> (von links nach rechts parsbar).

Wenn man für die Funktionszeiger – wie von Peter empfohlen – Typsynonyme
(in C mit typedef) nutzt, verschwinden die Knoten. In Pascal ist die
Verwendung von Typsynonymen im Zusammenhang mit Funktionszeigern sogar
vorgeschrieben, vermutlich um überlange Typspezifikationen zu vermeiden.

In C++ kann man zudem oft lange und komplizierte Typspezifikationen in
Deklarationen durch "auto" zu ersetzen. Das ist zwar keine vollwertige
Type-Inference wie bspw. in Haskell, hilft aber in vielen Fällen, den
Code übersichtlicher zu gestalten.

Gerade für die Verwendung von Funktionen als First-Class-Objekte (also
für die funktionale Programmierung, für die derzeit ein wachsender Trend
zu erkennen ist) hat C++ mittlerweile sehr viel mehr zu bieten als Free
Pascal. In Free Pascal sind wohl diesbezüglich auch Entwicklungen im
Gange, aber da wird man wohl noch eine Weile auf die Ergebnisse warten
müssen.


————————————
¹) Aus demselben Grund bevorzuge ich auch die Initialisierung mit = oder
   () anstelle des neuen {} (s. Nachbarthread zur Brace-Initialization).

von Wilhelm M. (wimalopaan)


Lesenswert?

Peter schrieb:
> Bei 4) finde ich dedizierte Typedefs/alias besser, da im Produktivcode
> der Typ des Function pointers erfahrungsgemäß mehrmals vorkommt,

Was bei generischen Typen wieder umständlicher wird, da man dann dafür 
ein alias-template deklarieren muss.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.