Forum: Mikrocontroller und Digitale Elektronik C++ call by reference Frage


von Michael (Gast)


Lesenswert?

Hallo, ich habe zwei C++ Anfängerfragen: Es geht darum, den Code für ein 
Spiel, das ich programmiert habe, zu optimieren. Programmiert wird für 
einen ATMega2560 mit dem Arduino Framework (via VS Code und PlatformIO).

Im Moment werden alle Parameter als Wert an Funktionen übergeben. Zwar 
werden die Parameter in den Funktionen nicht verändert, es wäre aber 
IMHO sauberer, die Parameter als Referenz zu übergeben.

Das Problem ist nun, dass Funktionen mit Variablen oder konstanten 
Werten aufgerufen werden (z.B.
1
SetAllPixel(TRUE);
) Somit aktzeptiert der Compiler die Deklaration nicht. Wie löst man das 
Problem sauber? Doch beim call by value bleiben?

Und noch eine Frage zu Klassen. Wäre es angemessen, alle Funktionen des 
Spiels in eine Klasse zu packen? Von der Klasse müsste eh nur eine 
Instanz erzeugt werden, aber wäre es sauberer, dies mit einer Klasse zu 
lösen, anstatt den gesamten Code in einer cpp-Datei unterzubringen?

Danke vorab und schöne Grüße!

von Vincent H. (vinci)


Lesenswert?

Michael schrieb:
> Hallo, ich habe zwei C++ Anfängerfragen: Es geht darum, den Code für ein
> Spiel, das ich programmiert habe, zu optimieren. Programmiert wird für
> einen ATMega2560 mit dem Arduino Framework (via VS Code und PlatformIO).
>
> Im Moment werden alle Parameter als Wert an Funktionen übergeben. Zwar
> werden die Parameter in den Funktionen nicht verändert, es wäre aber
> IMHO sauberer, die Parameter als Referenz zu übergeben.

Solche Optimierungen sind selten sinnvoll. Im Gegenteil. Kleine Objekte 
wie etwa einen Integer sollte man lieber via Kopie übergeben als via 
Referenz.


> Das Problem ist nun, dass Funktionen mit Variablen oder konstanten
> Werten aufgerufen werden (...) Somit
> aktzeptiert der Compiler die Deklaration nicht. Wie löst man das Problem
> sauber? Doch beim call by value bleiben?

Dieses Problem bestätigt meine Vermutung dass dein Vorhaben eher eine 
Pessimierung wäre. Wenn die Deklaration nach deinen Änderungen 
folgendermaßen aussieht
1
void SetAllPixel(bool&);

dann signalisierst du damit, dass der Übergabeparameter innerhalb der 
Funktion verändert wird. Im Gegensatz zur Kopie
1
void SetAllPixel(bool);

sorgt dies für Code der nicht nur langsamer, sondern auch deutlich 
schlechter lesbar ist.


Womit du dich trotzdem beschäftigten solltest wären die Begriffe lvalue, 
rvalue und generell value categories. Es ist nämlich sehr wohl möglich 
einen sogenannten rvalue (in deinem Fall true) an einen lvalue 
(Referenz) zu binden. So kann man folgende Funktion etwa mit true 
aufrufen.
1
void SetAllPixel(bool const&);

Wie vorher bereits erwähnt macht das Binden an eine konstante Referenz 
bei großen Objekten durchaus Sinn, aber niemals, NIEMALS solltest du 
eine (nicht-konstante) Referenz statt einer Kopie verwenden.


> Und noch eine Frage zu Klassen. Wäre es angemessen, alle Funktionen des
> Spiels in eine Klasse zu packen? Von der Klasse müsste eh nur eine
> Instanz erzeugt werden, aber wäre es sauberer, dies mit einer Klasse zu
> lösen, anstatt den gesamten Code in einer cpp-Datei unterzubringen?

Vermutlich nicht. C++ ist nicht Java. Nicht alles muss eine Klasse sein. 
Wenn schon, dann wird dein Spiel vermutlich aus vielen kleinen Klassen 
bestehen, die alle für sich kleine Teilbereiche deines Programms 
abdecken und jeweils eine Anforderung erfüllen. Das nennt sich im 
Fachjargon das "single responsibility principle".

von Guest (Gast)


Lesenswert?

Schöner wäre auch die eingebauten Datentypen bool und die Konstanten 
true/false statt irgendwelchen großgeschriebenen Sachen zu verwenden.

von Wilhelm M. (wimalopaan)


Lesenswert?

Vincent H. schrieb:
> Solche Optimierungen sind selten sinnvoll. Im Gegenteil. Kleine Objekte
> wie etwa einen Integer sollte man lieber via Kopie übergeben als via
> Referenz.

Am Anfang hilft hier ggf. eine Regel:

- für eingebaute DT: pass-by-value
- für user-defined-types: pass-by-const-ref

Vincent H. schrieb:
> Wenn die Deklaration nach deinen Änderungen
> folgendermaßen aussieht
> void SetAllPixel(bool&);

So etwas nennt man dann oft "output-parameter". Dies sollte die Ausnahme 
sein.

Zusammengefasst:
1
void foo(int); // Input-Parameter
2
void foo(const A&); // Input-Parameter
3
4
void bar(int&); // Output-Parameter
5
void bar(A&); // Output-Parameter

Vielleicht noch ein kleiner Hinweis, der aber u.U. für Anfänger ganz 
nützlich ist. Folgender kleiner Fehler
1
void foo(int); // in Header
2
3
void foo(int v) {
4
    if (v = 4) {...} // ups (nur warnung)
5
}

lässt sich ganz einfach vermeiden:
1
void foo(int); // in Header
2
3
void foo(const int v) {
4
    if (v = 4) {...} // error
5
}

Man setzt also in der Definition ein const hinzu. Damit hat man 
dieselbe Qualität wie bei pass-by-const-ref. (Natürlich warnt der 
Compiler bei dem obigen Missgeschick, aber viele, gerade Anfänger, 
nutzen nicht die höchste Warnstufe des Compilers).

von Helmie (Gast)


Lesenswert?

Wilhelm M. schrieb:
> void foo(int v) {
>     if (v = 4) {...} // ups (nur warnung)
> }

Warum eine Warning? Ich kann doch lokal mit dem v machen was ich will. 
Es ist nur eine Kopie.
:-<<<

von Helmie (Gast)


Lesenswert?

Wenn du den Fehler der Zuweisung bei if meinst, schreibt man das 
anders herum, also erst die Konstante.

   if (4 = V)  // richtig soll es 4 == v heissen

Man, man, man, ...

:-<<<

von Michael (Gast)


Lesenswert?

Danke vielmals für die Antworten, das hilft mir sehr weiter!

Vincent H. schrieb:
> dann signalisierst du damit, dass der Übergabeparameter innerhalb der
> Funktion verändert wird. Im Gegensatz zur Kopie
> void SetAllPixel(bool);

Ich dachte es ist genau anders herum, denn bei void SetAllPixel(bool); 
muss ja eben erst eine Kopie erstellt werden...

Guest schrieb:
> Schöner wäre auch die eingebauten Datentypen bool und die Konstanten
> true/false statt irgendwelchen großgeschriebenen Sachen zu verwenden.

Das stimmt natürlich, im richtigen Code hatte ich es klein geschrieben, 
im Beispiel dann dummerweise groß.

von Vincent H. (vinci)


Lesenswert?

Du betrachtest die Dinge stets aus der Sicht der Funktion. Die ist in 
der Praxis jedoch wesentlich unwichtiger als jene von außerhalb.

Eine Kopie signalisiert dem Nutzer der Funktion, dass seine eigenen 
Variablen nicht angefasst werden und sich folglich auch nicht ändern 
können. Was in der Funktion passiert ist dem Aufrufer egal.

Das ist auch der Grund weshalb man das const keyword so selten bei pass 
by value sieht obwohl es eigentlich sinnvoll wäre. Aber Programmierer 
sind halt faul...

von Wilhelm M. (wimalopaan)


Lesenswert?

Vincent H. schrieb:
> Du betrachtest die Dinge stets aus der Sicht der Funktion. Die ist in
> der Praxis jedoch wesentlich unwichtiger als jene von außerhalb.

Ein ganz wichtiger Satz wurde einmal von Scott Meyers dazu gesagt:

"Eine Schnittstelle sollte leicht richtig, und nur schwer falsch zu 
benutzen sein"

Diesen Satz sollte man sich bei jedem Schnittstellendesign in das 
Gedächtnis rufen und beherzigen.

Vincent H. schrieb:
> Das ist auch der Grund weshalb man das const keyword so selten bei pass
> by value sieht obwohl es eigentlich sinnvoll wäre.

Ganz allgemein ist const-correctness eine super wichtige Sache.

Auch wenn const-correctness natürlich ein weites Feld ist, sollte man 
auch hier etwa nach folgendem Spruch handeln:

"Mache alles(!) const was du kannst, und nur das non-const, was Du muss"

BTW: const in C++ heißt nicht im engeren Sinn "Konstant", sondern 
read-only.

Daher auch der Hinweis, Parametervariablen in der Funktionsdefinition 
auch bei pass-by_value const zu machen. Denn es sind Variablen, die nach 
obiger Regel (meistens) eben read-only sind. Denn würde man ihren Wert 
in der Funktion ändern, so kommst dies meist einem "Bedeutungswechsel" 
gleich. Und so etwas macht den Code schwer verständlich. Kein Angst vor 
zusätzlichen Kopien: vertraue dem Compiler, alles unnütze wird er 
wegoptimieren.

Bspw ist es m.E. besser zu schreiben:
1
void foo(const int startIndex) {
2
   ...
3
   auto index = startIndex;
4
   ...
5
   ++index;
6
   ...
7
}

als
1
void foo(int startIndex) {   
2
   ...
3
   ++startIndex;
4
   ...
5
}

denn hier hat die Variable startIndex später nicht mehr die Bedeutung 
des Start-Indizes.

von Rolf M. (rmagnus)


Lesenswert?

Michael schrieb:
> Vincent H. schrieb:
>> dann signalisierst du damit, dass der Übergabeparameter innerhalb der
>> Funktion verändert wird. Im Gegensatz zur Kopie
>> void SetAllPixel(bool);
>
> Ich dachte es ist genau anders herum, denn bei void SetAllPixel(bool);
> muss ja eben erst eine Kopie erstellt werden...

In der Regel werden solche einfachen Parameter in Registern übergeben.
Konkret beim avr-g++ wird dann gar nichts kopiert. Wenn du stattdessen 
aber eine (const-)Referenz übergibst, wird die Variable erst vom 
Register in den Speicher kopiert, dann deren Adresse in das 
Übergaberegister geschrieben und damit an die Funktion übergeben, so 
dass diese den bool dann wieder aus dem Speicher in ein Register 
zurückkopieren kann. Die Adresse, die an die Funktion übergeben wird, 
ist zudem größer als der bool selbst.
Das ist also mehr Aufwand.

von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Michael schrieb:
>> Vincent H. schrieb:
>>> dann signalisierst du damit, dass der Übergabeparameter innerhalb der
>>> Funktion verändert wird. Im Gegensatz zur Kopie
>>> void SetAllPixel(bool);
>>
>> Ich dachte es ist genau anders herum, denn bei void SetAllPixel(bool);
>> muss ja eben erst eine Kopie erstellt werden...
>
> In der Regel werden solche einfachen Parameter in Registern übergeben.
> Konkret beim avr-g++ wird dann gar nichts kopiert. Wenn du stattdessen
> aber eine (const-)Referenz übergibst, wird die Variable erst vom
> Register in den Speicher kopiert, dann deren Adresse in das
> Übergaberegister geschrieben und damit an die Funktion übergeben, so
> dass diese den bool dann wieder aus dem Speicher in ein Register
> zurückkopieren kann. Die Adresse, die an die Funktion übergeben wird,
> ist zudem größer als der bool selbst.
> Das ist also mehr Aufwand.

Oft kann der Compiler das aber optimieren.

Beim gcc ist diese Optimierung irgendwie immer nur bei 
template-Funktionen aktiv. Dazu folgendes Beispiel: hier ist das 
normalerweise stattfindende inlining man explizit abgeschaltet, damit 
man mal sieht, was der Compiler generieren würde, wenn mal nicht 
ge-inlined wird.
1
#include <cstdint>
2
3
volatile uint8_t x;
4
5
template<typename T>
6
void f(const T& v) __attribute__((noinline));
7
8
template<typename T>
9
void f(const T& v) {
10
    x = v / 2;
11
}
12
13
uint8_t a = 10;
14
uint16_t b = 20;
15
16
int main() {
17
    f(a);
18
    f(b);
19
}

Generiert:
1
.text
2
.type   _Z1fIhEvRKT_.isra.0, @function
3
_Z1fIhEvRKT_.isra.0:
4
/* prologue: function */
5
/* frame size = 0 */
6
/* stack size = 0 */
7
.L__stack_usage = 0
8
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:28:     x = v / 2;
9
lsr r24          ;  _3
10
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:28:     x = v / 2;
11
sts x,r24        ;  x, _3
12
/* epilogue start */
13
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:29: }
14
ret
15
.size   _Z1fIhEvRKT_.isra.0, .-_Z1fIhEvRKT_.isra.0
16
.type   _Z1fIjEvRKT_.isra.0, @function
17
_Z1fIjEvRKT_.isra.0:
18
/* prologue: function */
19
/* frame size = 0 */
20
/* stack size = 0 */
21
.L__stack_usage = 0
22
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:28:     x = v / 2;
23
lsr r25  ;  tmp46
24
ror r24  ;  tmp46
25
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:28:     x = v / 2;
26
sts x,r24        ;  x, tmp46
27
/* epilogue start */
28
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:29: }
29
ret
30
.size   _Z1fIjEvRKT_.isra.0, .-_Z1fIjEvRKT_.isra.0
31
.section        .text.startup,"ax",@progbits
32
.global main
33
.type   main, @function
34
main:
35
/* prologue: function */
36
/* frame size = 0 */
37
/* stack size = 0 */
38
.L__stack_usage = 0
39
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:35:     f(a);
40
lds r24,a        ; , a
41
call _Z1fIhEvRKT_.isra.0         ;
42
;  /home/lmeier/Projekte/wmucpp/doc0/bm00/bm91.cc:36:     f(b);
43
lds r24,b        ; , b
44
lds r25,b+1      ; , b
45
call _Z1fIjEvRKT_.isra.0         ;
46
ldi r25,0                ;
47
ldi r24,0                ;
48
/* epilogue start */
49
ret
50
.size   main, .-main

Man sieht, dass hier die isra-Optimierung gegriffen hat (alles, was 
kleiner eines Pointer-Typs ist, wird direkt kopiert).

Ärgerlicherweise wird das wohl beim Testen des AVR-Backends vergessen zu 
testen. Denn es schwankt immer nach Version des avr-g++, ob das 
stattfindet oder nicht. Aktuell ist es gerade wieder aus dem gcc-10.0.0 
draußen :-( Das obige wurde daher mit avr-g++ 9.2.0 erzeugt.

Umgekehrt werden aber auch kleine Objekte von UDTs, die per-value 
übergeben werden, direkt in Registern transportiert.

Das war nur der Vollständigkeit halber. Die obige Regel (primitive DF 
per-value, UDT per const-ref) bleibt erhalten.

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Ärgerlicherweise wird das wohl beim Testen des AVR-Backends vergessen zu
> testen. Denn es schwankt immer nach Version des avr-g++, ob das
> stattfindet oder nicht. Aktuell ist es gerade wieder aus dem gcc-10.0.0
> draußen :-( Das obige wurde daher mit avr-g++ 9.2.0 erzeugt.

Ich hatte es mit meinem avr-gcc mit einem non-template ausprobiert, und 
der hat es nicht gemacht. Deshalb hab ich gedacht, dass das immer so 
sei. Wenn man das ändert, heißt das ja auch jedes mal eine Änderung des 
ABI, was ja eigentlich möglichst vermieden werden sollte.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wilhelm M. schrieb:
> Ärgerlicherweise wird das wohl beim Testen des AVR-Backends vergessen zu
> testen. Denn es schwankt immer nach Version des avr-g++, ob das
> stattfindet oder nicht. Aktuell ist es gerade wieder aus dem gcc-10.0.0
> draußen :-( Das obige wurde daher mit avr-g++ 9.2.0 erzeugt.

Was führt dich zu dem Schluss, es sei ein Problem des Backends?

Rolf M. schrieb:
> [...] heißt das ja auch jedes mal eine Änderung des ABI,

Wo siehst du da eine ABI-Änderung?

> was ja eigentlich möglichst vermieden werden sollte.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:
> Was führt dich zu dem Schluss, es sei ein Problem des Backends?

Ehrlich gesagt: ich habe keine Ahnung.

Ich meine mich lediglich zu erinnern, dass das für andere Zielsysteme 
anders ist. Jedoch ist das schon eine Zeit her, und ich habe es 
tatsächlich nur gerade nochmal für AVR und gcc-9.2 vesrus 10.0 geprüft.

Also: kläre uns bitte auf! Wo findet diese Art der Optimierung statt?

von Rolf M. (rmagnus)


Lesenswert?

Johann L. schrieb:
> Rolf M. schrieb:
>> [...] heißt das ja auch jedes mal eine Änderung des ABI,
>
> Wo siehst du da eine ABI-Änderung?

Die Art, wie Parameter übergeben werden, ist Teil des ABI. Wenn ein 
skalarer const-Referenzparameter in einer Version per Zeiger und in der 
anderen direkt in einem Register übergeben wird, ist das 
selbstverständlich eine Änderung des ABI.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Rolf M. schrieb:
> Johann L. schrieb:
>> Rolf M. schrieb:
>>> [...] heißt das ja auch jedes mal eine Änderung des ABI,
>>
>> Wo siehst du da eine ABI-Änderung?
>
> Die Art, wie Parameter übergeben werden, ist Teil des ABI. Wenn ein
> skalarer const-Referenzparameter in einer Version per Zeiger und in der
> anderen direkt in einem Register übergeben wird, ist das
> selbstverständlich eine Änderung des ABI.

Testfall?

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wilhelm M. schrieb:
> Z1fIhEvRKT.isra.0:
> ...
> Z1fIjEvRKT.isra.0:

Die Parameterübergabe dieser beiden Funktionen ist per -fipa-sra
optimiert worden (deswegen das "isra" im Label), siehe

  https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

Die Funktionen sind nur in der Übersetzungseinheit, in der sie definiert
wurden, bekannt und müssen sich deswegen an keinerlei ABI-Konventionen
halten.

Werden diese Funktionen in mehreren Übersetzungseinheiten verwendet,
wird in diesen jeweils eine eigene Kopie der Funktionen angelegt, was
bei größeren Funktionen ziemlich speicherfressend wäre. Deswegen wird ab
einer gewissen Größe der Funktionen auf die obige Optimierung verzichtet
und statt dessen von jeder Funktion nur eine einzige, ABI-konforme
Instanz angelegt (bzw. dafür gesorgt, dass mehrere gleiche Instanzen
beim Linken zu einer einzigen zusammengefasst werden). Diese bekommt
dann tatsächlich nicht eine Kopie, sondern wie erwartet eine Referenz
von a bzw. b übergeben.

Die Kriterien dafür, ob die Parameterübergabe per SRA optimiert wird
oder nicht, können natürlich von Version zu Version etwas variieren.
Damit ist aber keine Änderung des ABI verbunden.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

vielleicht mal ein Hinweis für die Vorschnellen. GCC 10 ist noch lange 
nicht fertig. Es wäre vermessen jetzt schon voreilige Rückschlüsse zu 
ziehen.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Yalu X. schrieb:
> Wilhelm M. schrieb:
>> Z1fIhEvRKT.isra.0:
>> ...
>> Z1fIjEvRKT.isra.0:
>
> Die Parameterübergabe dieser beiden Funktionen ist per -fipa-sra
> optimiert worden (deswegen das "isra" im Label), siehe
>
>   https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
>
> Die Funktionen sind nur in der Übersetzungseinheit, in der sie definiert
> wurden, bekannt und müssen sich deswegen an keinerlei ABI-Konventionen
> halten.

Tatsächlich halten sich diese Funktionen ans ABI :-)

Das ABI ändert sich durch Inter-Procedure Value Propagation genauso 
wenig, wie es sich durch Inlining ändert.  Konkret: ist der 1. Parameter 
von xxx.isra.0 z.B. ein (impliziter) Pointer oder eine Referenz, so wird 
diese in R25:R24 übergeben.

ABER: Das Interface von xxx.isra.0 ist ein anderes als das von xxx. 
Sinn & Zweck von IPVP ist nämlich gerade, dass im Caller bekannte 
Parameter wie true nicht an den Callee übergeben werden (müssen), d.h. 
es wird eine andere, künstliche (artificial) Funktion erstellt, die 
andere Parameter hat als die Originalfunktion xxx.  Die 
Parameterübergabe des Clones folgt jedoch 100% dem ABI.

Intern werden Clones zwar anders erzeugt, man könnte sich ein Clone aber 
auch so erzeugt vorstellen:  1) Der Callee wird geinlinet 2) ein Stück 
des Callers wird in eine separate Funktion geoutlined, wobei der 
(Assembler-)Name der Outline-Funktion mit keinem Name (also mangled 
Name) einer C- oder C++-Funktion kollidiert, weil in einem anderen 
Namespace ("." im asm-Name).

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wilhelm M. schrieb:
> Also: kläre uns bitte auf! Wo findet diese Art der Optimierung statt?

IPA-Optimierungen in den IPA-Passes, diese können mit -fdump-ipa-xxx 
gedumpt werden, wobei xxx den Pass bezeichnet.  "all" für alle 
IPA-Passes.

http://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html#index-fdump-ipa

Für v8 sehe ich zum Beispiel folgende Passes:
1
.000i.cgraph
2
.000i.ipa-clones
3
.000i.type-inheritance
4
.016i.visibility
5
.017i.build_ssa_passes
6
.022i.opt_local_passes
7
.047i.remove_symbols
8
.059i.targetclone
9
.063i.free-fnsummary1
10
.066i.emutls
11
.067i.whole-program
12
.068i.profile_estimate
13
.069i.icf
14
.070i.devirt
15
.071i.cp
16
.072i.sra
17
.075i.fnsummary
18
.076i.inline
19
.077i.pure-const
20
.078i.free-fnsummary2
21
.079i.static-var
22
.080i.single-use
23
.081i.comdats
24
.082i.materialize-all-clones

Das "i" steht für "ipa".  "r" steht für "rtl" und "t" für "tree", die 
man mit -fdump-rtl-all -fdump-tree-all dumpen kann.

von Helmie (Gast)


Lesenswert?

Wilhelm M. schrieb:
> Bspw ist es m.E. besser zu schreiben:
> void foo(const int startIndex) {
>    ...
>    auto index = startIndex;

Das verkompliziert den Code nur unnötig. Da es in C (ich mag es auch 
nicht) extra die Unterscheidung 'call by ...' gibt, kann man, oder 
besser sollte jeder, es auch so nutzen. Basta!

von Guest (Gast)


Lesenswert?

Jo, sehe ich auch so.
a) Eine Deklaration
1
void foo(const int param);
 sieht unschön aus und was interessiert den Caller ob in der 
Implementierung param const ist.

b) In der Definition bringt ein const param IMHO keine Benefits, klar 
kann man den dann überschreiben aber das ist für mich legitimer Code.

von mh (Gast)


Lesenswert?

Helmie schrieb:
> Das verkompliziert den Code nur unnötig.

Guest schrieb:
> Jo, sehe ich auch so.

Seid ihr sicher, dass ihr gelesen und verstanden habt, was Wilhelm M. 
geschrieben hat?

Helmie schrieb:
> Basta!

Vermutlich nicht.

von Helmie (Gast)


Lesenswert?

Wie Willi schrieb, kümmert sich der Compiler um die Umsetzung. Also ist 
für den Programmierer egal was unter der Gaube passiert. C ist ne 
veraltete Sch... Sprache. Man muss es nicht unnötig kompliziert machen. 
Für Willi und seine Spießgesellen ist es Selbstzweck. Der Weg ist Euer 
Ziel. ?

von Wilhelm M. (wimalopaan)


Lesenswert?

Guest schrieb:
> Eine Deklarationvoid foo(const int param);  sieht unschön aus und was
> interessiert den Caller ob in der
> Implementierung param const ist.

Genau. Und deswegen hatte ich das auch NICHT(!) vorgeschlagen (s.o.).

Guest schrieb:
> In der Definition bringt ein const param IMHO keine Benefits, klar
> kann man den dann überschreiben aber das ist für mich legitimer Code.

Klar wäre das legitimer Code, genauso wie Code, der auf 
const-correctness verzichtet (nicht zwischen Beobachtern und 
Modifikatoren unterscheidet). Es ist aber schlechter Code.

Wenn man viel fremden Code liest / lesen muss, so ist man froh, wenn er 
so expressiv wie möglich ist und seine Intention möglich schnell zu 
erfassen ist. Und genau dazu trägt die const-Quali der 
Parametervariablen bei.

Zudem kann die const-Quali Fehler verhindern, wie oben schon mal 
geschrieben. Natürlich kann man z.B. triviale Fehler (s.o.) für 
primitive DT auch per YODA-Style zu verhindern versuchen. Es bleiben 
aber genug andere Fehlerquellen.

BTW: ich gehöre zu der Gruppe, die meint, ALLE Variablen sollten 
standardmäßig read-only sein. Und die Modifizierbarkeit sollte 
explizit(!) mit etwa dem Schlüsselwort mutable qualifiziert werden. Also 
genau umgekehrt als in C/C++ jetzt. Ebenso wie alle ctor'en 
standardmäßig explicit und alle Funktionen constexpr sein sollten.

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Wenn man viel fremden Code liest / lesen muss, so ist man froh, wenn er
> so expressiv wie möglich ist und seine Intention möglich schnell zu
> erfassen ist. Und genau dazu trägt die const-Quali der
> Parametervariablen bei.

Das Problem ist einfach, dass es sich beim Funktionskopf ja eigentlich 
um die Definition einer Schnittstelle handelt. Das const steht da mitten 
drin, ist aber eigentlich nicht Teil der Schnittstellendefinition. Das 
macht es meiner Meinung nach unschön.

> BTW: ich gehöre zu der Gruppe, die meint, ALLE Variablen sollten
> standardmäßig read-only sein.

Das würde ich auch besser finden. Gibt es eigentlich eine Sprache, die 
das so handhabt?

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Das Problem ist einfach, dass es sich beim Funktionskopf ja eigentlich
> um die Definition einer Schnittstelle handelt.

Nun ja, zwar ist eine Definition auch eine Deklaration. Jedoch ist es ja 
üblich, sinnvoll und meistens zwingend, Deklaration und Definition zu 
trennen. Und dann verwendet man die const-Quali bei per-value nur in der 
Definition, denn nur dort hat sie eine Bedeutung. In der Deklaration ist 
sie überflüssig (und verwirrend).

von Vincent H. (vinci)


Lesenswert?

Rolf M. schrieb:
> Das würde ich auch besser finden. Gibt es eigentlich eine Sprache, die
> das so handhabt?

Unter den funktionale Sprachen alle. Unter den Systemsprachen ging Rust 
diesen Weg.

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Und dann verwendet man die const-Quali bei per-value nur in der
> Definition, denn nur dort hat sie eine Bedeutung. In der Deklaration ist
> sie überflüssig (und verwirrend).

Verwirrend finde ich aber auch, wenn Deklaration und Definition 
unterschiedlich aussehen.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Wilhelm M. schrieb:
> BTW: ich gehöre zu der Gruppe, die meint, ALLE Variablen sollten
> standardmäßig read-only sein.

Ich auch.

Vincent H. schrieb:
> Rolf M. schrieb:
>> Das würde ich auch besser finden. Gibt es eigentlich eine Sprache, die
>> das so handhabt?
>
> Unter den funktionale Sprachen alle. Unter den Systemsprachen ging Rust
> diesen Weg.

Das war das erste Feature, das ich an Rust interessant fand und das mich
(zusammen mit der Rust-eigenen Pointer-Philosophie) veranlasste, mir
diese Sprache etwas näher anzuschauen.

Mich wundert es etwas, dass das nicht in viel mehr imperativen Sprachen
so gehandhabt wird. Denn schließlich werden in den meisten Programmen
gefühlt sowieso schon mehr als die Hälfte aller Variablen nur einmal
beschrieben und danach nicht mehr verändert. Man würde damit den
Programmierern also kaum Steine in den Weg legen. Const als Default
würde die Programmierer dazu bewegen, nicht nur diese, sondern auch noch
weitere Variablen immutable zu machen, was dem Aufdecken von Fehlern
durch den Compiler und den Optimierungsmöglichkeiten entgegenkommt.

C++ ist eine alte Sprache, da lässt sich so etwas im Nachhinein kaum
noch ändern. Aber warum ging man bspw. bei C# nicht diesen Weg? Die
Sprachdesigner hatten damals alle Freiheiten (es bestand keinerlei
Kompatibilitätszwang), und die Diskussion über die Vorteile von const
gab es ja schon länger.

Rolf M. schrieb:
> Verwirrend finde ich aber auch, wenn Deklaration und Definition
> unterschiedlich aussehen.

Ja, logisch sinnvoller wäre es, die const-Deklaration von Parametern
nicht im Funktionskopf, sondern im Implementierungsteil vorzunehmen.
Nur müsste dann fast jeder Parameter ein weiteres Mal deklariert werden,
was auch wieder unschön ist.

Auch hier würde ein const-Default helfen.

von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Verwirrend finde ich aber auch, wenn Deklaration und Definition
> unterschiedlich aussehen.

Finde ich eigentlich nicht: das eine ist die Schnittstelle, das andere 
die Realisierung.

Außerdem ist das auch bei anderen Charakteristiken von Funktionen so: 
z.B. default-Werte, non-/static und Attribute.

von Wilhelm M. (wimalopaan)


Lesenswert?

Yalu X. schrieb:
> Aber warum ging man bspw. bei C# nicht diesen Weg?

Schau Dir Java an:

- Variablen r/w
- final nicht konsequent (s.a. immutables)
- Monitore (synchronized) nicht konsequent
- Generics nicht konsequent
- ...

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Yalu X. schrieb:
> Wilhelm M. schrieb:
>> BTW: ich gehöre zu der Gruppe, die meint, ALLE Variablen sollten
>> standardmäßig read-only sein.
>
> Ich auch.
>
> Vincent H. schrieb:
>> Rolf M. schrieb:
>>> Das würde ich auch besser finden. Gibt es eigentlich eine Sprache, die
>>> das so handhabt?
>>
>> Unter den funktionale Sprachen alle. Unter den Systemsprachen ging Rust
>> diesen Weg.
>
> Das war das erste Feature, das ich an Rust interessant fand und das mich
> (zusammen mit der Rust-eigenen Pointer-Philosophie) veranlasste, mir
> diese Sprache etwas näher anzuschauen.

Inzwischen gibt es ein Bounty für GCC+Rust:

https://www.bountysource.com/issues/86138921-rfe-add-a-frontend-for-the-rust-programming-language

Ist vermutlich erfolgreicher als das Bounty für avr-gcc...

Es gibt zwar bereits Rust-Frontends für GCC, die aber nicht Teil der GCC 
sind.

http://gcc.gnu.org/ml/gcc-patches/2019-12/msg01568.html

Das Frontend muss also "nur" noch integriert werden.

Dass es irgendwann mal einen avr-grust geben wird, ist allerdings sehr 
unwahrscheinlich.  Zum einen fliegt AVR demnächst mit ziemlicher 
Sicherheit aus GCC raus, zum anderen ist eine Sprache mehr als ihr Kern, 
und ein Großteil ihrer Features wird per Bibliotheken verfügbar gemacht.

Hier würde sich wohl eine ähnliche Situation ergeben wie mit avr-g++: 
Wird zwar verwendet, aber jeder bastelt sich seine eigene type_traits, 
initializer_list etc., aber C++ auf AVR ist nicht so prickelnd, als dass 
jemand richtigen C++ Support hinzufügen würde.

von Rolf M. (rmagnus)


Lesenswert?

Yalu X. schrieb:
> C++ ist eine alte Sprache, da lässt sich so etwas im Nachhinein kaum
> noch ändern. Aber warum ging man bspw. bei C# nicht diesen Weg?

Wird const-Korrektheit dort überhaupt in dem Umfang wie in C++ gelebt?

Wilhelm M. schrieb:
> Rolf M. schrieb:
>> Verwirrend finde ich aber auch, wenn Deklaration und Definition
>> unterschiedlich aussehen.
>
> Finde ich eigentlich nicht: das eine ist die Schnittstelle, das andere
> die Realisierung.
>
> Außerdem ist das auch bei anderen Charakteristiken von Funktionen so:
> z.B. default-Werte, non-/static und Attribute.

static schreibe ich nur bei Klassenmembern nicht dazu, weil es da (warum 
auch immer) verboten ist. Das finde ich recht ärgerlich, da man dann an 
der Definition nicht gleich erkennen kann, dass es sich um eine 
statische Memberfunktion handelt.
Aber das hat weniger damit zu tun, dass es sich um die Definition der 
Funktion handelt, sondern damit, dass es außerhalb der Klassendefinition 
steht. Innerhalb der Klassendefinition ist es ja erlaubt. Also sowas 
geht:
1
class Foo
2
{
3
public:
4
    static void func() { std::cout << "Hello world\n"; }
5
}

Bei Default-Werten halte ich es aber auch für nicht so sinnvoll, die zu 
duplizieren und somit immer an zwei Stellen konsistent zu einander 
halten zu müssen. Aber ja, da ist das ein Unterschied, der mich aber 
irgendwie nicht so stört. Ich kann nicht so genau sagen, warum.

Und warum Attribute bei der Definiton verboten sind, ist mir ebenfalls 
unverständlich. Gerade die sind für den Nutzer der Funktion in der Regel 
weniger interessant. Und wie nervig das ist, sieht man ja auch an deinem 
Code von oben. Da musstest du den ganzen Krempel extra zweimal 
hinschreiben, nur weil das Attribut bei der Definition aus irgendeinem 
Grund verboten ist:
1
template<typename T>
2
void f(const T& v) __attribute__((noinline));
3
4
template<typename T>
5
void f(const T& v) {
6
    x = v / 2;
7
}

von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> static schreibe ich nur bei Klassenmembern nicht dazu, weil es da (warum
> auch immer) verboten ist.

Naja, da bedeutet static ja auch was anderes?

Am besten ist es, die moderne, inline static in-class definition zu 
verwenden. Dann steht es auch da, wo es hingehört.

Rolf M. schrieb:
> Bei Default-Werten halte ich es aber auch für nicht so sinnvoll, die zu
> duplizieren

Sollst Du auch gar nicht: die gehören ja nur in die Deklaration, sonst 
gehts es ja nicht.

Rolf M. schrieb:
> Und warum Attribute bei der Definiton verboten sind, ist mir ebenfalls
> unverständlich.

Ist doch klar: weil das in den meisten Fällen Info für den Caller ist, 
d.h. die Attribute müssen bei der Deklaration sichtbar sein, bei der 
Definition ist es zu spät.

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Rolf M. schrieb:
>> Bei Default-Werten halte ich es aber auch für nicht so sinnvoll, die zu
>> duplizieren
>
> Sollst Du auch gar nicht: die gehören ja nur in die Deklaration, sonst
> gehts es ja nicht.

Das hat nichts damit zu tun, ob es eine Definition ist. Es ist an der 
Stelle erlaubt, an der die Funktion zum ersten mal bekannt gemacht wird, 
egal ob das eine Definition ist oder nicht.
Also:
1
void func(int i = 3)  // erlaubt
2
{
3
4
}
5
6
void func(int i);     // erlaubt, default-Wert ist 3
7
void func(int i = 3); // Fehler, weil default-Wert schon vorher festgelegt
8
                      // Warum kann das nicht erlaubt sein, solange der Wert gleich ist?

> Rolf M. schrieb:
>> Und warum Attribute bei der Definiton verboten sind, ist mir ebenfalls
>> unverständlich.
>
> Ist doch klar: weil das in den meisten Fällen Info für den Caller ist,
> d.h. die Attribute müssen bei der Deklaration sichtbar sein, bei der
> Definition ist es zu spät.

Ok, für manche Attribute gilt das. Aber an sich würde es auch wie oben 
bei den Default-Werten reichen, wenn das Attribut für die erste Stelle, 
an der die Funktion bekannt gemacht wird, erlaubt wäre. Dann wäre bei 
deinem obigen Beispiel die Duplizierung des Funktionskopfs nur für das 
Attribut eben nicht nötig.
Und es würde auch nicht schaden, wenn es an allen Stellen erlaubt wäre, 
solange die Attribute überall die gleichen sind.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Das hat nichts damit zu tun, ob es eine Definition ist. Es ist an der
> Stelle erlaubt, an der die Funktion zum ersten mal bekannt gemacht wird,

Und das ist typischerweise die Deklaration (beachte, eine Definition ist 
auch eine Deklaration).

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.