Templates und Compiletime



  • Hi,

    ich habe hin und wieder Vorbehalte Templates zu nutzen, weil ich keine Lust habe meine Compiletime zu verschlechtern. Viele Module ändere ich eben ständig, was aber in der Non-Template-Variante nur eine ÜE betrifft. Bei Templates betrifft es wie man weiß sehr viele.

    Wie macht ihr das in größeren Projekten? Nutzt ihr irgendwelche Spezialisierungen oder bringen hier vorkompilierte Header etwas? Oder nehmt ihr das alles einfach in Kauf und dann "ist es eben so"? Ich habe keine Lust mein gesamtes Projekt neuzukompilieren, wenn ich in einer zentralen Klasse ein Implementierungsdetail ändere.

    Oder nutzt ihr Templates so gut es geht zusammen mit diesem Idiom (Namen vergessen :(), dass man nur die templatespezifische Schnittstelle wirklich als Template löst und die Methoden, welche darüber keine Kenntnis benötigen, als nicht-Template-Methoden ausarbeitet? (ich weiß gerade nicht, wie man das umgesetzt hat, ich hatte da Mal eine gute Nodeklasse zu)

    Nicht falsch verstehen, ich finde meine Lösungen gut und nicht zu java-like und ich brauche die Polymorphie bisher quasi immer. Aber ich muss zugeben, dass ich wenig nach Template-Lösungen suche, weil mich die gesteigerte Compiletime wirklich nervt. Aber da das ja einen nicht abhalten sollte, nehme ich an, dass ich etwas übersehe. 🙂

    Ach und weil facepalm ja gerade so aktiv ist (und etwas OT): Hast Du zufällig weiterführende Literatur dazu, wie man diese und jene Pattern in C++ löst, ohne dass Du es als java-like betiteln würdest? Irgendwoher musst Du Dein Wissen ja auch haben und ich habe noch nichts Brauchbares an Literatur zu diesem Thema gefunden.

    Beste Grüße und vielen Dank



  • Oder nutzt ihr Templates so gut es geht zusammen mit diesem Idiom (Namen vergessen :(), dass man nur die templatespezifische Schnittstelle wirklich als Template löst und die Methoden, welche darüber keine Kenntnis benötigen, als nicht-Template-Methoden ausarbeitet? (ich weiß gerade nicht, wie man das umgesetzt hat, ich hatte da Mal eine gute Nodeklasse zu)

    Templates sollen den Code generisch machen, Code welcher aber nicht generisch gemacht werden muss/kann wird eben auch nicht template-isiert.
    Wenn ich ein Klassentemplate habe, welches einige Member hat welche in keinster Weise von den Template-Parametern abhängen, dann extrahiere ich diese Memberfunktionen und -variablen in eine neue Basisklasse. Die kein Template ist.
    So auch in meinem aktuellen Projekt gelöst.

    Beispiel:

    struct ABase
    {
        // Variablen und Methoden, die nicht von Template-Parametern abhängen
    };
    
    // ABase kann nun ganz gemütlich in Source/Header ausgelagert werden.
    
    template<typename T>
    struct A : ABase
    {
        T param;
    
        T foo();
    
        // ...
    };
    

    Siehe bspw. ios_base und basic_ios . Da wurde auch in ios_base alles reingepackt, was nicht vom Zeichentyp (oder ddamit den Traits) abhängt.
    Hat aber bei der Designentscheidung natürlich nichts mit deinem Problem zu tun gehabt. 🤡

    Oder nehmt ihr das alles einfach in Kauf und dann "ist es eben so"? Ich habe keine Lust mein gesamtes Projekt neuzukompilieren, wenn ich in einer zentralen Klasse ein Implementierungsdetail ändere.

    Ich kann auf meinem System mehrere ÜEs gleichzeitig kompilieren, gerade ist das bei mir auf Acht Stück eingestellt. Wenn ich da einen HEader verändere, dann dauert das kompilieren nicht viel länger als mit einer einzigen ÜE.

    Keine Ahnung wie groß dein Projekt ist... aber du musst ja auch nicht alles generisch machen. Wenn es zu anstrengend wird, kannst du die Generizität auch mal ein wenig lockern. (Oder ist es bei dir zum Teil unentbehrlich?)



  • Ok, das ist fein. 🙂

    Und ja, bei mir werden auch 8 ÜE auf einmal bearbeitet. Wenn aber 100 statt 1 (oder meinetwegen 8 ) kompiliert werden müssen, macht es eben schon einen Unterschied.

    Edit: D.h. eigentlich sieht es immer so aus, als würde er nur eine auf einmal bearbeiten. Also da steht dann "bla.cpp wird kompiliert" (nicht genauer Wortlaut), dann arbeitet er das ab und dann steht da "blub.cpp wird kompiliert". Heißt das, er macht doch nur eine auf einmal? Ich bin gerade irritiert, muss man da in MSVC was einstellen?

    Generizität lockern... na ja, ich komme ja quasi aus der Schiene, ich überlege nur, ob etwas mehr davon nicht besser wäre.



  • Das Konzept nennt sich übrigens Type Erasure. Die Kombination statischer und dynamischer Polymorphie ist extrem mächtig und besonders in Situationen, wo der exakte Typ nicht interessant ist, äusserst nützlich.

    Grundsätzlich kannst du auch einiges dynamisch machen, indem zu das Strategy Pattern statt Policies einsetzt. Letztere benutze ich hauptsächlich auf tiefer Abstraktionsebene für generischen Code -- eher weniger im "normalen" Clientcode. Pimpl ist auch eine Möglichkeit; wenn du Templateparameter wegabstrahieren kannst, bist du wieder bei Type Erasure.

    Zur parallelen Kompilierung in MSVC: Schau dir mal das hier an.



  • Hi,

    ja, über type erasure sollte ich mich auch noch näher informieren. Ich suche gerade alle möglichen Artikel dazu, aber ich finde den Begriff irgendwie schwammig definiert (oder er ist sehr grundsätzlich). Ich weiß jetzt auch nicht genau, wo das hier eine Rolle spielt. Ich habe den Eindruck, type erasure heißt nur, dass man schlanke statische Typen hat, die aber von einem dicken dynamischen Typ sein können. Hält dann also v.a. Interfaces schlank und Abhängigkeiten gering. Wobei statischer/dynamischer Typ vermutlich auch zu ungenau ist, weil sich das auf Polymorphie only bezieht?

    Pimpl hatte ich auch schon im Kopf und Policies vs. Strategy nutze ich in etwa so, wie Du es vorschlägst. Ist das also der Weisheits letzter Schluss?

    Danke für den Thread bzgl. Kompilierung in MSVC 🙂 Ich musste \Gm ausstellen, um das zu aktivieren, jetzt geht's. Ist \Gm "besser" als parallele Kompilierung? Muss man wohl ausprobieren oder was nutzt ihr?



  • Templates sind genial um Codeverdopplung zu vermeiden.

    Es gibt aber das Problem, dass du einen Template-Parameter T hast, der sich durch das ganze Projekt zieht. Führt dazu, dass das Projekt ein einziger Riesenheader ist, nur weil du einmal int und einmal bigint willst.

    Strategie: Teile dein Projekt in verschiedene kleinere "Module" (= Header) auf. An den Modulgrenzen machst du Type-Erasure. In den cpp-Files dann hast du alle Freiheit der Welt. Kannst ad libitum mit Templates und Vererbung arbeiten. Wenn Templates den Binarybloat verdreifachen, egal. Der Bloat addiert sich über die Module, er multipliziert sich nicht, wie er das ohne Type Erasure machen würde.

    Mal ein Beispiel:

    #include "Datenquelle.h"
    
    typedef int DatenElement; // verhindert Template
    
    typedef function<bool(*)(DatenElement&)> VerarbeitungsFunktion;
    
    class Verarbeitungsapparat {
      unique_ptr<Datenquelle> d;
      vector<VerarbeitungsFunktion> queue;
    public:
      void addFunktion(VerarbeitungsFunktion f) { queue.push_back(f); }
      void liesWasAus() { // steht im cpp-File, hier für die Veranschaulichung inline
        while (!d->empty()) {
          auto e = d->gibsMir();
          for (auto& f : queue)
            if (!f(e)) // wenn false, dann herausgefiltert
              goto next;
          return e;
        next:;
        }
      }
    };
    
    // Dann in einem anderen File: Konkrete Verarbeitungsfunktionen.
    // Keinerlei Abhängigkeit, jede steht für sich. Auch State möglich, da std::function
    // std::function ist ein schönes Beispiel für Type Erasure
    VerarbeitungsFunktion verdopplungsFunktion();
    VerarbeitungsFunktion filterFunktion();
    

    (Überlege dir mal, wie das mit einer Klassenhierarchie im Speicher aussehen würde: eine lange einfach verlinkte Liste. Langsam. Hier vector: Schnell. Zudem Kapselungsvorzüge)

    Bei mir führt das oft dazu, dass das im Header eine triviale Klasse steht (collision_detector: add_object, query_range), die im cpp-File mit tausend Tempaltes (Template-Quadtree, Template-Memory-Pool) rumhantiert. Gab auch schon andere Fälle, wo im cpp-File sehr viel Vererbung war (handelte sich um einen Ast-Analyzer, im Header aber auch eine 10-Zeilen-Klasse).

    Vererbung über Modulgrenzen hinweg ist möglichst zu vermeiden. Vererbung ist das Gegenteil von Kapselung und das führt dazu, dass alles voneinander abhängt und jede kleine Änderung eine Änderung in der Klassenhierarchie mit sich zieht.



  • Das klingt etwas radikal, aber gefällt mir. 🙂 Vererbung über Modulgrenzen hinweg wäre ja nach Deiner Definition über mehrere Header hinweg. Das habe ich eigentlich ständig, einfach wenn eine Klasse eben von einer anderen erbt, ganz normal.

    Trotzdem würde ich das jetzt erstmal nicht umsetzen können oder bei mir im Projekt sehen, wo man das angemessen nutzen würde.



  • Ganz interessant ist auch folgendes: http://en.wikipedia.org/wiki/Policy-based_design

    Kommt bei mir ständig und überall in irgendeiner Form zum Einsatz. Schaus dir mal an 😉



  • Also vll habe ich das missverstanden, aber unter der Abkürzung Policies verstehe ich PBD, das nutze ich ohnehin, aber ist mir nicht neu.



  • Mal eine Anschlussfrage:

    Wenn ich so splitte wie bei basic_ios und ios_base, bringt es dann noch einen wesentlichen Vorteil das auf Header aufzuteilen oder reicht die Trennung für sich schon?

    Ich meine, wenn basic_ios ohnehin von ios_base erbt, dann wird es ohnehin ständig auch so inkludiert, also dürfte es doch egal sein?

    Die Ersparnis besteht dann aber "nur" darin, dass Methoden, die nicht auf T angewiesen sind, nicht mehrfach kompiliert werden, richtig? Das ist der ganze Vorteil.



  • Eisflamme schrieb:

    Wie macht ihr das in größeren Projekten? Nutzt ihr irgendwelche Spezialisierungen oder bringen hier vorkompilierte Header etwas?

    Vorkompilierte Header bringen enorm viel. Wer auf die verzichtet ist selbst schuld. Je nach Projekt kann es mit vorkompilierten Headern schon mal Faktor >= 10 schneller werden.

    Oder nehmt ihr das alles einfach in Kauf und dann "ist es eben so"?

    Im Prinzip ja. Allerdings erst nach Optimierung durch vorkompilierte Header bzw. was einem sonst noch einfällt.

    Ich habe keine Lust mein gesamtes Projekt neuzukompilieren, wenn ich in einer zentralen Klasse ein Implementierungsdetail ändere.

    Your loss.

    Oder nutzt ihr Templates so gut es geht zusammen mit diesem Idiom (Namen vergessen :(), dass man nur die templatespezifische Schnittstelle wirklich als Template löst und die Methoden, welche darüber keine Kenntnis benötigen, als nicht-Template-Methoden ausarbeitet? (ich weiß gerade nicht, wie man das umgesetzt hat, ich hatte da Mal eine gute Nodeklasse zu)

    Wenn es Sinn macht mache ich das natürlich. Aber nicht primär um auf Übersetzungszeit zu optimieren, sondern eher als Massnahme gege Code-Bloat.

    Aber ich muss zugeben, dass ich wenig nach Template-Lösungen suche, weil mich die gesteigerte Compiletime wirklich nervt. Aber da das ja einen nicht abhalten sollte, nehme ich an, dass ich etwas übersehe. 🙂

    Kauf dir nen schnelleren PC. Bzw. falls du wirklich keine precompiled headers verwenden solltest, was mMn. total irre ist, dann solltest du erstmal an dieser Stelle nachbessern.



  • Okay, dann sollte ich mir wohl wirklich Mal precompiled Header anschauen, die nutze ich in der Tat noch nicht. Hatte irgendwann in der Vergangenheit Mal ein paar Nachteile gelesen, zumindest schienen die umstritten zu sein. Aber wird nachgeholt! Danke!

    Die Anschlussfrage aus dem Beitrag vor Deiner Antwort wäre dann noch offen. 🙂



  • Eisflamme schrieb:

    Wenn ich so splitte wie bei basic_ios und ios_base, bringt es dann noch einen wesentlichen Vorteil das auf Header aufzuteilen oder reicht die Trennung für sich schon?

    Es reicht die Trennung in Nicht-Template und Template. Und natürlich solltest du den Nicht-Template Code in das .cpp File verschieben wenn du auf Übersetzungszeit optimieren willst.
    Zusätzliches Header-File halte ich aber für sinnlos.

    Ich meine, wenn basic_ios ohnehin von ios_base erbt, dann wird es ohnehin ständig auch so inkludiert, also dürfte es doch egal sein?

    Die Ersparnis besteht dann aber "nur" darin, dass Methoden, die nicht auf T angewiesen sind, nicht mehrfach kompiliert werden, richtig? Das ist der ganze Vorteil.

    Die Ersparnis ist auch dass bei Änderungen an der Implementierung der Nicht-Template Methoden nicht alles neu übersetzt werden muss.

    Und weil ich gerade lese dass das hier erwähnte wurde - natürlich baue ich alle Projekte mit /MP (also multithreaded). Auf nem Quad-Core bringt das auch nen ordentlichen Boost.



  • Okay, prima, so dachte ich mir das. Da habe ich ja wieder viele neue schöne Dinge zu entdecken *freu* Leider hab ich nur DualCore Hyperthreading, aber dass es schneller als vorher ist merke ich dennoch.

    Dankeschön!


Anmelden zum Antworten