mikrocontroller.net

Forum: Compiler & IDEs Partielle Spezialisierung in C++ Templates


Autor: rw (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich habe eine Klasse Handler, welche für verschiedene, in einer 
Enumeration definierte Werte unterschiedlich implementiert wird. 
Allerdings ist die Implementierung für verschiedene Wertebereiche 
gleich. Also möchte ich ungefähr so etwas realisieren:
template<uint8 nr>
class Handler {};

template<>
class Handler<0..3> {
  domeSomething() {
    // Algorithmus 1
    // irgendwas machen wobei nr wichtig ist
}


template<>
class Handler<4..7> {
  domeSomething() {
    // Algorithmus 2
    // irgendwas ganz anderes machen wobei nr wichtig ist
}

// noch 2,3 weitere Handler

Ist so etwas möglich?

Ein Strategiemuster möchte ich nicht implementieren, weil die 
nr-abhängigen Teile alle Methoden und auch den Konstruktor betreffen und 
ich in eingebetteten Systemen arbeite (AVR mit GCC).

Danke

Autor: klaus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Meines Wissens nach geht das nicht, da Templates nur auf Grundlage des 
Datentypes unterschiedlich instantiiert werden. Die Elemente eines enums 
stellen aber keine unterschiedlichen Datentypen dar. Afaik kann man also 
eine template Funktion z.B. für einen int spezialisieren und so von 
ihrer "allgemeinen Implementierung" unterscheiden.

Autor: klaus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Ähh, vergiß das mal, glaube habe dich falsch verstanden... interessanter 
Fall den du da hast

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Es gibt da schon was

[quote]
Potenzberechnung mit Hilfe von Metatemplates  [Bearbeiten]
 #include <iostream>
 
 template<long B, unsigned long E>
 struct pow_helper {
   static long const value = B * pow_helper<B, E - 1>::value;
 };
 
 template<long B>
 struct pow_helper<B, 0> {
   static long const value = 1;
 };
 
 template<long B, long E>
 struct power {
    static double const value;
 };
 
 template<long B, long E>
 double const power<B, E>::value= E < 0 ? 1.0 / pow_helper<B, E < 0 ? -E : 0>::value
                                        : pow_helper<B, E < 0 ? 0 : E>::value;
 
 int main() {
   std::cout << power<10, -3>::value << std::endl;
 }

Der Code funktioniert so, dass das Template power aufgerufen wird. 
Dieses wertet nun aus, ob der Exponent negativ ist und rechnet in dem 
Fall . Zum Berechnen der eigentlichen Potenz wird die Struktur 
pow_helper benutzt. Diese ruft sich selbst auf („Rekursion“), wobei der 
Exponent bei jedem Aufruf um 1 reduziert wird. pow_helper besitzt eine 
so genannte Spezialisierung für den Fall, dass der Exponent 0 ist und 
liefert in dem Fall das Ergebnis 1 zurück.
[/quote]

stammt aus Wikipedia
Und ich kann mich auch erinnern, dass die Template Gurus ganze 
'Taschenrechner' mit Templates gebaut haben, die Expressions zur 
Compiletime auswerten

Aber ich gestehe: Ich bin bei Templates zu schwach.

Und ob 0..3 syntaktisch korrekt sein kann, darüber will ich gar nicht 
näher nachdenken (ich denke nicht, dass das ok ist. Zumindest habe ich 
sowas noch nie gesehen)

Autor: Rolf Magnus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Was mir so einfallen würde, wäre das:
#include <iostream>
#include <stdint.h>

template<uint8_t nr>
struct Handler : public Handler<nr-1>
{
    static const unsigned int number = nr;
};

template<>
struct Handler<0>
{
    static const unsigned int number = 0;
    void domeSomething()
    {
        std::cout << "Algorithmus 1\n";
    }
};

template<>
struct Handler<4>
{
    static const unsigned int number = 4;
    void domeSomething()
    {
        std::cout << "Algorithmus 2\n";
    }
};

template<>
struct Handler<8>
{
};


int main()
{
    Handler<0>().domeSomething();
    Handler<3>().domeSomething();
    Handler<5>().domeSomething();
}


Allerdings kommt man so nicht mehr an die Nummer ran. Das könnte man so 
lösen:
#include <iostream>
#include <stdint.h>

template<uint8_t nr, uint8_t nr2>
struct HandlerBase : public HandlerBase<nr-1, nr2>
{
};

template<>
template<uint8_t nr>
struct HandlerBase<0, nr>
{
    void domeSomething()
    {
        std::cout << "Algorithmus 1 für " << int(nr) << '\n';
    }
};

template<>
template<uint8_t nr>
struct HandlerBase<4, nr>
{
    void domeSomething()
    {
        std::cout << "Algorithmus 2 für " << int(nr) << '\n';
    }
};

template<>
template<uint8_t nr>
struct HandlerBase<8, nr>
{
};

template<uint8_t nr>
struct Handler : HandlerBase<nr, nr>
{
};

int main()
{
    Handler<0>().domeSomething();
    Handler<3>().domeSomething();
    Handler<5>().domeSomething();
}

Autor: Klaus Wachtler (mfgkw)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Also ich würde da vermutlich gar nicht spezialisieren,
sondern das mit if oder switch brutal in den Quelltext
schreiben:
template<uint8_t nr>
class Handler
{
  void domeSomething()
  {
    switch( nr )
    {
      case 0:
      case 1:
      case 2:
      case 3:
        // Algorithmus 1
        // irgendwas machen wobei nr wichtig ist
        break;

      case 4:
      case 5:
      case 6:
      case 7:
        // Algorithmus 2
        // irgendwas machen wobei nr wichtig ist
        break;

        // case....

      default :

        break;
    }
  }
};

Wenn jetzt jemand einwendet, das wäre ineffizient, dann
verweise ich flink auf den Optimierer.
Schließlich ist nr ja konstant, also wird hoffentlich der
jeweils überflüssige Plunder eh rausgewirfen.

Autor: Rolf Magnus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Klaus Wachtler schrieb:

> Also ich würde da vermutlich gar nicht spezialisieren,
> sondern das mit if oder switch brutal in den Quelltext
> schreiben:

Ist ja langweilig ;-)

> Wenn jetzt jemand einwendet, das wäre ineffizient, dann
> verweise ich flink auf den Optimierer.
> Schließlich ist nr ja konstant, also wird hoffentlich der
> jeweils überflüssige Plunder eh rausgewirfen.

Ich gehe davon aus, daß das so optimiert wird.

Übrigens: Es gibt einen Unterschied zwischen unseren Versionen. Bei 
meiner bricht der Compiler mit einer Fehlermeldung ab, wenn man einen 
Wert größer als 7 angibt, bei deiner wird das stillschweigend ignoriert.

Autor: Klaus Wachtler (mfgkw)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Das ist richtig.
Aber wenn ich es richtig verstanden habe, will er eigentlich
gar keine Zahlen nehmen, sondern sich eine enum bauen und
deren Werte dann als Templateargument nehmen.
Dann tritt der Fall ja gar nicht auf:
enum eFaelle
{
  Fall0,
  Fall1,
  Fall2,
  Fall3,
  Fall4,
  Fall5,
  Fall6,
  Fall7,
};

template< const eFaelle nr >
class Handler
{
  void domeSomething()
  {
    switch( nr )
    {
      case Fall0:
      case Fall1:
      case Fall2:
      case Fall3:
        // Algorithmus 1
        // irgendwas machen wobei nr wichtig ist
        break;

      case Fall4:
      case Fall5:
      case Fall6:
      case Fall7:
        // Algorithmus 2
        // irgendwas machen wobei nr wichtig ist
        break;

        // case....

    }
  }
};

Autor: klaus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hat das eigentlich schonmal jemand probiert ?
Ich habe folgendes mit dem GCC 4.4.0 versucht:

#include <stdio.h>
#include <stdlib.h>

template<typename T>
class Test1
{
  public:
    Test1()
    {
      printf("Other types\n");
    }
};

template<>
class Test1<int>
{
  public:
    Test1()
    {
      printf("int\n");
    }
};

// template_demo.cpp:25: error: type/value mismatch at argument 1 in template parameter list for 'template<class T> class Test1'
// template_demo.cpp:25: error:   expected a type, got '3'
template<>
class Test1<3>
{

};

int main()
{
  Test1<int> x;
  return 0;
}


Die Spezialisierung mit einer Konstanten scheint er irgendwie nicht zu 
mögen, oder habe ich etwas übersehen ?

Autor: rw (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

aha, ich verstehe. Wenn ich einen solchen Vergleich in die Deklaration 
hineinbringe, meckert der Compiler "template argument ... involves 
template parameter(s)..." Das heißt, die Templateparameter können nicht 
als Templateargument ausgewertet werden. Was aber ginge, wäre folgendes:

template<uint8 nr, uint8 type>
class Handler {};

// für nr == 0..3 
template<uint8 nr>
class Handler<1> {
  void domeSomething() {
    // Algorithmus 1
    // irgendwas machen wobei nr wichtig ist
}

// für nr == 4..7
template<uint8 nr>
class Handler<2> {
  void domeSomething() {
    // Algorithmus 2
    // irgendwas ganz anderes machen wobei nr wichtig ist
}

// usw...


Jetzt könnte man das ganze über einen Delegator ansprechen.
template<uint8 nr>
class HandlerSwitcher {
public:
  Handler<nr, (nr/4)>* operator ->() {
    return &handler;
  }
private:
  Handler<nr, (nr/4)> handler;
};


Die Benutzung erfolgt dann ohne dass man weiß, welcher Handler denn nun 
im Hintergrund verwendet wird:

HandlerSwitcher<0> handlerMitAlgorithmus1;
HandlerSwitcher<4> handlerMitAlgorithmus2;

handlerMitAlgorithmus1->doSomething();
//...

Das hat aber den Nachteil, dass jedesmal ein Pointer auf den Handler 
gespeichert werden muss.


Eine andere Möglichkeit wäre, es über Vererbung zu lösen. Bei der Angabe 
der Elternklasse sind Vergleiche und ähnliche Dinge nämlich erlaubt:
template<uint8 nr>
class HandlerSwitcher : public Handler<nr, (nr < 4 ? 1 : 2)> {};

HandlerSwitcher<0> handlerMitAlgorithmus1;
HandlerSwitcher<4> handlerMitAlgorithmus2;

handlerMitAlgorithmus1.doSomething();
//...

Ich habe beide Möglichkeiten getestet. Beide führen lustigerweise zur 
fast gleichen Codegröße, jedoch ist der RAM-Verbrauch bei letzerer 
Variante ein klein bisschen größer. Möglicherweise liegt das daran, dass 
die Handlerklassen noch 2 virtuelle Methoden beinhalten. Ich weiß aber 
nicht genau, was dabei im Hintergrund mit den vtables geschieht schäm.


Allerdings verlieren beide Möglichkeiten eindeutig gegen die Variante, 
für jeden Algorithmus eine eigene Klasse zu schreiben und den Parameter 
nr als Membervariable zu speichern und als Parameter im Konstruktor zu 
übergeben, etwa so:
class Handler1 {
public:
  Handler1(uint8 nr) {
    //...
  };

  void doSomething() {
  //...
  }
};

// und so weiter


Der Code wird kleiner, der RAM-Verbrauch ist auch geringer. Das liegt in 
meinem Fall wohl daran, dass die Codeähnlichkeiten zwischen den 
einzelnen Klassen wohl doch zu groß sind.


Letztendlich habe ich noch einmal eine Kombination aus der herkömmlichen 
Lösung (für jeden Algorithmus eine extra Klasse, den Parameter nr durch 
den Konstruktor übergeben) und das Delegationsmuster ausprobiert. Dabei 
wird nr über den Konstruktor übergeben und nur der Algorithmus über den 
Templateparameter ausgewählt. Und siehe da: Code- und Ramverbrauch 
entsprechen in etwa der herkömmlichen Lösung, nur dass ich jetzt von 
außen nicht mehr immer die explizite Klasse mit ihrem Algorithmus 
angeben muss.
template<uint8 type>
class Handler {};

template<>
class Handler<1> {
public:
  Handler(uint8 nr) {
    //...
  };

  void doSomething() {
  //...
  }
};

//... und noch weitere Handler


template<uint8 nr>
class HandlerSwitcher {
public:
  Handler<(nr/4)>* operator ->() {
    return &handler;
  }
private:
  Handler<(nr/4)> handler;
};


Die Bentuzung erfolgt wie schon beschrieben. Also ist die "Smart 
Pointer"-Lösung (bzw. das Delegationsmuster) gar nicht so übel. So, alle 
noch da? War ein netter Exkurs.

Gruß
rw

Autor: rw (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Oh, während ich an meinem Text gebastelt habe, gab's weitere Antworten. 
Danke Danke.

@Klaus Wachtler: Das der Optimizer sehr gut optimiert, ist mir auch 
aufgefallen. Deine Lösung probiere ich in meinem Fall noch mal aus.

Autor: Rolf Magnus (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
klaus schrieb:
> Die Spezialisierung mit einer Konstanten scheint er irgendwie nicht zu
> mögen, oder habe ich etwas übersehen ?

Du versuchst, einen Typ-Template-Parameter mit einem Wert zu 
spezialisieren. Das geht natürlich nicht.

rw schrieb:
> Ich habe beide Möglichkeiten getestet. Beide führen lustigerweise zur
> fast gleichen Codegröße, jedoch ist der RAM-Verbrauch bei letzerer
> Variante ein klein bisschen größer. Möglicherweise liegt das daran,
> dass die Handlerklassen noch 2 virtuelle Methoden beinhalten. Ich weiß
> aber nicht genau, was dabei im Hintergrund mit den vtables geschieht
> schäm.

Wenn du avr-g++ verwendest, hast du da das Problem, daß er, weil GCC 
eigentlich nicht für Harvard-Architekturen gedacht ist, alle vtables im 
RAM hält. Das bedeutet, daß du pro Klasse und virtueller Memberfunktion 
Platz für einen Zeiger im RAM verbrauchst (und im Flash natürlich auch 
nochmal, für die Initialisierungswerte).

Autor: rw (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Übrigens habe ich es so implementiert wie von Klaus Wachtler 
vorgeschlagen.

Der gcc optimiert das switch/case weg und so ist der Code auch am 
leichtesten zu lesen/verstehen. Und genau das ist nicht zu verachten. :)

Autor: Klaus Wachtler (mfgkw)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Dafür hast du aber hoffentlich nicht ein knappes Jahr gebraucht, oder? 
:-)

Oder wolltest du die Lesbarkeit und Verständlichkeit nach einiger
Zeit testen?
Im Ernst: Das ist übrigens ein Test, der gemeinhin vernachlässigt
wird - m.E. zu Unrecht.

Autor: rw (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Aber sicher ;-)

Nö, ich bin grad mal bei der Suche nach was anderem über diesen Thread 
gestolpert und da dachte ich, schreib ich doch mal was dazu.

Geworden ist es damals übrigens eine generische Treiberklasse für die 
INTx-Eingänge des ATMega2560. Der hat ja 8 an der Zahl, wobei die 
Register immer bei 4 Stück gleich sind. Ich wollte mit minimalem 
Codeaufwand ein maximal effizientes Ergebnis in C++ haben.
Die Lektüre dieses Buches hat übrigens Templates für mich wirksam 
entzaubert: 
http://www.amazon.de/Templates-Complete-Nicolai-M-...

Ja, es ist gar nicht so einfach, lesbaren Code zu schreiben. Und Du hast 
recht: Wenn ich mir das Zeug von vor einem Jahr anschaue... ohje. Leider 
wird diese Disziplin an den Hochschulen nicht gelehrt, genauso wenig wie 
Codelesen. Kommt also nur durch Erfahrung.

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.