Was wären C und C++ ohne Präprozessor?



  • hustbaer schrieb:

    Nämlich an den Stellen, wo man ohne #defines Code x-fach wiederholen müsste.

    Nenn mal ein Beispiel was man nicht anders lösen könnte...



  • David W schrieb:

    Nenn mal ein Beispiel was man nicht anders lösen könnte...

    Include Guards.



  • Ein nettes Beispiel aus der VCL bzgl. '#define':

    #include <windows.h>
    
    class TCanvas
    {
    public:
      void TextOut(/*...*/);
    }
    

    Frage: Wie lautet der richtige Name der Methode (d.h. nachdem der Präprozessor diesen Code durchlaufen hat)?


  • Mod

    hustbaer schrieb:

    Ich sehe nur nicht wie die eine Art von "man kann Mist bauen/muss wissen was man tut" sich von der anderen Art unterscheiden soll?

    Das will ja auch niemand ersatzlos streichen.

    Nochmal speziell dazu:

    defines brechen alle sonstigen Spracheigenschaften, weil sie sämtliche Scopes verletzen.

    #defines befinden sich ausserhalb der Spracheigenschaften auf die du dich beziehst, und können sie daher weder brechen noch Scopes verletzen, da es an der Stelle noch gar keine Scopes gibt.

    Und eben das ist doch das Problem! Es ist ein Sprachelement, das sich völlig anders als der Rest der Sprache verhält. Ich hkann ein define vor deinen Code setzen und diesen (versehentlich oder absichtlich) dadurch vollkommen zerstören. Mit anderen Sprachmitteln kann mir das nicht so leicht versehentlich passieren, bzw. ich bekomme eine aussagekräftige Fehlermeldung, weil der Compiler das Problem analysieren kann. Sofern dein Code namespaces benutzt, müsste ich diese schon absichtlich missachten, um deinen Code kaputt zu machen. Wie oft kommen Fragen im Forum, weil irgendein dummer Header ein Makro namens min/max definiert hat? Bestimmt alle 1-2 Monate.

    Guter Ersatz für Präprozessordefines wäre zum Beispiel eine Formalisierung der jetzt schon vorhandenen pragmas. Ja, die gehören auch zum Präprozessor, sind aber weitaus weniger problematisch - ihr Problem ist die mangelnde Portabilität. Ein pragma once kann Includeguards ersetzen und man könnte auch ein Pragma für bedingte Compilierung einführen. Und damit hat man (zumindest für C++) schon einmal 99% der Präprozessoranweisungen ersetzt. Mit ein bisschen Nachdenken (nich unbedingt spontan um kurz vor 12 in der Nacht wie jetzt) kann man den Rest sicherlich auch noch gut ersetzen.



  • David W schrieb:

    hustbaer schrieb:

    Nämlich an den Stellen, wo man ohne #defines Code x-fach wiederholen müsste.

    Nenn mal ein Beispiel was man nicht anders lösen könnte...

    Mit momentanen Mitteln kann man vieles nicht ohne Makros lösen. Bzw. es ist möglich, aber nicht vernünftig oder man muss enorm viel Boilerplate-Code in Kauf nehmen. Was das anbelangt, sind Makros an manchen Orten unschlagbar, wo Compile- und Laufzeit-Abstraktionsmechanismen versagen. Konkret fallen mir neben den bereits genannten Anwendungsfällen z.B. Debug- und Log-Funktionen mit __LINE__ , __FILE__ und Stringize-Operator #, oder Lesbar-Machungen von Template-Traits und Dingen wie STATIC_ASSERT ein. Zumindest bei letzterem sehe ich auch nicht gerade, wie man das verbessern könnte...

    SeppJ schrieb:

    Sofern dein Code namespaces benutzt, müsste ich diese schon absichtlich missachten, um deinen Code kaputt zu machen. Wie oft kommen Fragen im Forum, weil irgendein dummer Header ein Makro namens min/max definiert hat? Bestimmt alle 1-2 Monate.

    Richtig, aber auch beim Präprozessor gibt es gewisse Regeln, mit deren Einhaltung man viel weniger Mist baut. Eine lautet z.B. "wähle keine Allerweltsnamen". Man kann genauso argumentieren, dass man absichtlich derart idiotische Makro-Bezeichner wählen muss, um Code zu zerstören. Aber es stimmt natürlich, dass die Auswirkungen bei Makros noch verheerender sind, weil man sie in einem anderen Scope nicht einfach verdecken kann.

    Ich sehe das Riesenproblem des Präprozessors ehrlich gesagt nicht. Besonders in C++ ist man ohnehin eher selten auf ihn angewiesen (von Includes und Header-Guards mal abgesehen). Makros sind sicher nicht der Weisheit letzter Schluss, aber manchmal sind sie eben doch verdammt praktisch. Ich weiss nicht, ob es klüger wäre, stattdessen etliche andere Sprachmittel einzubauen, und dabei immer noch einzelne Fälle ungelöst lassen.



  • SeppJ schrieb:

    defines brechen alle sonstigen Spracheigenschaften, weil sie sämtliche Scopes verletzen.

    #defines befinden sich ausserhalb der Spracheigenschaften auf die du dich beziehst, und können sie daher weder brechen noch Scopes verletzen, da es an der Stelle noch gar keine Scopes gibt.

    Und eben das ist doch das Problem! Es ist ein Sprachelement, das sich völlig anders als der Rest der Sprache verhält.

    Wieso soll das ein Problem sein?

    Ich hkann ein define vor deinen Code setzen und diesen (versehentlich oder absichtlich) dadurch vollkommen zerstören.

    Versehentlich kann dir das nur passieren, wenn du dich an keinerlei etablierte Regeln hältst oder gigantisch viel Pech hast. Und absichtlich kann man immer alles kaputtbekommen.

    Mit anderen Sprachmitteln kann mir das nicht so leicht versehentlich passieren, bzw. ich bekomme eine aussagekräftige Fehlermeldung, weil der Compiler das Problem analysieren kann.

    Ja, klar, bei sämtlichen anderen Dingen bekommt man ne aussagekräftige Fehlermeldung. Sorry, aber das ist nur mehr Quark. Sag mir mal wo du ne aussagekräftige Fehlermeldung herbekommen willst wenn du z.B. versehentlich nen Overload machst der das Programmverhalten in unerwünschter Form verändert.

    Sofern dein Code namespaces benutzt, müsste ich diese schon absichtlich missachten, um deinen Code kaputt zu machen.

    Und wenn mein Code entsprechende Prefixe für Makronamen verwendet (was er tut), müsstest du ebenso mutwillig Unfug anrichten wollen.

    Wie oft kommen Fragen im Forum, weil irgendein dummer Header ein Makro namens min/max definiert hat? Bestimmt alle 1-2 Monate.

    OK. Eine (grosse) Firma hat mit Makros Mist gebaut. Klar, das ist natürlich das Argument dafür dass das Feature vollkommen unbrauchbar weil viel zu gefährlich ist.

    Guter Ersatz für Präprozessordefines wäre zum Beispiel eine Formalisierung der jetzt schon vorhandenen pragmas. Ein pragma once kann Includeguards ersetzen und man könnte auch ein Pragma für bedingte Compilierung einführen. Und damit hat man (zumindest für C++) schon einmal 99% der Präprozessoranweisungen ersetzt. Mit ein bisschen Nachdenken (nich unbedingt spontan um kurz vor 12 in der Nacht wie jetzt) kann man den Rest sicherlich auch noch gut ersetzen.

    Ja, klar, wenn du den PP nie für was anderes als #ifdef _DEBUG und assert() verwendest mag das für dich so aussehen.

    Guck dir doch bitte einfach mal an was BOOST_FOREACH bzw. die Boost.Log Makros machen.



  • Defines sind doch der einzige weg, wenn man unterschiedliche OS APIs im selben Code unterstützen will.


  • Mod

    hustbaer schrieb:

    Versehentlich kann dir das nur passieren, wenn du dich an keinerlei etablierte Regeln hältst oder gigantisch viel Pech hast.

    Man muss sich also an Regeln halten, weil es gefährlich ist.

    Ja, klar, wenn du den PP nie für was anderes als #ifdef _DEBUG und assert() verwendest mag das für dich so aussehen.

    Guck dir doch bitte einfach mal an was BOOST_FOREACH bzw. die Boost.Log Makros machen.

    Tu nicht so, als würde ich diese Anwendungen nicht kennen. Dann tu ich auch nicht so, als würde dir dazu ein globales define als einzige Möglichkeit einfallen, das gleiche zu erreichen.



  • kjhgfas schrieb:

    Defines sind doch der einzige weg, wenn man unterschiedliche OS APIs im selben Code unterstützen will.

    Nein, man kann sich auch Buildfiles zusammenhacken.



  • Nick Unbekannt schrieb:

    David W schrieb:

    Nenn mal ein Beispiel was man nicht anders lösen könnte...

    Include Guards.

    Wir redeten von den defines, nicht den includes (wobei die auch ziemlich nervig sind, sprach SeppJ auch schon an.

    Nexus schrieb:

    Debug- und Log-Funktionen mit __LINE__, __FILE__

    Kann man auch in statischen klassen auslagern, ein TraceStack und done, muss nur noch entwickelt werden.

    Nexus schrieb:

    STATIC_ASSERT

    Gibts in anderen Sprachen schon lange, zb
    Contract.Requires(i == 5, "The parameter has to be 5");

    ich persönlich habe nichts gegen den Präprozessoren im allgemeinen, nur die defines sollten komplett ersetzt werden, das erzeugt schnell unübersichtlichen Code.
    -> Sauberkeit
    -> Übersicht
    -> Maintenance



  • otze schrieb:

    kjhgfas schrieb:

    Defines sind doch der einzige weg, wenn man unterschiedliche OS APIs im selben Code unterstützen will.

    Nein, man kann sich auch Buildfiles zusammenhacken.

    Dann hast du aber nicht mehr den selben Code sondern zwei Files die teilweise den gleichen und teilweise unterschiedlichen OS abhängigen Code enthalten.



  • David W schrieb:

    Wir redeten von den defines, nicht den includes (wobei die auch ziemlich nervig sind, sprach SeppJ auch schon an.

    Und wenn man diese mit defines schützt, sind sie alles andere als nervig. Auch kann man erst dann von portablen include Guards reden. pragma once ist zwar eine Möglichkeit, aber nicht portabel. Und das auch nicht grundlos, weil die bedingte Compilierung, die bei den include guards genutzt wird, sind wesentlich mächtiger. Ich finde implizite includes, wie zum Beispiel in Java, wesentlich verwirrender und auch fehleranfälliger.

    David W schrieb:

    Kann man auch in statischen klassen auslagern, ein TraceStack und done, muss nur noch entwickelt werden.

    Gibts aber nicht. Und das auch aus nachvollziehbaren Grund. Du müsstest so Klassen entwickeln, die weit mehr über ihren Kontext wissen müssen, als eine Sprache wie C++ ihnen zugesteht. Diese fehlenden Laufzeitinformationen, sind meiner Meinung nach auch ein riesen Vorteil von C++, gegenüber anderen Sprachen.

    David W schrieb:

    Gibts in anderen Sprachen schon lange,

    Wäre wieder eine Einmischung in den Code, die dort nichts verloren hat. Wenn man so etwas braucht, wäre eine Überarbeitung des Präprozessors sicher die bessere Wahl. Wobei ich davon auch nichts halte, weil in seiner jetzigen Form bietet der Präprozessor auch noch Verwendungsmöglichkeiten, ohne Compiler.



  • kjhgfas schrieb:

    otze schrieb:

    kjhgfas schrieb:

    Defines sind doch der einzige weg, wenn man unterschiedliche OS APIs im selben Code unterstützen will.

    Nein, man kann sich auch Buildfiles zusammenhacken.

    Dann hast du aber nicht mehr den selben Code sondern zwei Files die teilweise den gleichen und teilweise unterschiedlichen OS abhängigen Code enthalten.

    Das sollte man sowieso machen. Für die OS-spezifischen sachen eigene dateien zu verwenden ist das absolute minimum. Klar gibt es spezies, die alles in eine datei hacken *grusel*.

    Der allgemeine teil kommt in eine allgemeine implementierung, OS-spezifische sachen in jeweils eine eigene datei. Und das selbstverständlich auch, wenn ich den PP dafür benutze. Alles andere ist ein scheiss hack, welcher bei jeder portierung erneut angepasst werden muss.



  • SeppJ schrieb:

    hustbaer schrieb:

    Versehentlich kann dir das nur passieren, wenn du dich an keinerlei etablierte Regeln hältst oder gigantisch viel Pech hast.

    Man muss sich also an Regeln halten, weil es gefährlich ist.

    Jetzt sind wir wieder ganz am Anfang: das Leben ist gefährlich. Get over it. 🙄

    Ja, klar, wenn du den PP nie für was anderes als #ifdef _DEBUG und assert() verwendest mag das für dich so aussehen.

    Guck dir doch bitte einfach mal an was BOOST_FOREACH bzw. die Boost.Log Makros machen.

    Tu nicht so, als würde ich diese Anwendungen nicht kennen. Dann tu ich auch nicht so, als würde dir dazu ein globales define als einzige Möglichkeit einfallen, das gleiche zu erreichen.

    DU tust doch so als würdest du diese Anwendungen nicht kennen. Entweder das, oder du unterschätzt die Aufgabenstellung gewaltig, einen Ersatz für den Präprozessor zu definieren, mit dem solche Dinge nach wie vor möglich sind. (Und der dann auch noch weniger "gefährlich" ist).

    Entweder du kastrierst die Sprache, oder du erlaubst Dinge die "gefährlich" sind. Guck dir bloss mal Reflection in C# oder Java an. Ist auch "gefährlich", dafür kann man einiges damit machen.



  • hustbaer schrieb:

    SeppJ schrieb:

    hustbaer schrieb:

    Versehentlich kann dir das nur passieren, wenn du dich an keinerlei etablierte Regeln hältst oder gigantisch viel Pech hast.

    Man muss sich also an Regeln halten, weil es gefährlich ist.

    Jetzt sind wir wieder ganz am Anfang: das Leben ist gefährlich. Get over it. 🙄

    Genau schmeißt die C++ Versicherung Concept und Module aus dem übernächsten Standard auch raus, wir haben unsere Werkzeuge - Get over it!



  • Die Compiler (Parser) könnten auch um einiges schneller sein, wenn es den PP nicht in diesem Umfang gäbe.
    Und auch die IDE-Unterstützung wäre dann präziser (was manchmal für ein Mist bei IntelliSense bzw. sogar Visual Assist rauskommt...).

    C# hat hier (meiner Meinung nach 😉 einen guten Kompromiss gefunden (nur bedingte Kompilierung ist mittels #define möglich - keine Textersetzung).

    Klar habe ich mir auch schon selber Makros gebastelt, um Template-Klassen bzw. Methoden eleganter definieren bzw. aufrufen zu können, aber es besteht schon ein Unterschied ob man Makro-Funktionen bewußt einsetzt oder aber durch irgendwelche Headerdateien (evtl. vom SDK bzw. Fremdbibliotheken) komische Defines aufgezwungen bekommt (bzw. diese dann erst selber wieder per '#undef' abschalten muß).

    Aber bis C++ ein vernünftiges Modul-Konzept hat, wird es leider noch lange dauern...


  • Mod

    hustbaer schrieb:

    SeppJ schrieb:

    hustbaer schrieb:

    Versehentlich kann dir das nur passieren, wenn du dich an keinerlei etablierte Regeln hältst oder gigantisch viel Pech hast.

    Man muss sich also an Regeln halten, weil es gefährlich ist.

    Jetzt sind wir wieder ganz am Anfang: das Leben ist gefährlich. Get over it. 🙄

    Du kannst alles mit Assembler schreiben. Wozu also eine Hochsprache? Get over it. Ach, wozu Assembler. Einfach direkt in Maschinensprache schreiben. Da das ja voll ausreichend ist, ist es egal wie schlecht das zum Entwickeln ist. Jedenfalls sollte die Menschheit niemals auf irgendeinem Gebiet nach Erleichterungen streben.[quote]

    Entweder du kastrierst die Sprache, oder du erlaubst Dinge die "gefährlich" sind. Guck dir bloss mal Reflection in C# oder Java an. Ist auch "gefährlich", dafür kann man einiges damit machen.

    Ganz einfacher Vorschlag: defines die den Scope beachten? Metafunktionen die zur Compilezeit ausgewertet werden, so ähnlich wie Templatemetaprogrammierung, nur imperativ anstatt funktional*? Schon haste sehr viele der Probleme des Präprozessors gelöst und automatische Codegenerierung geht trotzdem noch. Aber nein, bloß keine Veränderung, das Leben könnte ja leichter werden 🙄 .

    *: Dies würde die Schwierigkeit natürlich auf den Compilerbau abwälzen. Aber darum geht es ja: Der Präprozessor war extrem einfach zu implementieren und man brauchte ihn um gewisse Probleme zu lösen. Aber die beste Lösung für den Entwickler der Anwendungsprogramme (nicht aus Sicht des Compilerentwicklers) ist er nicht.



  • *: Dies würde die Schwierigkeit natürlich auf den Compilerbau abwälzen. Aber darum geht es ja: Der Präprozessor war extrem einfach zu implementieren und man brauchte ihn um gewisse Probleme zu lösen. Aber die beste Lösung für den Entwickler der Anwendungsprogramme (nicht aus Sicht des Compilerentwicklers) ist er nicht.

    Das ist auch so ein Punkt der mir bei C++ innerhalb der letzten Monate, in denen ich angefangen habe mich mit Haskell zu beschäftigen, aufgefallen ist: Warum muss ich mir als Programmierer Gedanken um die Codegenerierung machen?
    Warum kann der Compiler nicht selber wissen, welche Datei er schon kompiliert hat?
    Warum muss ich darauf achten, dass Funktionen zumindest deklariert sind, bevor ich sie benutze? Der Kompiler könnte doch einfach nach der Definition in den eingebundenen Dateien suchen und erst wenn er sie überhaupt nicht findet einen Fehler ausgeben.
    Warum kann ich Templates nicht genauso einfach in .h und .cpp Dateien aufteilen, wie jede andere Klasse auch?

    Sind das alles Altlasten aus C oder nimmt das C++ Standardisierungskomitee hier Rücksicht auf die Kompilerhersteller, weil das so schwer zu implementieren ist?



  • Freed schrieb:

    Warum kann ich Templates nicht genauso einfach in .h und .cpp Dateien aufteilen, wie jede andere Klasse auch?

    Es hat ja mal einen Versuch mit dem Schlüsselwort export gegeben, das sich aber äusserst schlecht in die Sprache integriert hat und im Endeffekt mehr Nach- als Vorteile gebracht hat. Why We Can't Afford Export

    Freed schrieb:

    Sind das alles Altlasten aus C oder nimmt das C++ Standardisierungskomitee hier Rücksicht auf die Kompilerhersteller, weil das so schwer zu implementieren ist?

    Das ganze Konzept mit Aufteilung in Header- und Implementierungsdatei ist von C geerbt. Doch eine Umsetzung eines Modulsystems wie in anderen Sprachen ist in C++ nicht so einfach, weil die Sprache um einiges komplexer ist. Getrennte Deklarationen und Definitionen, ODR, verschiedene Speicherklassen (Linkage), Templates, Inline-Funktionen sind alles Dinge, die eng mit dem Header-System von C++ zusammenhängen.


  • Mod

    Freed schrieb:

    Sind das alles Altlasten aus C oder nimmt das C++ Standardisierungskomitee hier Rücksicht auf die Kompilerhersteller, weil das so schwer zu implementieren ist?

    Vermutlich beides. Auf diese Weise ist es jedenfalls ausreichend den Code genau einmal durchzugehen. Das macht den Compilerbau einfach und das Compilieren relativ schnell. Und das mit der Aufteilung der Templates nach .h und .cpp steht im Standard sogar drin, aber das kann nur kaum ein Compiler (soweit ich weiß nur einer), weil es ziemlich aufwändig zu implementieren ist. Daher wurde c++0x auch ausgiebiger mit den Compilerherstellern diskutiert als der Standard mit den Templates, so dass man nicht wieder Features zur Sprache hinzufügt, die nur schwer zu implementieren sind.

    edit: Zu langsam. Und im Gegensatz zu Nexus habe ich noch nicht einmal den schönen Link dabei 😞 .


Anmelden zum Antworten