Forum: PC-Programmierung [C - C++] wohin gehört das #define


von H2 O. (Gast)


Lesenswert?

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

von Eric (Gast)


Lesenswert?

Sowohl cpp als auch der Compiler liest die Datei von oben bis nach 
unten. Damit ist es egal, wo die Anweisung steht.

von Irgend W. (Firma: egal) (irgendwer)


Lesenswert?

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

von Andresen (Gast)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

: Bearbeitet durch User
von Random .. (thorstendb) Benutzerseite


Lesenswert?

Hi,

willst du Konstanten im Namespace einer Klasse haben, deklariere sie als 
"enum" ("typedef enum" macht man in C++ nicht :-) ) innerhalb der 
Klasse:
1
class Types {
2
public:
3
  Types() {}
4
  ~Types() {}
5
6
  enum usage_e { 
7
    USAGE_UNDEF=0, 
8
    USAGE_READ, 
9
    USAGE_WRITE, 
10
    USAGE_READWRITE 
11
  };
12
};

ausserhalb der Klasse dann:
1
Types::usage_e usage = Types::USAGE_READ;

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

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:
1
enum { size = 100 } size_enum;
2
int array[size];

Und ein
1
enum { pi = 3.14 } pi_enum;
geht schlicht gar nicht.

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

Rolf M. schrieb:
> Die beste Quelle ist die ISO-Norm. Die kann man sich als Draft
> runterladen. Hier zum Beispiel:

Die ist allerdings nicht einfach zu lesen.

Für solch grundlegende Dinge reicht auch jedes x-beliebige C-Buch, egal, 
ob off- oder online.

http://openbook.rheinwerk-verlag.de/c_von_a_bis_z/010_c_praeprozessor_002.htm#mjead11e16556a0e94647592cad9e3c437

Oliver

von Michael Gugelhupf (Gast)


Lesenswert?

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.

von Nick M. (Gast)


Lesenswert?

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
 return nextFetch * 60 * 60 * 24;

Ja, "echt", "zugesichert" und "so implementiert" sind verschiedene 
Sachen.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Bauform B. (bauformb)


Lesenswert?

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.

von Random .. (thorstendb) Benutzerseite


Lesenswert?

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(int i=0; i<128; i++) { ... }   // iterate up to maximum number of elements
2
for(int i=0; i<MAX_ELEMENTS; i++) { ... }

Ausserdem erspart es einem von vornherein Arbeit beim zukünftigem Umbau 
sowie bei Erweiterungen.

: Bearbeitet durch User
von Nick M. (Gast)


Lesenswert?

Rolf M. schrieb:
> Naja, man kann es z.B. nicht als Array-Größe verwenden oder in #ifdef.

Meintest du sowas?
1
  const int bufferSize = 100;
2
  char buffer[bufferSize];
3
  snprintf(buffer, bufferSize, "LinkBoardTask: %s %d\n", appData.rcvMsg.linkSlotTask.pName, mailBox);

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Bauform B. (bauformb)


Lesenswert?

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:
1
  char buffer[100];
2
  const int bufferSize = sizeof buffer / sizeof buffer[0];
3
  snprintf(buffer, bufferSize, "LinkBoardTask: %s %d\n", appData.rcvMsg.linkSlotTask.pName, mailBox);

von Oliver S. (oliverso)


Lesenswert?

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

von Nick M. (Gast)


Lesenswert?

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.

von A. S. (Gast)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Nick M. (Gast)


Lesenswert?

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.

von Michael Gugelhupf (Gast)


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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.

von DPA (Gast)


Lesenswert?

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
void somefunction(size_t s, T x[s]);
2
void something(){
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
1
// something.h
2
#define BLA_BLA_SOMETHING_SOMETHING \
3
  X(abra, "kadabra", 10)
4
  X(simsala, "bim", 99)
5
6
#define X(A,B,C) bla_bla_index_ ## A,
7
enum { BLA_BLA_SOMETHING_SOMETHING }
8
#undef X
9
10
extern struct bla_bla_entry bla_bla_list[];
11
extern size_t bla_bla_count;

something.c
1
#include <something.h>
2
3
#define X(A,B,C) { .name=(B), .number=(C) },
4
struct bla_bla_entry bla_bla_list[] = { BLA_BLA_SOMETHING_SOMETHING  };
5
size_t bla_bla_count = sizeof(bla_bla_list) / sizeof(*bla_bla_list);
6
#undef X

Und dann hab ich manchmal noch eigentlich das selbe wie oben, aber so, 
dass ich die zugrundeliegenden Daten ändere, die Codegenerierung in ein 
File auslagere, was ich includiere und darin das Makro wieder aufhebe, 
und je nach nem weiteren Macro daraus code für ne C oder ne H datei 
mache. Mach ich z.B. hier:
https://github.com/Daniel-Abrecht/dpaw/tree/master/wm/include/dpaw/atom
https://github.com/Daniel-Abrecht/dpaw/blob/master/wm/include/dpaw/atom.template
https://github.com/Daniel-Abrecht/dpaw/blob/master/wm/src/dpawindow/app.c
https://github.com/Daniel-Abrecht/dpaw/blob/master/wm/src/atom.c#L11

Dort hab ich listen verwendeter X11 Atoms. Wenn ich es inkludiere, hab 
ich extern Referenzen für die dazugehörenden IDs und Infos wie den Namen 
des Atoms, usw. Kompiliere ich eine der Dateien in include/dpaw/atom, 
bekomm ich den Code, der alle infos mit einer linked list zusammenhängt. 
Und beim starten initialisiert die atom.c alle elemente der Liste (fragt 
die ID ab), usw.

Es gibt vermutlich noch andere Anwendungsfälle für macros, aber das is 
wie und wo ich die normalerweise so verwende.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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
struct X {
6
  int a;
7
};
8
9
void f(std::size_t s, const X* x) {
10
  while (s--) {
11
    std::cout << (x++)->a << ",";
12
  }
13
  std::cout << std::endl;
14
}
15
16
// Variante 1
17
template <std::size_t N>
18
inline void f (const X (&x) [N]) {
19
  return f(N, x);
20
}
21
22
// Variante 2
23
inline void f(std::initializer_list<X> l) {
24
  f (l.size (), l.begin());
25
}
26
27
int main () {
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.

von PittyJ (Gast)


Lesenswert?

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.

von Bauform B. (bauformb)


Lesenswert?

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?

von DPA (Gast)


Lesenswert?

Bauform B. schrieb:
> aber was nimmt statt #if CONFIG_FOO?
1
int main(){
2
  extern void foo(void) __attribute__((weak));
3
  if(foo)
4
    foo();
5
}

von C++ Programmierer (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

Und das alles nur, um sich ein paar simple Defines zu sparen?

von Programmierer (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

Ich weiss, schon paar Tage her, aber ich finde folgende Beschreibung 
immer wieder gut:

"static const" vs "#define" vs "enum" 
https://stackoverflow.com/a/1674459/2931984

von Bauform B. (bauformb)


Lesenswert?

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
1
int vprintf (const char *fmt, va_list ap) {
2
(...)
3
  switch (*fmt) {
4
  case 'd':
5
(...)
6
#if PRINTF_USE_FLOAT
7
   case 'f':
8
      f = va_arg (ap, double);
9
      if (f < 0) {
10
         *p++ = '-';
11
         f = -f;
12
      }
13
      p = ftoa (p, f, precision);
14
      break;
15
#endif
16
   case 'p':

von Nick M. (Gast)


Lesenswert?

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'

von Programmierer (Gast)


Lesenswert?

Oder so in der Art:
1
#include <type_traits>
2
#include <utility>
3
#include <cassert>
4
5
int printfWithFloatSupport (const char* fmt, ...) {
6
  return 1;
7
}
8
9
int printfWithoutFloatSupport (const char* fmt, ...) {
10
  return 2;
11
}
12
13
template <typename... Args>
14
int cleverprintf (const char* fmt, Args&&... args) {
15
  if constexpr ((std::is_floating_point_v<std::remove_reference_t<Args>> || ...)) {
16
    return printfWithFloatSupport (fmt, std::forward<Args> (args)...);
17
  } else {
18
    return printfWithoutFloatSupport (fmt, std::forward<Args> (args)...);
19
  }
20
}
21
22
int main () {
23
  const float f = 1.2;
24
  double d = 1.2;
25
  long double ld = 3.14;
26
  assert (cleverprintf ("", 1, 2, 3, 'a', "foo") == 2);
27
  assert (cleverprintf ("", 1.234) == 1);
28
  assert (cleverprintf ("", 1, 1.234, 2) == 1);
29
  assert (cleverprintf ("", 1, 1.234f, 2) == 1);
30
  assert (cleverprintf ("", 1, 1.234L, 2) == 1);
31
  assert (cleverprintf ("", 1, f, 2) == 1);
32
  assert (cleverprintf ("", 1, d, 2) == 1);
33
  assert (cleverprintf ("", 1, ld, 2) == 1);
34
  assert (cleverprintf ("") == 2);
35
}

von Wilhelm M. (wimalopaan)


Lesenswert?


von Bauform B. (bauformb)


Lesenswert?

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 ;)

von Nick M. (Gast)


Lesenswert?

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.

von Bauform B. (bauformb)


Lesenswert?

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.

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.