Hallo zusammen,
hatte letztes eine hitzige Diskussion zum Thema Nutzen und vor allem der
Position von *#define* statements.
Ich bin der Meinung man könnte alles gesammelt am Anfang deklarieren
noch im Global Scope direkt nach dem Changelog Header und den *#include*
Anweisungen.
Mein gegenüber meinte hingegen jedoch das nicht global genutzte #define
statements in z.B den main() Scope müssten.
Aber nun mein Einwand, ich hab dann die Diskussion sein lassen und
wollte erstmal fragen. Ist es nicht so das es dem Compiler egal ist wo
die #define Anweisungen steht und er sie immer gleich behandelt?
Sprich stets den deklarierten Teil im Code einfach damit ersetzt?
Dazu wären mir "echte" Quellen sehr recht damit ich das ganze mal
anständig vom Tisch bringen kann.
Anmerkung: keine der beiden Personen hat C++ wirklich "gelernt"
Danke schon mal
Grüße
Der Präprozessor ist streng genommen eine eigenständiges Programm das
vor dem Compiler ausgeführt wird. Das Teil hat zwar deinen Ursprung in
der C-Umgebung wird aber durchaus auch an anderer Stelle eingesetzt.
Der weiß von eigentlich gar nichts von der Programmiersprache mit der
dessen Ergebnis dann weiterverarbeitet wird und kennt somit sowas wie
main, global, Gültigkeitsbereiche durch geschweifte Klammern usw.
überhaupt nicht.
https://de.wikipedia.org/wiki/C-Präprozessor
Moin,
Dein Gegenüber ist ein gutes Beispiel dafür, warum Konstanten in dieser
Form (#define) heute als 'bad practice' gelten.
"... nicht global genutzte ... in den main() Scope ..."
Da ist eines der Probleme mit #define.
Sie sind immer global und interessieren sich für Deine C++ Strukturen
überhaupt nicht. Letztlich handelt es sich hierbei um Platzhalter, die
vom Preprozessor ganz stumpf ersetzt werden, bevor der Compiler
übernimmt. Geschweifte Klammern sind ihm dabei völlig wumpe.
...wobei es natürlich auch ein bisschen drauf ankommt, wofür das #define
nun genau verwendet wird.
Gegen Konstrukte wie:
1
#ifndef MYFILE_H
2
#define MYFILE_H
3
...
4
#endif
spricht aus meiner Sicht nix.
Aber:
1
#include <stdio.h>
2
3
#define WHATEVER 5
4
5
void doSomething();
6
7
int main(int argc, char* argv[]) {
8
#define PI 3.14
9
doSomething();
10
return 0;
11
}
12
13
void doSomething() {
14
printf("PI = %.5f\n",PI);
15
printf("WHATEVER = %.5f\n",WHATEVER);
16
}
Ratsamer wäre es in diesen Fällen, mit
1
const double pi = 3.14;
zu arbeiten. Denn hier hast Du auch einen Scope, Namespaces, einen
Datentyp und bei Compiler-Fehlern eine vernünftige Aussage zur
Fehlerursache.
Na jedenfalls liegst Du mit Deiner Meinung richtig. Der Scope spielt
hier keine Rolle und ist auch kein Argument.
Man könnte allenfalls überlegen, ob ein #define in der Nähe seiner
Verwendungsstellen besser aufgehoben ist als am Anfang der Datei...
Wegen der Lesbarkeit.
Thomas B. schrieb:> Hallo zusammen,> hatte letztes eine hitzige Diskussion zum Thema Nutzen und vor allem der> Position von *#define* statements.
#define ist kein Statement, sondern eine Präprozessor-Direktive.
> Ist es nicht so das es dem Compiler egal ist wo die #define Anweisungen> steht und er sie immer gleich behandelt? Sprich stets den deklarierten> Teil im Code einfach damit ersetzt?
Ja. Der Präprozessor macht nur reine Textersetzung, vollkommen
unabhängig von der Struktur des Programms.
> Dazu wären mir "echte" Quellen sehr recht damit ich das ganze mal> anständig vom Tisch bringen kann.> Anmerkung: keine der beiden Personen hat C++ wirklich "gelernt"
Die beste Quelle ist die ISO-Norm. Die kann man sich als Draft
runterladen. Hier zum Beispiel:
https://web.archive.org/web/20181230041359if_/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf
Dort ist in Kapitel 5.1.1.2 gelistet, welche Übersetzungsphasen es gibt
und in welcher Reihenfolge sie bearbeitet werden. Das Auflösen von
Makros ist in Schritt 4, d.h. danach gibt es die Makros nicht mehr. Erst
in Schritt 7 passiert dann die semantische Analyse, die so Dinge wie
Scope berücksichtigt.
Andresen schrieb:> Ratsamer wäre es in diesen Fällen, mit> const double pi = 3.14;>> zu arbeiten. Denn hier hast Du auch einen Scope, Namespaces, einen> Datentyp und bei Compiler-Fehlern eine vernünftige Aussage zur> Fehlerursache.
Das ist aber in C keine echte Compilezeit-Konstante. Das einzige, was in
C eine echte Konstante ist, ist der direkt hingeschriebene Zahlenwert.
Deshalb ist ja das #define so populär dafür geworden. Damit kann man dem
Wert einen Namen geben, und der Präprozessor setzt per Textersetzung vor
dem eigentlichen Compilerlauf die Zahl dafür ein.
Random .. schrieb:> willst du Konstanten im Namespace einer Klasse haben, deklariere sie als> "enum" ("typedef enum" macht man in C++ nicht :-) ) innerhalb der> Klasse:
enum ist für Aufzählungstypen, nicht für Konstanten. Dein Beispiel zeigt
ja auch die Verwendung als Aufzählungstp. Ich würde aber nicht sowas
schreiben:
Andresen schrieb:> Man könnte allenfalls überlegen, ob ein #define in der Nähe seiner> Verwendungsstellen besser aufgehoben ist als am Anfang der Datei...> Wegen der Lesbarkeit.
Nur steht dem mehrere Jahrzehnte an Konvention entgegen Defines am
Anfang einer Datei zu schreiben. Für den erfahrenen Programmierer
verringerst du die Lesbarkeit mit Defines in der Nähe der
Verwendungsstellen weil sie dort nicht erwartet werden.
Vieles in C beruht darauf dass sich der Programmierer zusammen reißen
kann und nicht alles macht was die Sprache hergibt.
C++ ist da auch keine Lösung. Mit jeder Release kommen neue super,
sonder, spezial, fünf mal um die Ecke gedachte, mit abartiger Syntax in
die Sprache gezwängte neue Konstrukte hinzu. Auch wenn es die Meisten
nicht zugeben, es gibt kaum einen C++ Programmierer der wirklich den
gesamten aktuellen Sprachumfang von C++ beherrscht.
Rolf M. schrieb:> Das ist aber in C keine echte Compilezeit-Konstante.
Hmm ... das mag sein.
Aber letztendlich macht ein vernünftiger Compiler genau das draus. Er
sieht, dass der Wert sich nicht ändern kann und wird ihn in der weiteren
Verwendung wie bei einem #define verarbeiten. Das ist aber keine
zugesicherte Eigenschaft. Insofern nicht echt, effektiv aber doch.
Genau so, wie der Compiler für den Multiplikator daraus eine Zahl macht
(machen sollte). Mit dem Vorteil, dass statt der magic number was
verständliches geschrieben wurde.
1
returnnextFetch*60*60*24;
Ja, "echt", "zugesichert" und "so implementiert" sind verschiedene
Sachen.
Nick M. schrieb:> Rolf M. schrieb:>> Das ist aber in C keine echte Compilezeit-Konstante.>> Hmm ... das mag sein.>> Aber letztendlich macht ein vernünftiger Compiler genau das draus. Er> sieht, dass der Wert sich nicht ändern kann und wird ihn in der weiteren> Verwendung wie bei einem #define verarbeiten. Das ist aber keine> zugesicherte Eigenschaft. Insofern nicht echt, effektiv aber doch.
Naja, man kann es z.B. nicht als Array-Größe verwenden oder in #ifdef.
Es ist also effektiv nicht 100%ig das gleiche. Und wenn man es auf
globaler Ebene definiert, müsste man es static machen, wenn man nicht
möchte, dass es als Symbol exportiert wird und Speicher braucht, auf den
man aber eigentlich nie zugreift, weil der Wert direkt eingesetzt wird.
Mache ich es aber static und stecke es in einen Header, dann bekomme ich
überall, wo der Header eingebunden ist, die Konstante aber nicht
verwendet wird, die Warnung, dass sie unbenutzt ist.
> Genau so, wie der Compiler für den Multiplikator daraus eine Zahl macht> (machen sollte).
Im Prinzip machen muss, denn das Ergebnis kann ich als Array-Größe
verwenden, muss also auch schon zur Compilezeit feststehen.
Andresen schrieb:> Man könnte allenfalls überlegen, ob ein #define in der Nähe seiner> Verwendungsstellen besser aufgehoben ist als am Anfang der Datei...> Wegen der Lesbarkeit.
Wenn es nur eine Verwendungsstelle gibt, ist das #define nicht nur
überflüssig, sondern verwirrend (wo wird das noch gebraucht?). Wenn es
mehrere Verwendungsstellen gibt, kann das #define nicht in der Nähe
stehen.
Fazit: #defines sind böse, außer vielleicht, wenn der gleiche Quelltext
in unterschiedlichen Konfigurationen verwendet werden muss. Und auch das
kann man leicht falsch machen, weil der cpp undefined wie 0 behandelt.
Bauform B. schrieb:> Wenn es nur eine Verwendungsstelle gibt, ist das #define nicht nur> überflüssig, sondern verwirrend
Nicht wirklich, Stichwort "Magic Numbers". Der Code wird lesbarer, und
man spart sich Kommentare.
1
for(inti=0;i<128;i++){...}// iterate up to maximum number of elements
2
for(inti=0;i<MAX_ELEMENTS;i++){...}
Ausserdem erspart es einem von vornherein Arbeit beim zukünftigem Umbau
sowie bei Erweiterungen.
Ja, in #ifdef kann man das nicht mehr weiterverarbeiten.
Und ja, der Mist an den #defines ist, dass sie keinen Scope haben.
Ich mach gelegentlich innerhalb einer Funktion ein #define und an deren
Ende ein #undef. Ist aber auch Mist, weil man das #undef vergessen kann
und das Thema scope auch nicht wirklich gelöst ist.
Random .. schrieb:> Bauform B. schrieb:>> Wenn es nur eine Verwendungsstelle gibt, ist das #define nicht nur>> überflüssig, sondern verwirrend>> Nicht wirklich, Stichwort "Magic Numbers". Der Code wird lesbarer, und> man spart sich Kommentare.
Dann ist es genau das Gegenteil von verwirrend: Es versieht eine
verwirrende erstmal zufällig wirkende Zahl mit einem aussagekräftigen
Namen. Das ist unabhängig davon, ob es nur einmal oder mehrmals
verwendet wird.
Nick M. schrieb:> Rolf M. schrieb:>> Naja, man kann es z.B. nicht als Array-Größe verwenden oder in #ifdef.>> Meintest du sowas? const int bufferSize = 100;> char buffer[bufferSize];> snprintf(buffer, bufferSize, "LinkBoardTask: %s %d\n",> appData.rcvMsg.linkSlotTask.pName, mailBox);
kein Problem:
Thomas B. schrieb:> Anmerkung: keine der beiden Personen hat C++ wirklich "gelernt"
In C++ gibt es eigentlich keinen sinnvollen Grund für
#define-Konstanten.
Oliver
Bauform B. schrieb:> kein Problem:
Naja, das ist nur eine Umkehrung. Und wenn ich einen zweiten Puffer
brauch der doppelt so groß sein soll wie bufferSize?
Also dein Gegenbeispiel funktioniert, aber ich empfinde es als "von
hinten durch die Brust ins Auge".
Ist aber nur meine Meinung dazu.
Thomas B. schrieb:> Ich bin der Meinung man könnte alles gesammelt am Anfang deklarieren> noch im Global Scope direkt nach dem Changelog Header und den *#include*> Anweisungen.>> Mein gegenüber meinte hingegen jedoch das nicht global genutzte #define> statements in z.B den main() Scope müssten.
Deine Begriffe sind verwirrend. Ich beschränke mich mal auf C.
Normalerweise hast Du
* mehrere .c-Dateien (a.c, b.c, ....), wovon z.b. eine auch main()
enthält.
* mehrere .h-Dateien, die alles enthalten, was von mehreren
.c-Dateien genutzt wird
Jede C-Datei includiert wenn nötig .h-Dateien und wird einzeln für sich
übersetzt.
Dann sind nun folgende Positionen für #define zu unterscheiden:
(0 im Makefile / Buildumgebung, wirken als ständen sie in der ersten
Zeile)
1) in einer .h-Datei --> in allen .c-Dateien ab #include wirksam (und in
manchen .h-Dateien, die danach kommen)
2) in einer .c-Datei vor den #includes --> sind auch in den .h-Dateien
wirksam, z.b. um Module ein/auszuschalten (#define USE_XY 1)
3) irgendwo am Anfang (nach den #includes) --> im "ganzen" c-File gültig
4) irgendwo mitten im Code, z.B. vor der 7ten Funktion --> ab der 7ten
Funktion gültig
5) innerhalb eines kleinen Bereichs, mit einem #undef am Ende --> nur
genau dazwischen gültig, das gleiche #define kann dann vorher oder
nachher nochmal genauso verwendet werden.
Alle 6 Fälle haben ihre Vorteile oder Berechntigung. Für die meisten
hier (vor allem C++ler) sind alle verpönt, für andere nur die letzten
beiden. Aber das ist ein anderes Thema.
Nick M. schrieb:> Meintest du sowas? const int bufferSize = 100;> char buffer[bufferSize];> snprintf(buffer, bufferSize, "LinkBoardTask: %s %d\n",> appData.rcvMsg.linkSlotTask.pName, mailBox);
In C geht sowas dank VLAs, allerdings nur bei lokalen Variablen, nicht
bei globalen. Außerdem sind VLAs optional. Es ist also nicht garantiert,
dass das geht.
In C++ ist es kein Problem, weil da bufferSize eine echte Konstante ist.
> Und ja, der Mist an den #defines ist, dass sie keinen Scope haben.>> Ich mach gelegentlich innerhalb einer Funktion ein #define und an deren> Ende ein #undef. Ist aber auch Mist, weil man das #undef vergessen kann> und das Thema scope auch nicht wirklich gelöst ist.
In der Praxis habe ich damit aber selten ein Problem. Ich habe keine so
riesigen C-Files, dass ich über die dort verwendeten #defines keinen
Überblick mehr hätte. Und wenn ich versuche, eins zweimal zu definieren,
bricht der Compiler eh mit Fehler ab.
In Headern sollte man natürlich vorsichtiger damit umgehen.
Bauform B. schrieb:> kein Problem:> char buffer[100];> const int bufferSize = sizeof buffer / sizeof buffer[0];> snprintf(buffer, bufferSize, "LinkBoardTask: %s %d\n",> appData.rcvMsg.linkSlotTask.pName, mailBox);
So, und jetzt will ich double buffering machen, brauche also nochmal
einen zweiten Buffer in der selben Größe.
Bauform B. schrieb:> const int bufferSize = sizeof buffer / sizeof buffer[0];
"sizeof buffer[0]" ist relativ sinnlos, denn das ist immer 1, da
"sizeof(char)" immer 1 ist.
Rolf M. schrieb:> In der Praxis habe ich damit aber selten ein Problem. Ich habe keine so> riesigen C-Files, dass ich über die dort verwendeten #defines keinen> Überblick mehr hätte.
Schön für dich... Wenn man mal mit einer großen Codebasis hantiert (z.B.
AOSP-Code) dann verflucht man soetwas.
Michael Gugelhupf schrieb:> C++ ist da auch keine Lösung. Mit jeder Release kommen neue super,> sonder, spezial, fünf mal um die Ecke gedachte, mit abartiger Syntax in> die Sprache gezwängte neue Konstrukte hinzu.
Die musst du ja nicht benutzen. Freue dich doch stattdessen, dass C++
Alternativen für dumme Makro-Textersetzung bietet, und nutze diese.
Außerdem sind viele dieser komplexeren Konstrukte hauptsächlich für
Low-Level-Bibliotheken (insb. die Standard-Bibliothek) gedacht; in
normalem Anwendungs-Code braucht man sich darum nicht zu kümmern.
Programmierer schrieb:> Bauform B. schrieb:>> const int bufferSize = sizeof buffer / sizeof buffer[0];>> "sizeof buffer[0]" ist relativ sinnlos, denn das ist immer 1, da> "sizeof(char)" immer 1 ist.
Gilt aber nur, solange der Elementtyp char ist. Wenn man den mal ändert,
fällt man auf die Nase, wenn man das sizeof buffer[0] weggelassen hat.
Da es auch bei char keinen Schaden anrichtet und dieses sizeof buffer /
sizeof buffer[0] der übliche Konstrukt ist, um die Zahl der Elemente
eines Arrays zu ermitteln, würde ich es auch in diesem Fall
hinschreiben.
> Rolf M. schrieb:>> In der Praxis habe ich damit aber selten ein Problem. Ich habe keine so>> riesigen C-Files, dass ich über die dort verwendeten #defines keinen>> Überblick mehr hätte.>> Schön für dich... Wenn man mal mit einer großen Codebasis hantiert (z.B.> AOSP-Code) dann verflucht man soetwas.
Klingt irgendwie nach Murks. Aber manchmal muss man mit sowas halt
arbeiten. Das kann ich schon verstehen.
> Michael Gugelhupf schrieb:>> C++ ist da auch keine Lösung. Mit jeder Release kommen neue super,>> sonder, spezial, fünf mal um die Ecke gedachte, mit abartiger Syntax in>> die Sprache gezwängte neue Konstrukte hinzu.>> Die musst du ja nicht benutzen. Freue dich doch stattdessen, dass C++> Alternativen für dumme Makro-Textersetzung bietet, und nutze diese.> Außerdem sind viele dieser komplexeren Konstrukte hauptsächlich für> Low-Level-Bibliotheken (insb. die Standard-Bibliothek) gedacht; in> normalem Anwendungs-Code braucht man sich darum nicht zu kümmern.
Und viele andere neue Konstrukte vereinfachen es eher. Die alten sollte
man eigentlich vermeiden, weil sie komplizierter und fehleranfälliger
sind. Die neue Syntax ist halt nötig, weil man die alten Konstrukte aus
Kompatibilitätsgründen drin lässt. Das führt aber eben auch dazu, dass
man die alten weiterhin verwenden kann, wenn man die neuen nicht lernen
will.
Rolf M. schrieb:>> Und ja, der Mist an den #defines ist, dass sie keinen Scope haben.>>>> In der Praxis habe ich damit aber selten ein Problem. Ich habe keine so> riesigen C-Files,
YMMV. :-)
Aber eine einfache Lösung für den Scope von #defines in C ist es, die
geschickt zu benamsen.
In "DoSomething.c" alle #defines mit "DoSomething" beginnen. Also z.B.
"#define DoSomethingMaxEntries 33"
So kann man zumindest üble Nebeneffekte vermeiden.
Programmierer schrieb:> Die musst du ja nicht benutzen.
Das Problem ist ein anderes. Sie werden benutzt.
Wenn du Code hast der über ein paar Jährchen von wechselnden Teams und
Programmierern weiterentwickelt und gewartet wird, dann hast du
irgendwann alles drin. Der Code wird immer weniger wartbar weil du keine
Programmierer mehr findest die alles verstehen.
Das multipliziert sich damit, dass Code der lange weiterentwickelt und
gewartet wird sowie so schon ausufert und kaputt gehackt ist.
> Außerdem sind viele dieser komplexeren Konstrukte hauptsächlich für> Low-Level-Bibliotheken (insb. die Standard-Bibliothek) gedacht; in> normalem Anwendungs-Code braucht man sich darum nicht zu kümmern.
Leider eben doch. Weil immer irgendeiner irgendwann auf die Idee kommt
ein bestimmtes Konstrukt einzubauen. Das ist, muss man leider sagen, oft
dem Ego der Programmierern geschuldet, die zeigen wollen was sie anderen
Programmierern überlegen sind.
Rolf M. schrieb:> Klingt irgendwie nach Murks.
Große Software-Projekte sind Murks? Oder nur AOSP? Das schon.
Rolf M. schrieb:> Gilt aber nur, solange der Elementtyp char ist. Wenn man den mal ändert,> fällt man auf die Nase, wenn man das sizeof buffer[0] weggelassen hat.
Das stimmt. In C++ könnte man einfach std::size(buffer) machen, und egal
welcher Container-Typ das ist, kommt immer die richtige Größe raus...
Michael Gugelhupf schrieb:> Der Code wird immer weniger wartbar weil du keine> Programmierer mehr findest die alles verstehen.
Ja. Aber das ist mit allen Sprachen so. Die Lösung kann nicht sein, auf
dem Technologiestand von Anno-Dazumal stehen zu bleiben, nur damit
überall ein konsistent (antiker) Sprachlevel benutzt wird. Man kann neue
Komponenten mit neuen Mitteln implementieren, und alte Komponenten nach
und nach aktualisieren.
Michael Gugelhupf schrieb:> Das ist, muss man leider sagen, oft> dem Ego der Programmierern geschuldet, die zeigen wollen was sie anderen> Programmierern überlegen sind.
Das ist ein Management-Problem und kein Programmiersprachen-Problem.
C-Programmierer verwendet auch gerne mal wilde Konstrukte mit Makros
oder "void***". Das ist auch nicht besser.
Ich würde sagen, es kommt darauf an, wofür und wo man das Makro braucht.
Manchmal hab ich Makros, die brauch ich nur um an einer Stelle etwas
Schreibarbeit zu sparen. z.B.
1
voidsomefunction(size_ts,Tx[s]);
2
voidsomething(){
3
#define S(X) sizeof(X)/sizeof(*(X)), (X)
4
somefunction(S((X){{1},{2}}));
5
somefunction(S((X){{3},{5},{7},{8}}));
6
#undef S
7
}
In dem fall schreib ich das entweder oben ins C file, wenn ich es an
mehreren stellen darin brauche, oder direkt an den Anfang der Funktion,
wo ich es brauche, und undefiniere es nachher wieder. Wenn ich es aber
in vielen Dateien brauche, kommt es in eine Headerdatei für utility
macros, aber dann bekommt es einen langen, deskriptiven Namen.
Manchmal hab ich auch ein Makro, das einen Funktionsaufruf aufhübscht,
oder sonstwie zu einer Schnittstelle gehört. Die packe ich in die selbe
header Datei, wie der rest zu dem sie gehört.
Und dann hab ich noch die Codegenerierungsmakros, bei denen ich ein
Template habe, das ich mehrfach verwende, um unterschiedlichen code zu
generieren. Im einfachsten fall sieht das z.B. so aus:
something.h
Programmierer schrieb:> Rolf M. schrieb:>> Klingt irgendwie nach Murks.>> Große Software-Projekte sind Murks? Oder nur AOSP? Das schon.
Software-Projekte, bei denen man in einem einzelnen C-File die Übersicht
über die darin definierten Makros verliert.
DPA schrieb:> Manchmal hab ich Makros, die brauch ich nur um an einer Stelle etwas> Schreibarbeit zu sparen. z.B.
Gerade dieser Fall lässt sich in C++ z.B. so umsetzen:
1
#include<cstddef>
2
#include<iostream>
3
#include<initializer_list>
4
5
structX{
6
inta;
7
};
8
9
voidf(std::size_ts,constX*x){
10
while(s--){
11
std::cout<<(x++)->a<<",";
12
}
13
std::cout<<std::endl;
14
}
15
16
// Variante 1
17
template<std::size_tN>
18
inlinevoidf(constX(&x)[N]){
19
returnf(N,x);
20
}
21
22
// Variante 2
23
inlinevoidf(std::initializer_list<X>l){
24
f(l.size(),l.begin());
25
}
26
27
intmain(){
28
f({{1},{2},{3}});
29
}
Komplett ohne Makros.
DPA schrieb:> Und dann hab ich noch die Codegenerierungsmakros, bei denen ich ein> Template habe, das ich mehrfach verwende, um unterschiedlichen code zu> generieren.
Dafür sind Makros schon eher nötig; Bezeichner lassen sich in C++ auch
nicht generieren...
Rolf M. schrieb:> Software-Projekte, bei denen man in einem einzelnen C-File die Übersicht> über die darin definierten Makros verliert.
Wenn jedes File (indirekt) hunderte andere Files inkludiert, die alle
möglichen Makros definieren, kommt man schonmal durcheinander.
Außer für Include-Guards braucht man keine #defines mehr.
Das meiste geht auch mit z.B.
static const int aaa=17
für Konstanten oder Enums für Aufzählungen.
Und ansonsten nimmt man gleich Funktionen, die optimiert der Compiler
auch ganz gut.
Michael Gugelhupf schrieb:> C++ ist da auch keine Lösung. Mit jeder Release kommen neue super,> sonder, spezial, fünf mal um die Ecke gedachte, mit abartiger Syntax in> die Sprache gezwängte neue Konstrukte hinzu. Auch wenn es die Meisten> nicht zugeben, es gibt kaum einen C++ Programmierer der wirklich den> gesamten aktuellen Sprachumfang von C++ beherrscht.
Also das ist ein ziemlich blödsinniges Argument. Man kann durchaus in
C++ Programme schreiben, ohne alle in C++ verfügbaren Konstrukte
einzusetzen. Dann nimmt man halt das, was man kennt. Je nachdem ist man
eh gezwungen, sich auf ein Subset von C++ zu beschränken (SIL, Misra
...), wenn man sich für seine Arbeit bezahlen lassen möchte.
Bauform B. schrieb:> PittyJ schrieb:>> Außer für Include-Guards braucht man keine #defines mehr.>> Dagegen gäbe es #pragma once -- aber was nimmt statt #if CONFIG_FOO?
Templates und constexpr-if
Programmierer schrieb:> Dafür sind Makros schon eher nötig; Bezeichner lassen sich in C++ auch> nicht generieren...
Wenn die übliche TMP nicht mehr ausreicht, kann man auf die nächste
Ebene aufsteigen mit "Circle": ein C++-Compiler mit einem
Meta-C++-Interpreter, Reflection und Introspection.
Wenn man das "richtig" mit templates macht, hat das noch ein paar
Vorteile:
- Man kann mehrere Config-Varianten in der selben Source Datei/Projekt
kompilieren, bzw. muss nicht mit mehreren unterschiedlichen Sätzen an
'-D' Flags für den Compiler hantieren
- Dadurch kann man zur Laufzeit zwischen Varianten umschalten
- Es lässt sich leichter testen weil man templates besser "mocken" kann
- Man kann die Konfigurationseinstellungen zusammen mit anderen Arten
von Einstellungen oder C++ Konstanten verwenden; bei "#if FOO" muss
alles durch den Präprozessor
Der Hauptnachteil ist aber dass es deutlich unintuitiver und mehr
Schreibarbeit ist. Es kann auch die Kompilation selbst verlangsamen. Das
ist aber alles stark vom Einzelfall abhängig.
Ich bin ja nun wirklich für den Einsatz von templates (was hier sicher
jeder weiß), aber:
Programmierer schrieb:> Wenn man das "richtig" mit templates macht, hat das noch ein paar> Vorteile:> - Man kann mehrere Config-Varianten in der selben Source Datei/Projekt
ist doch bei #ifdef nicht anders
> kompilieren, bzw. muss nicht mit mehreren unterschiedlichen Sätzen an> '-D' Flags für den Compiler hantieren
da wird man wohl drum herum kommen, denn wie willst Du sonst "von außen"
zur Compilezeit Alternativen auswählen, die sich nicht aus der Umgebung
durch den Compiler selbst ableiten lassen.
> - Dadurch kann man zur Laufzeit zwischen Varianten umschalten
templates bedeutet statische Polymorphie, und die findest zur
Compilezeit statt. Also kein Laufzeitkonstrukt.
> - Es lässt sich leichter testen weil man templates besser "mocken" kann> - Man kann die Konfigurationseinstellungen zusammen mit anderen Arten> von Einstellungen oder C++ Konstanten verwenden; bei "#if FOO" muss> alles durch den Präprozessor
Was meinst Du denn damit?
> Der Hauptnachteil ist aber dass es deutlich unintuitiver und mehr> Schreibarbeit ist.
Würde ich so nicht bestätigen.
> Es kann auch die Kompilation selbst verlangsamen.
Solange es keine stark rekursiven templates sind (was bei einfachen
std::conditional_t<> so ist) wohl kaum.
Programmierer schrieb:> Dafür sind Makros schon eher nötig; Bezeichner lassen sich in C++ auch> nicht generieren...
Manchmal muss man Bezeichner auch gar nicht explizit generieren. Gutes
Beispiel dafür ist std::tuple. Man würde sich zwar im ersten Moment
wünschen, dass man Bezeichner generieren kann, aber wie in TMP üblich,
kann man die Iteration, um die Bezeichner für die Datenelemente zu
generieren, durch rekursive Vererbung ersetzen.
Klummel 6. schrieb:> ich finde folgende Beschreibung immer wieder gut:>> "static const" vs "#define" vs "enum"> https://stackoverflow.com/a/1674459/2931984
in der Tat, viele Argumente, jetzt kann ich mein Programm zur
#define-freien Zone erklären -- bis auf eine Anwendung, die dort und
hier kaum erwähnt wird. Gibt es dafür auch eine Alternative? Zum
Beispiel
Bauform B. schrieb:> jetzt kann ich mein Programm zur> #define-freien Zone erklären
if (false) {
...
(Ersetze 'false' durch was vernünftiges).
Jeder vernünftige Compiler macht 'dead-code-elimination'
Nick M. schrieb:> if (false) {
faszinierend. Es ist kein 1:1 Ersatz weil man mit #if auch "case 'f':"
eliminieren kann. Dadurch werden hier ca. 2 Maschinenbefehle mehr
erzeugt und deshalb wird ein simples diff mühsam.
Aber: Variablen, die nur in der Float-Version gebraucht werden kann
immer drin lassen, die werden restlos weg optimiert. Und der Rest
scheint Bit für Bit identisch zu sein. Vollkommen. Irre.
Ist das dann schon Obfuscated C oder versteht das jeder?
Programmierer schrieb:> template <typename... Args>Wilhelm M. schrieb:> Sagte ich doch:
Ihr immer mit eurem gedopten C, ich bin froh, wenn ich allmählich if und
#if unterscheiden kann ;)
Bauform B. schrieb:> Es ist kein 1:1 Ersatz weil man mit #if auch "case 'f':"
Ja, das stimmt!
Ich hab seine Frage aber eher als allgemein aufgefasst.
Wäre trotzdem interessant, was der Compiler aus einem leeren case macht.
Nick M. schrieb:> Wäre trotzdem interessant, was der Compiler aus einem leeren case macht.
Das ist doch von der Sprache her klar geregelt. In diesem speziellen
Fall bleibt als Quelltext sowas übrig, also ein total leerer case und %f
wird als hex formatiert:
1
case'd':
2
// dezimal konvertieren
3
break;
4
case'f':
5
case'x':
6
// hex konvertieren
7
break;
8
default:
9
// falschen format-string kopieren
Ein nicht ganz so leerer case mit einem break; zwischen 'f' und 'x' ist
ebenso eindeutig, es ist praktisch ein Sprung hinter switch() und es
passiert garnichts. Dieser Sprung wird manchmal als cmp und beq (o.ä.)
übersetzt und manchmal als tbb plus Tabelle mit Zieladressen. In dem
Fall verschwindet der leere case völlig in Tabelle. Eine dritte Variante
ist ein ldr pc aus einer Adresstabelle, also praktisch ein tbb ohne
Grenzen.
Wann der gcc eine Tabelle generiert und wann einzelne Abfragen hängt
natürlich vor allem davon ab, wie groß und wie dicht besetzt die Tabelle
wird. Wahrscheinlich spielen noch tausend andere Kleinigkeiten wie
Registermangel eine Rolle.