Hallo zusammen,
ich habe ein großes , konstantes, globales struct, was eine globale
Konfiguration meines µC-Programms enthält.
Die Deklaration und das Typedef befindet sich in einer Header-Datei. Die
Definition in einer *.c-Datei.
Um die Benennung der Variablen und ihren Inhalt nicht komplett getrennt
bearbeiten zu müssen, gibt es ein großes #define. In der *.c-Datei wird
die Variable dann mit dem großen Define zugewiesen. Ungefähr so:
1
settings_header.h
2
typedefstructSettings_S
3
{
4
intvar0;
5
intvar1;
6
intvar2;
7
}
8
Settings_t;
9
10
externSettings_tSettings;
11
12
#define SETTINGS_DEFAULT \
13
{\
14
.var0 = 0,\
15
.var1 = 2,\
16
.var2 = 1,\
17
}
und
1
#include"settings_header.h"
2
3
Settings_tSettings=(Settings_t)SETTINGS_DEFAULT;
Was mich stört: Fehlersuche. Ein Semikolon statt Komma kann man schon
ein Weilchen suchen, weil der Compiler logischerweise die Zeile der
"echten" Definition nicht kennt.
Geht das auch schöner?
Bei der Menge der Fragen muß ich dann doch mal den Wilhelm machen, und
die Cpp Core Guideline CPL.1 zitieren:
"Prefer C++ to C"
Das Leben könnte so einfach sein ;)
Oliver
Oliver S. schrieb:> Das Leben könnte so einfach sein
Mitten in einem Projekt eine neue Programmiersprache zu lernen, gehört
nicht zu den Dingen, die das Leben vereinfachen.
Walter T. schrieb:> Was mich stört: Fehlersuche. Ein Semikolon statt Komma kann man schon> ein Weilchen suchen, weil der Compiler logischerweise die Zeile der> "echten" Definition nicht kennt.
Doch die kennt er :-)
Problem ist aber, dass Location-Info aus #line Notes bezogen wird, die
ins Präcompilat eingefügt wurden und sich auf die c-Datei beziehen und
eben nicht aufs Präcompilat.
Ergo:
Wenn in einem Makro ein Tippfehler ist und man aus den üblichen, auf's
.c / .h bezogenen Fehlermeldung nicht schlau wird, dann kann man bei GCC
übersetzten mit
1
-save-temps -P
Dies bewirkt zweierlei:
1) Das Präcompilat wird nicht gelöscht und bleibt als .i erhalten (.ii
bei C++, .s bei Assembler).
2) Die Fehlermeldung bezieht sich auf die übersetzte Quelle, also auf's
i-File. Damit kommt man direkt zur Fehlerstelle und kann so besser auf
den Fehler im Makro zurückschließen.
Für C++ geht das natürlich genauso, nur indem man einen g++ nimmt ändert
sich ja nichts daran, dass sich die Diagnostic auf die .cpp Datei
bezieht und nicht auf's .ii.
Johann L. schrieb:> Für C++ geht das natürlich genauso, nur indem man einen g++ nimmt ändert> sich ja nichts daran, dass sich die Diagnostic auf die .cpp Datei> bezieht und nicht auf's .ii.
Schon. Allerdings kann man in C++ die default-Initialisierungen gleich
mit in die Deklaration schreiben, und damit das tritt das Problem gar
nicht auf.
Oliver
Wenn ich das Komma in Zeile 14 der Header-Datei in ein Semikolon ändere,
erhalte ich folgende Fehlermeldung (GCC 9.1.0):
1
In file included from settings.c:1:
2
settings_header.h:14:12: error: expected ‘}’ before ‘;’ token
3
14 | .var1 = 2;\
4
| ^~
5
15 | .var2 = 1,\
6
|
7
settings_header.h:14:12: note: in definition of macro ‘SETTINGS_DEFAULT’
8
14 | .var1 = 2;\
9
| ^~
10
15 | .var2 = 1,\
11
|
12
settings_header.h:12:1: note: to match this ‘{’
13
12 | {\
14
| ^~
15
13 | .var0 = 0,\
16
|
17
settings_header.h:12:1: note: in definition of macro ‘SETTINGS_DEFAULT’
18
12 | {\
19
| ^~
20
13 | .var0 = 0,\
21
|
Der Compiler meckert zwar statt des falschen Semikolons eine fehlende
geschweifte Klammer an (das ist für ihn schwer zu unterscheiden, ohne
die nachfolgenden Zeilen bereits im Blick zu haben), zeigt aber dennoch
mit seinem Finger genau auf die richtige Stelle im Code.
Vielleicht wird es so langsam doch mal Zeit, deinen Compiler upzudaten.
Seit deiner 4.x-Version wurden viele deutliche Verbesserungen
vorgenommen, insbesondere auch bei der Qualität der Fehlermeldungen.
Oliver S. schrieb:> Bei der Menge der Fragen muß ich dann doch mal den Wilhelm machen, und> die Cpp Core Guideline CPL.1 zitieren:>> "Prefer C++ to C">> Das Leben könnte so einfach sein ;)>> Oliver
Ich danke Dir: besser hätte ich es nicht schreiben können.
Nur noch zwei Hinweise (auch wenn der Thread mit C überschrieben ist):
1) Du brauchst keine neue Sprache zu lernen, denn man kann ja
inkrementell auf C++ umsteigen, also genau diesen einen Punkt
verbessern.
2) Sollte es mal komplizierter werden mit den Initialisierungen (etwa
Arrays, deren Elemente man etwas komplizierter berechnen muss): IIFE
(immediate invoked function expressions), das sind
constexpr-lambda-expressions, die man sofort aufruft, und die zur
Compilezeit ein Objekt liefern, mit dem dann
kopier-/verschiebungsinitialisiert wird.
Man kann durchaus auch in C (natürlich längst nicht so schön wie in C++)
"information hiding" betreiben. Einigermassen elegant geht das mit einer
separaten Übersetzungseinheit, die "nach aussen" nur Setter und Getter
bereitstellt.
Muß also nicht unbedingt gleich C++ lernen (aber sich Macros abgewöhnen
- zumindest dort, wo man sie nicht unbedingt braucht).
C++ steht zwar auf der Liste von Sachen, die ich mal anfangen will -
aber zu meiner Aussage von oben stehe ich weiterhin. Eine neue
Programmiersprache lernt man am besten, wenn man ein kleines neues
Projekt aufzieht.
Wenn man ein altes Projekt mit frisch erworbenem Wissen und Konzepten
umstellen wollte, produziert man hauptsächlich Blindleistung.
Ich gehe mal davon aus, daß wenn selbst Johann eher darauf hinweist, wie
man Fehler in der obigen Vorgehensweise findet, daß es keine wirklich
viel schönere Alternative gibt.
Was auch noch ginge, wäre eine Definition in eine #ifdef-Abzweigung zu
packen, die nur in einer inkludierenden Datei definiert ist. Aber
wirklich viel schöner ist das auch nicht.
Walter T. schrieb:> C++ steht zwar auf der Liste von Sachen, die ich mal anfangen will -> aber zu meiner Aussage von oben stehe ich weiterhin. Eine neue> Programmiersprache lernt man am besten, wenn man ein kleines neues> Projekt aufzieht.
Jein. Ich gehe einfach mal davon aus, dass Du so viel know-how hast,
dass es Dir kein Kopfzerbechen macht, C und C++ zu mischen. Deswegen
brauchst Du nicht gleich eine vermeintlich neue Sprache komplett zu
lernen, nur bestimmte Anteile daraus. Natürlich macht man das nicht an
einem produktiven System. Man kann aber gezielt einige Sachen
transferieren.
Walter T. schrieb:> Was mich stört: Fehlersuche. Ein Semikolon statt Komma kann man schon> ein Weilchen suchen, weil der Compiler logischerweise die Zeile der> "echten" Definition nicht kennt.>> Geht das auch schöner?
Ganz pragmatisch: Schreib Dir ein kleines Komandozeilen Tool, welches
eine Config Datei liest (z.B. im JSON Format) und Dir daraus die
Konfiguration als C-Code rausschmeißt.
Das kannst Du dann im Build Prozess automatisch aufrufen und gut.
Walter T. schrieb:> Ich gehe mal davon aus, daß wenn selbst Johann eher darauf hinweist, wie> man Fehler in der obigen Vorgehensweise findet, daß es keine wirklich> viel schönere Alternative gibt.
Ja, doch. Man könnte auf die statischen Initialisierungen verzichten und
das sauber mit einer Funktion regeln. Global wirksame defines sind echt
der Anfang vom Ende. Ich könnte schreien, wenn ich irgendwo in einem
Header
#define min ...
#define max ...
oder am besten noch
#define B0 ...
sehe.
C++ ist in dieser Hinsicht ungefährlicher, aber immer noch grausam, was
schlechte Mechanik betrifft: Wie viele tausend Zeilen Quelltext
inkludiert man, um auch nur einen unique_ptr zu benutzen? Ein schon
a-priori gescheitertes Konzept, die Implementierung(en) in eine
_Kopf_datei zu verschieben. Das ist in etwa so, wie seine E-Mails im
Betreff zu verfassen!
Der Kompiler streckt nach den ersten 50k Zeilen an Templates alle viere
von sich und braucht für eine 100 Zeilen Datei 2 Sekunden zum
übersetzen, IDEs mit ihren Indexern können das nicht handeln und einem
Entwickler bringt ein "Header", der zu 99% aus Implementierungs-Details
besteht, natürlich auch überhaupt gar nichts. Das nennt man
Fortschritt.
Heiko L. schrieb:> Man könnte auf die statischen Initialisierungen verzichten und> das sauber mit einer Funktion regeln.
Was ist denn an einem Initializer "unsauber"?
Das über eine Funktion zu regeln hat doch nur Nachteile:
* Es funktioniert nicht für const.
* Mehr Code; sowohl für die Funktion als auch für deren Aufruf.
* Das Problem mit dem Makro ist nicht gelöst, denn wir hätten
1
#include"settings_header.h"
2
3
Settings_tSettings;
4
5
voidinit_Settings()
6
{
7
Settings=(Settings_t)SETTINGS_DEFAULT;
8
}
Es ist nämlich immer noch unklar, warum hier der Zwischenschritt über
ein Makro gegangen wurde bzw. dies für notwendig erachtet wurde.
Johann L. schrieb:> Was ist denn an einem Initializer "unsauber"?
1. Das ist ein globales define, das jegliches textliche Vorkommen seines
Namens gnadenlos ersetzt ohne jeden Kontextbezug.
2. Details gehören nicht in Header.
Johann L. schrieb:> * Es funktioniert nicht für const.
Doch, natürlich. Return by value.
Statische Initialisierung geht nicht.
Johann L. schrieb:> * Mehr Code; sowohl für die Funktion als auch für deren Aufruf.
Touche. Faulheit wird kein Vorschub geleistet.
Johann L. schrieb:> * Das Problem mit dem Makro ist nicht gelöst, denn wir hätten> #include "settings_header.h">> Settings_t Settings;>> void init_Settings()> {> Settings = (Settings_t) SETTINGS_DEFAULT;> }
Ich dachte eher an
Heiko L. schrieb:> C++ ist in dieser Hinsicht ungefährlicher, aber immer noch grausam, was> schlechte Mechanik betrifft:
Die Implementierung muss in diesem Fall im Header stehen, weil es sich
um ein Template handelt und der Compiler den Code für alle möglichen
Typen erzeugen können muss.
Man könnte natürlich sagen, dass ein unique_ptr für alle Typen im
Prinzip das gleiche macht. Das ist aber nicht der Fall, weil es z.B. für
Arraytypen int[] ein anderes new/delete gibt. Außerdem kann sich auch
das Verhalten durch noexcept oder eben kein noexcept ändern.
torusle schrieb:> Ganz pragmatisch: Schreib Dir ein kleines Komandozeilen Tool, welches> eine Config Datei liest (z.B. im JSON Format) und Dir daraus die> Konfiguration als C-Code rausschmeißt.
Das wäre ja dann quasi das, was der Präprozessor macht, nur mit einem
eigenen Tool.
Johann L. schrieb:> Heiko L. schrieb:> ABC_ABI ABC_INLINE ABC_DECL(Settings,> default_settings) ABC_WHATEVER {>> ABC_INLINE_RETURN(Settings, { 1,2,3 });>> }> Was soll das denn darstellen?
Das ist eine "portable" Definition einer Funktion, wenn man noch auf
sowas wie alte C-Compiler Rücksicht nimmt...
> Mach's mal konkret.
Aber ich denke mal, du meinst
M.K. B. schrieb:> Die Implementierung muss in diesem Fall im Header stehen, weil es sich> um ein Template handelt und der Compiler den Code für alle möglichen> Typen erzeugen können muss.
Apologet! Wenn das ein vernünftiger Grund wäre, würde das ISO-Komitee
nicht darüber nachdenken, wie Module realisiert werden könnten.
Ob dein Vorgehen (C, defines) richtig oder gut ist, kann ich nicht
beurteilen, ich kenne aber Fälle, wo es alternativlos gut ist.
Überleg dir vielleicht, das zu trennen: zu jedem Strukturelement x ein
define DX, und dann in der .c die Zuweisung explizit.
Durch die feste namens-Konvention ist der Aufwand klein, die
Flexibilität groß. Z.b. kann DVar1 dann leicht DVar0+5 sein.
>> Mach's mal konkret.> Aber ich denke mal, du meinst
1
> inline Settings default_settings() {
2
> return (Settings){1,2,3};
3
> }
Ich wollte wissen, was DU damit meintest.
Das von dir vorgeschlagene funktioniert jedenfalls nicht als
Initializer, weil das in C für Variablen im Static Storage keine
Rückgabewerte von Funktionen sein dürfen.
Heiko L. schrieb:> Das ist eine "portable" Definition einer Funktion, wenn man noch> auf sowas wie alte C-Compiler Rücksicht nimmt...
Du meinst vor C89, also über 30 Jahre alt, und das verwendet Walter in
seinem aktuellen Projekt...?
Johann L. schrieb:> Das von dir vorgeschlagene funktioniert jedenfalls nicht als> Initializer, weil das in C für Variablen im Static Storage keine> Rückgabewerte von Funktionen sein dürfen.
Ja, das habe ich auch vor einigen Beiträgen so geschrieben:
Heiko L. schrieb:> Statische Initialisierung geht nicht.Johann L. schrieb:> Du meinst vor C89, also über 30 Jahre alt, und das verwendet Walter in> seinem aktuellen Projekt...?
Oder zB Microsoft, die 2015 verkündet haben, dass das neue Visual Studio
jetzt doch schon C99 fast komplett unterstützt.
Gibt es überhaupt irgendeinen Compiler, der das 100% ISO-konform
übersetzt?
torusle schrieb:> Schreib Dir ein kleines Komandozeilen Tool, welches> eine Config Datei liest (z.B. im JSON Format) und Dir daraus die> Konfiguration als C-Code rausschmeißt.
Dann kann er im JSON-Code nach verlorenen Kommas suchen, auch nichts
gewonnen.
Ich setze zwar auch in Python geschriebene Codegeneratoren ein, aber nur
wenn ich relativ viel redundanten oder schreibintensiven Boilerplate
erzeugen will mit relativ wenig Konfiguration. Nicht wenn der Aufwand
fast gleich und der Gewinn minimal oder gar negativ ist wie in diesem
Fall.
Bernd K. schrieb:> torusle schrieb:>> Schreib Dir ein kleines Komandozeilen Tool, welches>> eine Config Datei liest (z.B. im JSON Format) und Dir daraus die>> Konfiguration als C-Code rausschmeißt.>> Dann kann er im JSON-Code nach verlorenen Kommas suchen, auch nichts> gewonnen.>> Ich setze zwar auch in Python geschriebene Codegeneratoren ein, aber nur> wenn ich relativ viel redundanten oder schreibintensiven Boilerplate> erzeugen will mit relativ wenig Konfiguration. Nicht wenn der Aufwand> fast gleich und der Gewinn minimal oder gar negativ ist wie in diesem> Fall.
Er wollte doch nicht mitten im Projekt eine neue Sprache lernen ;-)
Das macht man zur Compilezeit alles in C++; zur Not auch einen
JSON-Parser der Zur Compilezeit arbeitet.
Johann L. schrieb:> Es ist nämlich immer noch unklar, warum hier der Zwischenschritt über> ein Makro gegangen wurde bzw. dies für notwendig erachtet wurde.
Damit die Initialisierung im Header direkt neben der Strukturdefinition
stehen kann, nicht in ner anderen Datei. Ist ordentlicher.
Ich persönlich mach das übrigens exakt genauso, man könnte fast meinen
OP wär in meinen Rechner oder in mein Gehirn eingedrungen und hätte
meinen Code kopiert. Das geht aber nicht denn der Code ist unter
Verschluß, nur 3 Leute haben ihn je gesehen, nur 2 haben ihn gelesen,
also keine Anschuldigung ;-) Ich hab echt kurz innegehalten und mir die
Augen gerieben so verblüffend identisch ist das in meinem eigenen
settings-Modul, Zeile für Zeile(!), bis auf die Groß-Kleinschreibung,
bei mir ist es extern settings_t settings;
Bei mir wird auch der Header beibehalten und nur die C-Datei wird
ausgetauscht.
Zufälle gibts, erstaunlich...
Eine Möglichkeit sind XMacros.
Dabei lagert man die Information in eine eigene Datei aus, hier
field.def. Eignet sich dann, wenn man es in recht vielen
unterschiedlichen Konfigurationen / Projekten verwendet und die
def-Datei mehr als eine handvoll Einträge hat.
In Situationen, die nicht allzu kompliziert sind, einfacher als ein
externer Code-Generator.
*field.def*
Und weil die originale Frage zu C ist, darf der obligatorische Hinweis
auf C++ Features nicht fehlen.
Ab C++17 gibt es inline-Objekte, die man in einem Header definieren
kann. Zweck ist "Header-only" Programmierung, so dass man keine extra
Module braucht wenn Objekte (im Static Storage) gebraucht werden:
*settings.h*
1
structSettings{...};
2
3
inlineSettingssettings={0,2,1};
Damit hat man dann Typ-Definition und Object-Definition an einer Stelle.
Intern wird settings als ein weak Symbol definiert und als comdat oder
linkonce — je nach Unterstützung im Linker. Hier eine Umsetzung als
comdat:
Bernd K. schrieb:> Damit die Initialisierung im Header direkt neben der Strukturdefinition> stehen kann, nicht in ner anderen Datei. Ist ordentlicher.
Aber das kann doch auch der präprozessor:
Johann L. schrieb:> Es ist nämlich immer noch unklar, warum hier der Zwischenschritt über> ein Makro gegangen wurde bzw. dies für notwendig erachtet wurde.
Ich nehme zwar an, dass sich die Frage erledigt hat, aber der
Vollständigkeit halber: Es geht darum, die Einstellungen möglichst in
einer Datei zu deklarieren und zu definieren, weil hier eine Trennung
syntaktisch nötig, aber inhaltlich ungünstig ist.
Bernd K. schrieb:> so verblüffend identisch ist das in meinem eigenen> settings-Modul,>> [...]> Zufälle gibts, erstaunlich...
Hm. Ich hätte gesagt: Einstellungen braucht jedes µC-Projekt, das auch
etwas macht. Vom Programmablauf sind die Stellen, in denen die
Einstellungen vorgenommen und/oder gespeichert werden, oft komplett an
anderen Stellen, als da, wo sie benötigt werden. Und sie müssen irgendwo
in einem NVRAM gesichert werden.
Also wird vermutlich in jedem kleinen oder mittelgroßen µC-Projekt ein
großes struct existieren, das von vielen Programmteilen ausgelesen wird,
in irgendeinem Einstellungsmodul bearbeitet werden kann und in
irgendeiner Form in einem NVRAM hinterlegt ist. Mit dem netten
Nebeneffekt, dass man es auch mit dem Debugger übersichtlich lesen und
verändern kann.
Johann L. schrieb:> struct Settings { ... };>> inline Settings settings = { 0, 2, 1 };
Das liest sich sogar für einen nicht-C++-Kenner erstaunlich naheliegend.
A. S. schrieb:> extern Settings_t Settings;> #ifdef _SETTING_C_> Settings_t Settings={> ....
Und genau eine der inkludierenden Dateien definiert
1
#define _SETTINGS_C
2
#include settings.h
3
#undef _SETTINGS_C
Genau. Das war die anderen Variante.
Johann L. schrieb:> Eine Möglichkeit sind XMacros.
Da muß ich nach dem Samstagseinkauf mal gucken, ob ich die schöner oder
häßlicher als den IST-Stand finde. Es dürfte auf jeden Fall sehr gut
skalieren.
Walter T. schrieb:> Da muß ich nach dem Samstagseinkauf mal gucken, ob ich die schöner oder> häßlicher als den IST-Stand finde.
Macros sind immer(!) hässlich.
Dr. Sommer schrieb:> Bezeichner die mit Unterstrich + Großbuchstabe oder 2 Unterstrichen> anfangen sind reserviert ;-)
Stimmt. Ich habe das nur von oben gecopypasted. Bei mir heißt das
natürlich #ifdef INCLUDE_DEFINE_VARIABLES, aber das Prinzip ist das
Gleiche.