Hat man einige Flags (bool) etwa als statics in seinen Klassen, könnte
man dafür ja auch ein GPIOR im I/O-Bereich verwenden, das dann sbi/cbi
unterstützt, was der g++ ja auch entsprechend einsetzt.
Wie Frage entsteht allerdings, wie ordnet man die einzelnen Bits den
einzelnen Klassen sicher ohne Überschneidung zu. Etwa durch einen
Template-Parameter:
1
template<autoBit>classA{};
2
template<autoBit>classB{};
3
...
4
A<0>a;
5
B<1>b;
Wie verhindert man dann sicher, dass jemand später
1
A<1>c;
schreibt. Oder das zuviele Bits aus dem GPIOR verwendet werden. Das geht
sicher mit TMP ;-) Dann hat man einen Compilezeitfehler -> gut.
Oder macht man sich einen Registrar:
1
Registrar<GPIOR,0>reg;
2
3
Aa(reg);
4
Bb(reg);
5
...
Der Registrar stellt die unikate Vergabe der BitNummern sicher, bei
zuvielen gibts es leider nur einen Laufzeitfehler -> weniger gut.
Hat da jemand einen besseren Ansatz?
Dazu gibt es ja schon bereits einige funktionierende Ansätze. Aber die
sind alle meiner Meinung nach nicht sehr nützlich, weil:
a) Sprechenden Code kriegt man mit Makros viel einfach hin:
1
#define ALARM_LED_AUS PORTB &= (1<<PB4)
2
#define ALARM_LED_EIN PORTB |= (1<<PB4)
b) Spätestens wenn du mehrere Bits gleichzeitig ansteuern willst (z.B.
für ein LC-Diplay) nützt Dir so eine Bit orientierte Klasse gar nichts
mehr.
c) Kannst du für komplexere Sachen auch inline Funktionen verwenden, die
sind genau so effizient, wie Makros. Mit der Optimierungsstufe -O2
verwandelt der Compiler ohnehin einige solcher Funktionen von ganz
alleine in einzelne sbi/cbi Befehle:
Randy B. schrieb:> Hat man einige Flags (bool) etwa als statics in seinen Klassen, könnte> man dafür ja auch ein GPIOR im I/O-Bereich verwenden, das dann sbi/cbi> unterstützt, was der g++ ja auch entsprechend einsetzt.>> Wie Frage entsteht allerdings, wie ordnet man die einzelnen Bits den> einzelnen Klassen sicher ohne Überschneidung zu. Etwa durch einen> Template-Parameter:>>
1
>template<autoBit>classA{};
2
>template<autoBit>classB{};
3
>...
4
>A<0>a;
5
>B<1>b;
6
>
>> Wie verhindert man dann sicher, dass jemand später>>
1
>A<1>c;
2
>
>> schreibt. Oder das zuviele Bits aus dem GPIOR verwendet werden. Das geht> sicher mit TMP ;-) Dann hat man einen Compilezeitfehler -> gut.
Also die Nutzung von Bits die überhaupt nicht genutzt werden dürfen
lässt sich recht simpel durch static_assert(Bit < 31) oder ähnliches
abfangen.
Die doppelte Nutzung zu unterbinden ist da schon schwieriger und in
meinen Augen auch sehr gefährlich. Oft will man ja absichtlich Bits
doppelt nutzen!
Stefan U. schrieb:> Dazu gibt es ja schon bereits einige funktionierende Ansätze. Aber die> sind alle meiner Meinung nach nicht sehr nützlich, weil:> Blabla
Gratuliere zur sinnlosen Antwort.
Stefan U. schrieb:> c) Kannst du für komplexere Sachen auch inline Funktionen verwenden, die> sind genau so effizient, wie Makros. Mit der Optimierungsstufe -O2> verwandelt der Compiler ohnehin einige solcher Funktionen von ganz> alleine in einzelne sbi/cbi Befehle:
Das habe ich ja oben bereits geschrieben.
>
1
>voidalarm_led_aus()
2
>{
3
>PORTB&=(1<<PB4);
4
>}
5
>
6
>voidalarm_led_aus()
7
>{
8
>PORTB&=(1<<PB4);
9
>}
10
>
Und es geht nicht um Pins, sondern um bspw. GPIOR0
Moby schrieb im Beitrag #5242378:
> Randy B. schrieb:>> Hat da jemand einen besseren Ansatz?>> Ja. Sprich unmittelbar, direkt, kurz und knapp mit den Pins. Das macht> man in Asm und spart sich damit tausend überflüssige Überlegungen zur> Anwendung der aufgeblasenen C++ Bürokratie!
Es geht nicht um einen PORTx, sondern um bspw. GPIOR0. Und wie löse ich
das Verwaltungsproblem in Assembler?
Moby schrieb im Beitrag #5242390:
> Randy B. schrieb:>> Es geht nicht um einen PORTx, sondern um bspw. GPIOR0.>> Ist doch egal. In der Ansprache ist beides dasselbe.>>> Und wie löse ich>> das Verwaltungsproblem in Assembler?>> Du hast hier mit C++/OOP ein Problem welches mit Asm schlicht nicht> existiert :-)
Warum nicht?
Moby schrieb im Beitrag #5242402:
> Randy B. schrieb:>> Warum nicht?>> Gehörst Du zur Generation die nur noch in abstrakten Sphären unterwegs> ist? Jede Bodenhaftung zur Hardware verloren hat?
Heute nen schlechten Tag gehabt? Oder ist das Dein normaler Umgangston?
> Kennst Du Assembler> überhaupt? Du konstruierst hier mit C++ Probleme die überflüssig sind.> Mit Asm sprichst Du die vorab vereinbarten Bits direkt an. Da brauchts> keine Klassen, Template Parameter und erst recht keine> "Compilezeit"-Fehler...
Ich bin überzeugt, dass man das auch in ASM braucht. Auch dort gestaltet
man seinen Code modular und wiederverwendbar. Wie also stellt man
sicher, dass Modul1 und Modul2 nicht unbeachsichtigt dasselbe Bit im
GPIOR benutzen?
Moby schrieb im Beitrag #5242416:
> Randy B. schrieb:> Das stellt man sicher indem man es schlicht vereinbart.
Nun gut, das kann ich auch in C++ (s.o.).
> Einfach denken!> Einfach handeln!
Wenn der Programmierer beim Einsatz der Module richtig denkt und richtig
handelt, ist ja alles in Ordnung. Aber auch darum geht es nicht. Würden
wir nach der Devise alle handeln können, wäre SW fehlerfrei ...
> Der Asm-Programmierer hat seinen MC samt Code komplett> im Griff! Das kommt möglicherweise auch daher weil er nur seine konkrete> Anwendung und nicht allerlei künstliche Probleme, die sich nur aus der> Bürokratie einer Programmiersprache ergeben im Blick haben muß.
Es geht darum, dass der Code gar nicht erst falsch benutzt werden kann.
Ohne Doku, ohne alles im Blick zu haben. Falls doch, gibt es im besten
Fall einen Compilezeitfehler, etwas weniger gut einen Laufzeitfehler
durch eine Assertion.
Randy B. schrieb:> Moby schrieb im Beitrag #5242402:>> Randy B. schrieb:>>> Warum nicht?>>>> Gehörst Du zur Generation die nur noch in abstrakten Sphären unterwegs>> ist? Jede Bodenhaftung zur Hardware verloren hat?>> Heute nen schlechten Tag gehabt? Oder ist das Dein normaler Umgangston?
Der hat keinen schlechten Tag, sondern hier Hausverbot wegen schlechten
Benehmens, was, trotz dem bekannt rauen Umgangston hier, bisher nur
wenigen gelungen ist haben.
Randy B. schrieb:> Heute nen schlechten Tag gehabt? Oder ist das Dein normaler Umgangston?
Bedauerlicherweise ist das Mobys normaler Umgangston, und unter anderem
deswegen hat er hier auch Hausverbot. Leider hält ihn das nicht davon
ab, regelmäßig aufzutauchen und Threads zu stören, die C++ betreffen,
womit er eine ganz besondere Haßliebe pflegt. Bitte nicht antworten,
sondern melden, die Moderatoren kümmern sich dann darum.
Zum Thema: leider kenne ich auch keine andere "harte" Lösung, als eine
Registry zu implementieren, idealerweise vermutlich als Singleton. Wie
Du aber schon absolut korrekt erkannt hast, ergäbe das nur
Laufzeitfehler, obwohl das Problem natürlich besser zur Compilezeit
abgefangen würde.
Eine andere Idee wäre es, die Ressourcen per Enum oder Typedefs mit
Namen zu versehen und die Allokation an einer Stelle zu vereinen. Das
würde das Problem zwar nicht verhindern, aber man könnte es besser
sehen.
1
template<intT>classA_{};
2
template<intT>classB_{};
3
4
typedefA_<0>A;
5
typedefB_<1>B;
6
7
intmain(void){
8
Aa;
9
Bb;
10
11
return0;
12
}
Meine dritte und ebenfalls ziemlich unelegante Lösung wäre, den Code vor
der Kompilierung mit einem eigens dazu entwickelten Werkzeug zu parsen
und einen Fehler zu werfen, wenn Parameter mehrmals benutzt werden. C++
parsen ist allerdings...
Arduino F. schrieb:> [OT]> Der Bursche ist ja schlimmer, als Kurt...> [/OT]
Problem ist halt, daß er über den Rand seiner Nußschale nie
hinausgesehen hat, aber fest davon überzeugt ist das ganze
Tiny13-Universum zu überblicken. Wobei er selbst da schon mal baden
gegangen ist mit seiner "besser kleiner flexibler"-Theorie.
Sheeva P. schrieb im Beitrag #5242435
> Meine dritte und ebenfalls ziemlich unelegante Lösung [...]
Nachdem ich noch einmal etwas intensiver darüber nachgedacht habe, bin
ich zur Überzeugung gelangt, daß sich die Mühe eigentlich nicht lohnt.
Denn in einem Punkt hat unser Störenfried ja sogar sowas Ähnliches wie
ein wenig Recht, wenngleich in einem ganz anderen Kontext als er glaubt.
Es ist die Aufgabe des Entwicklers, logische Fehler wie solche
Mehrfachbelegungen zu vermeiden, egal in welcher Sprache. Und auch wenn
Compiler und Interpreter ziemlich gut darin sind, verschiedene Typen von
Fehlern zu erkennen und zu vermeiden, ist das bei logischen Fehlern
schwierig, oft sogar unmöglich.
Zumal, wie Vincent Hamp richtig anmerkt, das hier als "Fehler" gesehene
Verhalten manchmal durchaus nützlich und intendiert ist, und hier ist es
obendrein so, daß dies recht offensichtlich und mit bloßem Auge relativ
einfach zu erkennen ist. Insofern: laß' es, irgendwas muß der Entwickler
schließlich auch noch zu tun und zu bedenken haben. ;-)
Edit: PS: Arduino und Carl, bitte bleibt beim Thema und diskutiert nicht
über unseren Störenfried -- denn wenn Ihr das tut, hat er wieder einen
C++-Thread gestört und somit sein Ziel erreicht.
Was ich nicht nachvollziehen kann: wieso man ÜBERHAUPT unbedingt
einzelne Pins so verpacken will. Nur ist man in C++ anders als in Java
nicht zu OOP für alles gezwungen. Insbesondere bietet das hier nur
syntaktischen Zucker, aber keine wirkliche Abstraktion und auch nicht
mehr Übersichtlichkeit. Man hantiert danach nämlich immer noch mit
einzelnen Pins herum.
Ein sinnvolles Objekt wäre z.B. ein Display-Objekt, das eine Handvoll
IO-Pins beinhaltet und vor allem auch von diesen gleich abstrahiert. Da
kann man außerdem auch gleich den Bildschirmspeicher drin integrieren,
der nämlich der wesentliche Zustand des Objektes ist.
Fehler wie falsche Pinbelegung?! Also bitte, solche Fehler sind nicht
nur trivial, sondern auch schnell zu debuggen und entstehen ohne
Änderung vor allem auch nicht mehr wieder von Neuem.
Nop schrieb:> Was ich nicht nachvollziehen kann: wieso man ÜBERHAUPT unbedingt> einzelne Pins so verpacken will. Nur ist man in C++ anders als in Java> nicht zu OOP für alles gezwungen.
Natürlich.
> Insbesondere bietet das hier nur syntaktischen Zucker, aber keine> wirkliche Abstraktion und auch nicht mehr Übersichtlichkeit.
Die Idee von solchem "syntaktischem Zucker" ist es, die
Übersichtlichkeit zu verbessern, eine echte Abstraktion muß dabei nicht
einmal stattfinden. Nehmen wir ein einfaches Beispiel:
1
#include"hal.hpp"
2
3
intmain(void){
4
5
Motormotor(&DDRB,&PORTB,&PINB,PB1);
6
Beleuchtungbeleuchtung(&DDRB,&PORTB,&PINB,PB2);
7
8
Startknopfstartknopf(&DDRD,&PORTD,&PIND,PD1);
9
Bremsebremse(&DDRD,&PORTD,&PIND,PD2);
10
11
Getriebegang(&DDRD,&PORTD,&PIND,PD3);
12
13
while(1){
14
if(startknopf.gedrueckt()){
15
beleuchtung.ein();
16
if(gang.eingelegt()andbremse.gedrueckt()){
17
motor.ein();
18
}
19
}else{
20
motor.aus();
21
beleuchtung.aus();
22
}
23
}
24
}
> Man hantiert danach nämlich immer noch mit einzelnen Pins herum.
Natürlich. Aber im vorherigen Beispiel sind die Pins und ihre Funktionen
klar und offensichtlich benannt. Dabei ist zB. "startknopf.gedrückt()"
nur eine Art "Alias" für "InputPin::isHigh()" oder "PIND & (1<<PIND1)"
-- und egal, wie man es formuliert, der Compiler macht da nur ein "sbis
0x10, 1" daraus. Diese Minimal-"Abstraktion" optimiert der Compiler also
komplett weg, das kostet im Vergleich zu einer Lösung in C oder
Assembler rein gar nichts. Aber dafür ist der "Algorithmus" so sprechend
und offensichtlich, daß sogar die beste Ehefrau von allen ihn sofort
versteht.
Klar: man kann das alles auch in einem Verhau von entsprechend benannten
Funktionen, #defines etc. abbilden, und die eigentliche Stärke --
nämlich, daß der "Algorithmus" sowohl für Laien als auch für Entwickler
besonders lesbar und verständlich wird -- kommt bei so einem kleinen
Beispiel noch nicht wirklich heraus. Aber bei größeren und komplexeren
Algorithmen als diesem einfachen Beispiel sieht die Sache natürlich
völlig anders aus.
Insofern freue ich mich als Entwickler über jedes Zeichen Dokumentation,
das ich nicht schreiben oder lesen muß, weil der Programmlauf auch ohne
Dokumentation und ohne weiteres Nachdenken sofort verständlich ist. Also
investiere ich lieber eine halbe Stunde in eine derartige "Abstraktion",
erspare mir damit aber heute eine halbe Stunde Dokumentation, und
morgen, übermorgen, und immer, wenn ich den Code pflegen oder verändern
muß, je eine weitere halbe Stunde Lektüre von Code und Dokumentation.
Insofern kostet mich diese "Abstraktion" heute wenig bis nichts, nutzt
mir dafür aber zu späteren Zeitpunkten umso mehr.
> Ein sinnvolles Objekt wäre z.B. ein Display-Objekt, das eine Handvoll> IO-Pins beinhaltet und vor allem auch von diesen gleich abstrahiert. Da> kann man außerdem auch gleich den Bildschirmspeicher drin integrieren,> der nämlich der wesentliche Zustand des Objektes ist.
Das ist ein gutes Beispiel, in dem sich der OO-Ansatz dann wirklich auch
mal so richtig offensichtlich lohnt. Aber auch in Deinem Display-Objekt
vereinfacht es die Lesbarkeit, Schreibbarkeit und Verständlichkeit, wenn
Deine Pins intern mit sprechend benannten Methoden angesteuert werden.
Die Idee mit der Objektorientierung funktioniert nach meiner Erfahrung
insbesondere dann am Besten, wenn man sie konsequent durchzieht und am
Anfang beginnt, alles andere bleibt sonst Stückwerk. Das ist einer der
beiden Aspekte, unter denen der Ruf der OO in manchen Bereichen leider
immer noch leidet: viel zu viele Unternehmen und Entwickler verstehen
Objektorientierung immer noch als Marketing-Buzzword, um Modernität zu
signalisieren. Sie wollen (oder können) nicht verstehen, daß das eine
alternative Herangehens- und Denkweise ist, deren besondere Stärken und
Leistungsfähigkeit sich erst dann entfalten können, wenn sie konsequent
durchgehend und von Grund auf angewendet wird.
Nop schrieb:> Was ich nicht nachvollziehen kann: wieso man ÜBERHAUPT unbedingt> einzelne Pins so verpacken will. Nur ist man in C++ anders als in Java> nicht zu OOP für alles gezwungen. Insbesondere bietet das hier nur> syntaktischen Zucker, aber keine wirkliche Abstraktion und auch nicht> mehr Übersichtlichkeit. Man hantiert danach nämlich immer noch mit> einzelnen Pins herum.
Einzelne Pins machen natürlich für sich allein wenig Sinn. Wenn man aber
"PinSet"s hat, in die man diese verpacken kann und denen man auch einen
Werte-Typ verpassen kann, dann sieht das ganz anders aus.
Wobei da "Pins" nicht mal das beste Ziel sind. Bitsets in IO-Registern
lassen sich so verpacken, z.B. WGM eines AVR-Timers. Das ist manchmal
über mehrere physische Register verteilt und kann trotzdem ein Wert
behandelt werden, der einen bestimmten Typ hat (ein enum mit den für den
Controler erlaubten Werten) und trotzdem stehen im Binär-File nur die
minimal notwendigen Maschinenbefehle. Nie mehr die falschen Bits im
falschen Register auf einen ungültigen Wert setzen.
> Ein sinnvolles Objekt wäre z.B. ein Display-Objekt, das eine Handvoll> IO-Pins beinhaltet und vor allem auch von diesen gleich abstrahiert. Da> kann man außerdem auch gleich den Bildschirmspeicher drin integrieren,> der nämlich der wesentliche Zustand des Objektes ist.
Das wird in der Arduino-Lib versucht, allerdings so halbherzig, daß es
maximalen Runtime-Overhead produziert. Ein auf variadic templates
basierendes PinSet kann das besser.
> Fehler wie falsche Pinbelegung?! Also bitte, solche Fehler sind nicht> nur trivial, sondern auch schnell zu debuggen und entstehen ohne> Änderung vor allem auch nicht mehr wieder von Neuem.
Die "Änderung" entsteht schnell, wenn man auf dem 2€-China-Board
entwickelt, weil das so schön auf's Steckbrett paßt, USB-befüllbar ist,
aber einen Mega328 hat und nicht den Tiny24, der auf dem echten Board
sitzen soll. Mit den richtigen Templates sortiert der Compiler aber
alles in Sekundenbruchteilen um.
@sheeva: die Diskussion über Randerscheinung ging darum, jemand auf dem
Gebiet unerfahrenen zu informieren. Normalerweise mach ich da nur drei
Klicks.
Nop schrieb:> Was ich nicht nachvollziehen kann: wieso man ÜBERHAUPT unbedingt> einzelne Pins so verpacken will.
Es geht hier nicht um einzelne Pins, das habe ich schon mehrfach gesagt.
Hier geht es darum, die Bits in den GPIORs einiger ATTinys/ATMegas als
boole'sches Flag in irgendwelchen anderen Konstrukten zu verwenden. Mit
dem Hintergrund, dass eine normale bool Variable / Datenelement oder
BitFeld ja (mindestens) ein Byte belegt und der Zugriff auf die GPIOR im
IO-Bereich per sbi/cbi erfolgt.
Sheeva P. schrieb:> Nachdem ich noch einmal etwas intensiver darüber nachgedacht habe, bin> ich zur Überzeugung gelangt, daß sich die Mühe eigentlich nicht lohnt.
Das sehe ich auch so. Die minimale Einsparung an CPU-Zyklen ist
bestenfalls eine Mikrooptimierung, also rausgeschmissene
Entwicklungszeit und geht zu Lasten der Lesbarkeit und Wartbarkeit.
Ich spare mir auch die Mühe, mehrere Flags per Bitstruct in ein Byte zu
quetschen und gönne jedem Flag ein ganzes Byte.
Man ist ja schon lange nicht mehr fürs Platz sparen auf den ATtiny12
angewiesen, sondern kann einfach den pinkompatiblen ATtiny85 nehmen.
Peter D. schrieb:> Sheeva P. schrieb:>> Nachdem ich noch einmal etwas intensiver darüber nachgedacht habe, bin>> ich zur Überzeugung gelangt, daß sich die Mühe eigentlich nicht lohnt.>> Das sehe ich auch so.
Deswegen ja die Frage: wäre doch toll gewesen, wenn jemand da eine gute
Lösung gehabt hätte.
Die Frage kann man ja auch weiter stellen: wie verhindert man für den
Programmierer transparent, das zwei oder mehr SW-Konstrukte dieselbe
HW-Ressource in nicht vorgesehener Weise nutzen ...
> Die minimale Einsparung an CPU-Zyklen ist> bestenfalls eine Mikrooptimierung,
Wird wohl so sein ...
> also rausgeschmissene> Entwicklungszeit und geht zu Lasten der Lesbarkeit und Wartbarkeit.
Das sehe ich eben nicht so.
Carl D. schrieb:> Das wird in der Arduino-Lib versucht, allerdings so halbherzig, daß es> maximalen Runtime-Overhead produziert. Ein auf variadic templates> basierendes PinSet kann das besser.
Nur ist die Arduino-Lib aber auch nicht auf Performance ausgelegt,
sondern darauf, daß relativ fachfremde Leute damit schnell Ergebnisse
basteln können. Daß man dann nicht mit variadic templates ankommt, ist
schon OK, denn die sind schwieriger zu debuggen und werfen im Zweifel
kryptische Fehlermeldungen beim Compile, die länger als das ganze
Programm sind.
> Mit den richtigen Templates sortiert der Compiler aber> alles in Sekundenbruchteilen um.
Er sortiert es auch mit den richtigen Defines schnell um. Logischerweise
würde man im Treiber fürs Display nicht mit haufenweise PA3 um sich
werfen, sondern man würde mit DISP_RW usw. arbeiten, was geeignet
definiert wird. Dann ist das an genau einer Stelle zu ändern - nicht
anders als mit der Template-Lösung, wo der Compiler es ja auch nur
umsortiert, wenn man es denn ändert. Der einzige Unterschied ist bloß,
daß man einmal ein Define ändert, das andere mal eine
Template-Instanzierung.
Und kein Template wird Dich davor schützen, daß Du RS in der vorigen
Hardware auf Pin2 hattest, jetzt aber im endgültigen Layout auf Pin3 und
vergessen hast, das anzupassen. Außer Du schreibst ein Template, welches
auch gleich noch den Schaltplan versteht. ;-)
Was mich mal interessieren würde bezgl. Templates: angenommen, man hat
so vier Datenpins auf demselben Port, dann würde man die ja zugleich
setzen. Liegen sie aber auf verschiedenen Ports, sind es mehrere
Befehle. Ist es möglich, ein Template so zu schreiben, daß es diese
Situationen automatisch erkennt und den jeweils optimalen Code erzeugt?
Randy B. schrieb:> Die Frage kann man ja auch weiter stellen: wie verhindert man für den> Programmierer transparent, das zwei oder mehr SW-Konstrukte dieselbe> HW-Ressource in nicht vorgesehener Weise nutzen ...
Der Knackpunkt ist "in nicht vorgesehener Weise". Wenn es exklusiver
Zugriff sein soll, läuft es auf einen Allokator hinaus. Wenn nicht
exklusiv, aber auch nicht beliebg für jeden, auf ein Rechtesystem.
Ich finde auch, dass das zunächst einmal eine sinnvolle generelle Frage
ist: wie verwaltet man zur Compilezeit die HW-Ressourcen zulässig.
Man kann das mit den Bits tatsächlich machen. Die Idee dazu ist, sich
eine Meta-Funktion (also ein template, dass ein Type-Mapping durchführt)
zu schreiben, die die Bits zur Compilezeit verwaltet und damit die
Ressourcen-Typen parametriert.
Ich habe das vor einiger Zeit einmal geschrieben, als ich mir eine
Meta-Funktionsbibliothek erstellt habe. Eigentlich nicht mit diesem
Hintergrund, sondern nur, um eine generelle Meta-Funktions-Bibliothek zu
haben, mit der ich Typen, Typ-Listen und Template-Listen bearbeiten
kann.
Wer sich noch nicht mit TMP beschäftigt hat, sollte das untige einfach
vergessen ...
Ich poste hier mal ein Beispiel, wie es prinzipiell funktionieren kann.
Das Prinzip ist, dass Abstraktionen A und B jeweils ein Flag benötigen.
Das kann man per Hand parametrieren und dabei ggf. einen Fehler machen.
Mann kann es aber auch den Controller erledigen lassen. Der Controller
ist eine MetaKlasse, die die Bits verwaltet und hier z.B. A und B
parametriert. Die Metafunktion get<> berechnet den notwendigen konkreten
Typ für die Abstraktion A und B.
1
// Beispiel einer Ressource, die ein Flag benötigt
2
template<typenameT,typenameFlag>
3
structAfinal{
4
A()=delete;
5
staticvoidf(){
6
Flag::set();
7
}
8
inlinestaticTmData{};
9
};
10
11
// Beispiel einer Ressource, die ein Flag benötigt
12
template<typenameFlag,typenameT>
13
structBfinal{
14
B()=delete;
15
staticvoidf(){
16
if(Flag::isSet()){
17
Flag::reset();
18
}
19
}
20
inlinestaticTmData{};
21
};
22
23
// für den Controller müssen die zu parametrierenden Typen genau einen Parameter haben, das Flag
24
// diese Meta-Funktionen bildet den Typ A<...> auf AF<Flag> für den Controller ab
25
template<typenameFlag>
26
usingAF=A<uint8_t,Flag>;
27
28
// diese Meta-Funktionen bildet den Typ B<...> auf BF<Flag> für den Controller ab
29
template<typenameFlag>
30
usingBF=B<Flag,uint16_t>;
31
32
// registrieren der Ressourcen Typen (templates) im Controller
33
usingcontroller=Controller<flagRegister,AF,BF>;
34
35
// ermitteln der durch den Controller konkret erzeugten Typen
36
usinga=controller::get<AF>::type;
37
usingb=controller::get<BF>::type;
38
39
intmain(){
40
a::f();
41
b::f();
42
}
Die Mapper AF und BF sind nicht nötig, wenn bspw. der
Flag-Template-Parameter immer der einzige Template-Parameter ist ...
Um gleich unschönen Kommentare vorzubeugen: das obige Beispiel
funktioniert. Allerdings fehlt dazu natürlich der Controller und die
Meta-Funktionsbibliothek. Der Controller ist recht kurz, die Biblothek
schon recht umfangreich, deswegen hier nicht gezeigt. Wer aber daran
wirklich interessiert ist, dem zeige ich es ....
Wen wundert es da noch wenn C++ immer wieder zerrissen wird? Ich kann
Moby's asm-Verbortheit auch nicht ab, aber das ist das krasse
Gegenbeispiel. Welchen Vorteil soll das alles irgendjemanden bringen?
Auch beim Debuggen kommt da immer wieder große Freude auf. Außerdem
bringen diese Konstrukte erst dann ihre angedachte Performance, wenn man
die Optimierung auf Werte stellt die ein vernünftiges Debuggen
verhindert.
Wilhelm M. schrieb:> Allerdings fehlt dazu natürlich der Controller und die> Meta-Funktionsbibliothek. Der Controller ist recht kurz, die Biblothek> schon recht umfangreich, deswegen hier nicht gezeigt. Wer aber daran> wirklich interessiert ist, dem zeige ich es ....
Ich für meinen Teil hab da genug gesehen. Wie konnten bisher die ganzen
Autos nur fahren, Waschmaschinen waschen, Geschirrspüler spülen...
Wilhelm M. schrieb:> Das kann man per Hand parametrieren und dabei ggf. einen Fehler machen.
Das rechtfertigt den Aufwand in die Romane und die Knoten im Hirn die
man braucht um das zu durchblicken in keiner Weise. Die Gefahr einen
Bufferüberlauf zu programmieren halte ich für kleiner als in diesem
ganzen Geraffel Fehler zu machen.
temp schrieb:> Wilhelm M. schrieb:>> Das kann man per Hand parametrieren und dabei ggf. einen Fehler machen.> Das rechtfertigt den Aufwand in die Romane und die Knoten im Hirn die> man braucht um das zu durchblicken in keiner Weise. Die Gefahr einen> Bufferüberlauf zu programmieren halte ich für kleiner als in diesem> ganzen Geraffel Fehler zu machen.
Mmh, ja in C ist das wohl recht häufig mit den Overflows ...
Du argumentierst gegen eine Bibliothek, die - wie jede andere auch -
transparent für den Anwender sein soll. Oder schaust auch immer bis zum
letzten Statement in die C/C++-Bibliotheken rein, die Du benutzt?
Ausserdem habe ich oben davon gesprochen, dass man das so machen kann,
nicht sollte oder muss. Für mich war es mehr eine Fingerübung in TMP.
Und ich gebe zu, dass TMP sich nicht jedem erschließt, weil man sich
damit im wesentlichen eine Typ-Algebra schafft. Mit Typen statt mit
Objekten und ihren Werten zu rechnen, ist halt sehr ungewohnt. Hat aber
eben den großen Vorteil, dass davon zur Laufzeit nichts übrig bleibt,
weil es Typen im Maschinencode nicht gibt.
Sheeva P. schrieb:> Nehmen wir ein einfaches Beispiel:> #include "hal.hpp"
Ich wäre dir sehr verbunden, wenn du die hal.hpp, oder einen Link da
hin, mal zeigen würdest.
Ich würde mir das gerne mal anschauen.
Nop schrieb:> Was mich mal interessieren würde bezgl. Templates: angenommen, man hat> so vier Datenpins auf demselben Port, dann würde man die ja zugleich> setzen. Liegen sie aber auf verschiedenen Ports, sind es mehrere> Befehle. Ist es möglich, ein Template so zu schreiben, daß es diese> Situationen automatisch erkennt und den jeweils optimalen Code erzeugt?
Ding Ding Ding!
Ja, das geht. Und genau aus diesem Grund ist ein vernünftiges Template
wartbarer und in der Nutzung simpler als der entsprechende C-Code.
Man stelle sich etwa vor, man hätte eine QSPI-Schnittstelle mit 6(?
sinds glaub ich...?) und davon liegt die Hälfte auf Unterschiedlichen
Ports. Allein die Tatsache, dass ich in einer einzigen Zeile der
Projekt-Config sowas wie:
1
usingQspiPins=Gpio<A<2,4>,C<8,9,10,13>>;
schreiben kann ist einfach Gold Wert.
In C hätte ich mindestens sowas wie:
1
#define QSPI_CLK_PORT GPIOC
2
#define QSPI_D0_PIN 8
3
#define QSPI_D1_PIN 9
4
#define QSPI_D2_PIN 10
5
#define QSPI_D3_PIN 13
6
#define QSPI_CS_PORT GPIOA
7
#define QSPI_CS_PIN 2
8
#define QSPI_... erm ka was PIN 3
Persönlich brauch ich das für das Konfigurieren von Ports auch viel
öfter, als fürs eigentliche Bit-Banging... Weil, ganz ehrlich, so viel
Bit-Banging macht man mit modernen Prozessoren ohnehin nicht mehr.
Vincent H. schrieb:> Allein die Tatsache, dass ich in einer einzigen Zeile der> Projekt-Config sowas wie:using QspiPins = Gpio<A<2, 4>, C<8, 9, 10,> 13>>;> schreiben kann ist einfach Gold Wert.
Und im erzeugten Assembler-Code fällt dann am Ende für die beiden Pins
auf A nur eine RMF (read, modify, write) auf den Port raus, wenn man die
zugleich schreiben will? Würde man hingegen den Pin A4 stattdessen auf
C4 verlegen, sie aber immer noch zugleich schreiben, dann wären es
automatisch zwei RMF?
Nop schrieb:> Vincent H. schrieb:>> Allein die Tatsache, dass ich in einer einzigen Zeile der>> Projekt-Config sowas wie:using QspiPins = Gpio<A<2, 4>, C<8, 9, 10,>> 13>>;> schreiben kann ist einfach Gold Wert.>> Und im erzeugten Assembler-Code fällt dann am Ende für die beiden Pins> auf A nur eine RMF (read, modify, write) auf den Port raus, wenn man die> zugleich schreiben will? Würde man hingegen den Pin A4 stattdessen auf> C4 verlegen, sie aber immer noch zugleich schreiben, dann wären es> automatisch zwei RMF?
Ja.
Nop schrieb:> Vincent H. schrieb:>>> Ja.>> Uih. Na, danke. (:
An jener Stelle sei trotzdem angemerkt, dass der notwendige
"BoilerPlate" je nach Feature-Umfang recht groß sein kann. Typen und
Werte-Listen durchsuchen, sortieren usw. gehört da alles dazu. Je nach
Architektur muss man dann auch noch aufpassen wohin man schreibt, ob man
Werte nicht vorher irgendwie umhershiften muss usw. usf.
Belohnt wird der Aufwand aber wie oben schon gschrieben mit viel viel
Wartungsleichtigkeit.
Vincent H. schrieb:> An jener Stelle sei trotzdem angemerkt, dass der notwendige> "BoilerPlate" je nach Feature-Umfang recht groß sein kann.
Klar, mir ging's auch nur um die prinzipielle technische Möglichkeit.
Vincent H. schrieb:> using QspiPins = Gpio<A<2, 4>, C<8, 9, 10, 13>>;
Der Nachteil ist allerdings, daß nicht mehr erkennbar ist, welcher Pin
in der Liste welche Bedeutung hat.
Man muß es erst in der Doku zu "using QspiPins" nachlesen.
Schnell sind mal Pins verwechselt und dann wäre es schön, wenn man
direkt das Headerfile mit dem Schaltplan vergleichen kann.
Peter D. schrieb:> Vincent H. schrieb:>> using QspiPins = Gpio<A<2, 4>, C<8, 9, 10, 13>>;>> Der Nachteil ist allerdings, daß nicht mehr erkennbar ist, welcher Pin> in der Liste welche Bedeutung hat.> Man muß es erst in der Doku zu "using QspiPins" nachlesen.> Schnell sind mal Pins verwechselt und dann wäre es schön, wenn man> direkt das Headerfile mit dem Schaltplan vergleichen kann.
Das kann man auch mit named-constants, dann spielt sogar die Reihenfolge
in der Parameterliste keine Rolle mehr
Wieso wollt ihr überhaupt eine neue Lösung?
Die Compiler liefern ja bereits Definitionen mit. Leider nach
unterschiedlichen Ansätzen.
In Summe kommen wir noch am besten bei weg, wenn alle Libraries das
Konzept der mitgelieferten Include-Dateien benutzen.
Besser eine einzige unzulängliche Lösung, als ein Sammelsurium von
inkompatiblen Lösungen.
Noch einer schrieb:> Wieso wollt ihr überhaupt eine neue Lösung?>> Die Compiler liefern ja bereits Definitionen mit. Leider nach> unterschiedlichen Ansätzen.
Der Compiler liefert diesbezüglich gar nichts mit. Das kommt aus den
Herstellerbibliotheken.
>> In Summe kommen wir noch am besten bei weg, wenn alle Libraries das> Konzept der mitgelieferten Include-Dateien benutzen.
Hier werden nichts anderes als #include verwendet: das sind alles
header-only Bibliotheken (noch ein Vorteil).
Nop schrieb:> Vincent H. schrieb:>>> Ja.>> Uih. Na, danke. (:
Nachdem du mich sinngemäß schon mal das selbe gefragt hattes, auch von
mir ein deutliches JA.
Bei dem Beispiel 4-Bit LCD kann man Data als PinSet aus 4 Pins
deklarieren, die Steuerleitungen einzeln oder auch zusammengefaßt an das
LCD-Template übergeben und dann Code schreiben, der keine Rücksicht mehr
auf die tatsächlichen Pin-Belegungen nehmen muß. Kann man nur hier nicht
veröffentlichen, weil schon die Erwähnung der Implementierungssprache
die zuständigen Forumsdeppen anzieht. Ich lebe zwar von Software, aber
die Rechner auf denen die läuft, ist etwas größer als ein Tiny13, ich
beschäftige mich aber (zum Entspannen) mit C++, besonders seit C++11 ist
das sehr spannend, und will einerseits Abstraktion und andererseits
bestmöglichen MaschinenCode. Erwähnte F.D.en sind da mental ein paar LJ
zurück ;-)
Aber ja, es geht und z.B. bei der Abbildung der AVR-Timer-WGM-Bits als
3..5Bit-Feld, darf sich dieses, wie bei den Megas auch auf mehrere
Timer-Register erstrecken. Und das Bitset bekommt einen enum Value-Typ,
d.h. es kann kein ungültiger Wert in WGM[4..0] stehen. Ob es sich für's
Hobby lohnt? Wenn ich davon leben müßte, wollte ich es jedenfalls nicht
anders wollen.
Der GCC entscheidet sich übrigens gegen RMW, wenn er statt dessen
2*cbi/SBI verwenden kann. Ist kürzer ;-)
Wilhelm M. schrieb:> Das kann man auch mit named-constants, dann spielt sogar die Reihenfolge> in der Parameterliste keine Rolle mehr> using clockPin = Spi::Clock<Pin<PortB, 1>>;> using dataPin = Spi::Data<Pin<PortC, 2>>;>> using spi = Spi<dataPin, clockPin>;Vincent H. schrieb:> using QspiPins = Gpio<A<2, 4>, C<8, 9, 10, 13>>;Carl D. schrieb:> Bei dem Beispiel 4-Bit LCD kann man Data als PinSet aus 4 Pins> deklarieren, die Steuerleitungen einzeln oder auch zusammengefaßt an das> LCD-Template übergeben und dann Code schreiben, der keine Rücksicht mehr> auf die tatsächlichen Pin-Belegungen nehmen muß.
Wo kann ich Informationen finden wie ich sowas mit Templates umsetzen
kann? Gibt es schon Open Source Libs die diese Konzepte verwenden? Ich
finde es sehr interessant und würde solche Konstrukte gerne in meinen
nächsten Projekten verwenden.
Mcucpp benutzt diese Technik mit TemplateMetaProgramming.
Ist sehr sporadisch bis gar nicht kommentiert, aber sehr konsequent
geschrieben. Die Beispiele und vor allem die Includes genau studieren.
GPIO -> 7Seg -> Lcd -> Usart -> Timer war mein Weg das Prinzip zu
verstehen.
Usart zeigt einen konsequenten Weg mit /avr/basic_usart und
avr/uc_type/usart Hardwarespezifisches zu kapseln.
Dieses Prinzip von Usart kann auch auf Timer angewendet werden.
************************************************
AVR ist als Einstieg besser geeignet als STM32.
************************************************
https://github.com/KonstantinChizhov/Mcucpp
Da ich befürchte, dass meine Frage im Gemülle des Priesters
untergegangen ist, möchte ich sie gerne wiederholen:
Sheeva P. schrieb:
> Nehmen wir ein einfaches Beispiel:> #include "hal.hpp"
Ich wäre dir sehr verbunden, wenn du die hal.hpp, oder einen Link da
hin, mal zeigen würdest.
Das würde mir gerne mal anschauen.
> Warum bekommen die Admins das nicht in den Griff?
Man müsste den Zugang für Gäste verweigern. Allerdings würde das
vermutlich die Anzahl der Besucher und somit die Einnahmen durch Werbung
reduzieren. Ich vermute mal, dass Andreas darauf nicht verzichten
möchte.
Im Groben und Ganzen haben die Moderatoren das Forum allerdings im
Griff. Ich hatte selbst mal eins betrieben, das wurde von Trollen und
Spammern mehrmals pro Monat so verhunzt, dass ich das ganze Forum
irgendwann gelöscht hatte.
Arduino F. schrieb:> Da ich befürchte, dass meine Frage im Gemülle des Priesters> untergegangen ist, möchte ich sie gerne wiederholen:>> Sheeva P. schrieb:> Ich wäre dir sehr verbunden, wenn du die hal.hpp, oder einen Link da> hin, mal zeigen würdest.
Die ist nicht untergegangen, ich war nur unterwegs. ;-)
Sheeva P. schrieb:> Die ist nicht untergegangen,
Danke!
Habe das mal untersucht!
Winzige Änderungen, und es läuft in der Arduino IDE.
Jut!
Einen Vorteil hat es, man kann die Dinge in ein Array stopfen und drüber
weg iterieren.
Dieses Feature wird erkauft, durch Ram verbrauch.
Dieses, dein Beispiel, verbrät 35 Byte auf dem Stack.
Heimlich.
Wenn man das "in ein Array" Feature nicht braucht, ist der Speicher
verschwendet.
---
Dass das PINDEF Macro in meiner Arduinowelt nicht unbedingt zielführend
ist, ist ein anderes Problem, welches einer Nacharbeit würdig ist.
In der Arduino Welt sind halt die Arduino Pinnummern verbreitet, und
ihre Verwendung würde damit "Dem Prinzip der geringsten Verwunderung"
entsprechen.
---
Result:
Ich werde es tiefer untersuchen, bis ich alle Details klar sehe.
Vermutlich/Sicherlich kann ich davon noch was mitnehmen.
Denke aber, dass ich meine Variante weiter entwickeln werde.
Arduino F. schrieb:> Sheeva P. schrieb:>> Die ist nicht untergegangen,>> Danke!>> Habe das mal untersucht!> Winzige Änderungen, und es läuft in der Arduino IDE.>> Jut!> Einen Vorteil hat es, man kann die Dinge in ein Array stopfen und drüber> weg iterieren.> Dieses Feature wird erkauft, durch Ram verbrauch.>>> Dieses, dein Beispiel, verbrät 35 Byte auf dem Stack.
Alternative:
1
#include"mcu/avr8.h"
2
#include"mcu/ports.h"
3
4
usingnamespaceAVR;
5
6
usingPortB=Port<AVR::ATMega328P::PortRegister,B>;
7
usingPortD=Port<AVR::ATMega328P::PortRegister,D>;
8
9
usingmotor=Pin<PortB,1>;
10
usingbeleuchtung=Pin<PortB,2>;
11
usingstartknopf=Pin<PortD,1>;
12
usingbremse=Pin<PortD,2>;
13
usinggang=Pin<PortD,3>;
14
15
intmain(){
16
motor::dir<Output>();
17
beleuchtung::dir<Output>();
18
startknopf::dir<Input>();
19
bremse::dir<Input>();
20
gang::dir<Input>();
21
22
while(true){
23
if(startknopf::isHigh()){
24
beleuchtung::on();
25
if(gang::isHigh()andbremse::isHigh()){
26
motor::on();
27
}
28
}else{
29
motor::off();
30
beleuchtung::off();
31
}
32
}
33
}
erginbt Stackverbrauch / Ramverbrauch == 0
(Wenn gewünscht, kann man die Namen noch mappen, z.B. on() ->
eingeschaltet())
ASM dazu:
Wilhelm M. schrieb:> Alternative:
Danke.
Aber, leider, gefällt mir die auch nicht!
Da sind mir erheblich zu viele Doppelpunkte drin.
Und, noch wesentlicher:
Man kann einen als Input deklarierten Pin zum Output machen, ohne dass
der Kompiler aufschreit.
OK...
Ich sehe schon...
Wer kritisiert, muss auch eigene Leistung zeigen!
Darum im Anhang meine vorläufige PinDefinition.h
Diese ist noch nicht fertig!
Darum bitte ich um ein bisschen Nachsicht, denn da sind noch einige
Unsauberkeiten drin.
Aber sie zeigt, wie man ohne Ramverbrauch, ohne static, von den Arduino
Pin Bezeichnern zu den Registern kommt.
Hier das zugehörige Testprogram:
(0 Byte Ram, 22 Byte Flash, mehr gegenüber einem leeren Programm)
Arduino F. schrieb:> 22 Byte Flash
Da habe ich mich verrechnet:
Es sind 28 Byte
Im Anhang eine geringfügig korrigierte Version, welche keine Warnings
mehr wirft.
Arduino F. schrieb:> Winzige Änderungen, und es läuft in der Arduino IDE.
Das wundert mich nicht, aber dafür war es natürlich nicht gedacht. Ich
persönlich bevorzuge den "klassischen" Weg: einen guten Editor (bei mir
GNU Emacs) und eine leistungsfähige, moderne Kommandozeile (bei mir:
bash) und für größere Programme aus mehreren Einheiten natürlich GNU
make.
> Einen Vorteil hat es, man kann die Dinge in ein Array stopfen und drüber> weg iterieren.> Dieses Feature wird erkauft, durch Ram verbrauch.>> Dieses, dein Beispiel, verbrät 35 Byte auf dem Stack.
Mit -Os verbraucht mein Beispiel nicht mehr als eine äquivalente (und
IHMO weniger gut lesbare) "PORTD & (1<<PD1)"-Version in C, wenn ich mich
richtig erinnere.
> Dass das PINDEF Macro in meiner Arduinowelt nicht unbedingt zielführend
Du kannst statt "(PINDEF(D, PD5)" aber auch "(&DDRD, &PORTD, &PIND,
PD5)" benutzen. Das ist zwar auch nicht direkt Arduino-kompatibel, aber
für Arduino-User vielleicht etwas offensichtlicher.
Sheeva P. schrieb:> Mit -Os verbraucht mein Beispiel nicht mehr als eine äquivalente (und> IHMO weniger gut lesbare) "PORTD & (1<<PD1)"-Version in C, wenn ich mich> richtig erinnere.
Jeweils 7 Byte RAM für die Objekteigenschaften.
Womit man die 35 Byte erreicht hätte, wenn ich mich nicht irre.
Auch wenn der Kompiler das jeweilige this fest ins Flash kompiliert,
wird es doch genutzt. Also nicht gänzlich weg optimiert.
Das ist der Preis, für die Möglichkeit, es ins Array zu stopfen zu
können.
Dann kommen allerdings noch zwangsläufig die Zeiger/Referenzen dazu.
Dass mir Makros nicht gefallen, kommt noch erschwerend dazu.
> Jedes eliminierte Makro ist ein gutes Makro.
Außerdem erzeugt die Version mit "pad12" (analog wie von dir verwendet)
ein Objekt im Static Storage. Mit globaler Optimierung wird das zwar
u.U. wegoptimiert, toll ist das aber trotzdem nicht:
class dump:
1
Class PinDefinition::OutputPin<12>
2
size=1 align=1
3
base size=0 base align=1
asm:
1
.global pad12
2
.section .bss
3
.type pad12, @object
4
.size pad12, 1
5
pad12:
6
.zero 1
Die "portList" wird für große Devices "etwas" unübersichtlich, da wäre
eine Lösung mit Portname wie "PortD+Pad1" oder "PortD1" m.E.
vorzuziehen.
Und an einigen Stellen hat g++ rumzumäkeln:
1
PinDefinition.h: In instantiation of 'void PinDefinition::OutputPin<arduinoPin>::setHigh() [with unsigned char arduinoPin = 12]':
Das Beispiel von SheevaPlug mit Mcucpp formuliert:
*************************************************
Mit AtmelStudio in Mcucpp\examples\ ein neues Projekt anlegen und die
generierte Projekt.cpp mit der angefügten SheevaPlug.cpp ersetzen.
Unter Directories in Projekt|Eigenschaften Pfad auf
..\mcucpp
..\mcucpp\avr eintragen.
Johann L. schrieb:> Fehlt>> Also z.B.
Arduino halt, da ist byte schon definiert.
Johann L. schrieb:> Wenn ich ein kleines Beispiel übersetze, bekomm ich gerne mal für fast> jeden Zugriff eine Funktion (Übersetzt mit avr-g++ v8 -Os ...)>...> Außerdem erzeugt die Version mit "pad12" (analog wie von dir verwendet)> ein Objekt im Static Storage. Mit globaler Optimierung wird das zwar> u.U. wegoptimiert, toll ist das aber trotzdem nicht:
Beides sehe ich im Kompilat nicht.
Werde ich ausgiebiger testen.
Johann L. schrieb:> Und an einigen Stellen hat g++ rumzumäkeln:
Meiner ist ruhig.
Johann L. schrieb:> Die "portList" wird für große Devices "etwas" unübersichtlich,
Stimmt.
Ist aber auch nicht für menschliche Augen gedacht.
Maschinen generierter, wartungsfreier, Code.
Ein Umbau auf ein (übersichtlicheres) 2D Array ist trivial.
Werde ich überdenken.
Arduino F. schrieb:> Sheeva P. schrieb:>> Mit -Os verbraucht mein Beispiel nicht mehr als eine äquivalente (und>> IHMO weniger gut lesbare) "PORTD & (1<<PD1)"-Version in C, wenn ich mich>> richtig erinnere.>> Jeweils 7 Byte RAM für die Objekteigenschaften.> Womit man die 35 Byte erreicht hätte, wenn ich mich nicht irre.>> Auch wenn der Kompiler das jeweilige this fest ins Flash kompiliert,> wird es doch genutzt. Also nicht gänzlich weg optimiert.>> Das ist der Preis, für die Möglichkeit, es ins Array zu stopfen zu> können.
Was willst Du denn in ein Array "stopfen"?
>> Shiqq, könntest Du vielleicht bitte noch die Header beifügen? THX!
Mmh, das wird in Summe wohl recht viel ... aber ich schaue mal, wie ich
das zusammenkoche, so dass es ein nachvollziehbares Beispiel bleibt. Wie
aber nicht morgen sein ... (und ich ahne schon, welche Kommentare dazu
kommen ;-))
Arduino F. schrieb:> Sheeva P. schrieb:>> Mit -Os verbraucht mein Beispiel nicht mehr als eine äquivalente (und>> IHMO weniger gut lesbare) "PORTD & (1<<PD1)"-Version in C, wenn ich mich>> richtig erinnere.>> Jeweils 7 Byte RAM für die Objekteigenschaften.
Wie hast Du das denn ermittelt? Mit avr-size oder Kopfrechnen? Und hast
Du die Kompilate mal gestrippt?
Denn bei mir optimiert mein avr-g++ (v 4.9.2) das komplett weg -- die
C++-Version hat mit avr-size genau vier Byte mehr, weil die
Datenrichtungsbits im Konstruktor der Input-Pins explizit gelöscht
werden (Pin.hpp Zeile 53). Wenn ich diese (eigentlich überflüssige)
Veranstaltung auskommentiere, ist die C++-Version sogar zwei Byte
kleiner (140 Byte) als die C-Version.
Anbei die C-Version, die ich zum Vergleich benutze; sie kommt mit -Os
und einer anschließenden Nachbehandlung mit avr-strip auf 142 Byte:
Wilhelm M. schrieb:> Mmh, das wird in Summe wohl recht viel ... aber ich schaue mal, wie ich> das zusammenkoche, so dass es ein nachvollziehbares Beispiel bleibt. Wie> aber nicht morgen sein ... (und ich ahne schon, welche Kommentare dazu> kommen ;-))
Das hat keine Eile -- aber um Beispiele nachvollziehen und sie mit
anderen Implementierungen vergleichen zu können, sollten sie natürlich
vollständig sein. Dies insbesondere auch deswegen, weil Du IMHO einer
der erfahrensten und fähigsten C++-Entwickler hier im Forum bist, und
ich deswegen geradezu darauf brenne zu sehen, wie Du die Sache angehst.
;-)
Sheeva P. schrieb:> Denn bei mir optimiert mein avr-g++ (v 4.9.2) das komplett weg
Gerade nochmal mit dem Docker-Image dea82/avr-gcc-docker mit avr-g++
7.2.0 getestet:
C++-Version mit explizitem Setzen der Datenrichtung bei Inputs 146 Byte,
ohne explizites Setzen der Datenrichtung 140 Byte, äquivalente C-Version
142 Byte.
Alles jeweils übersetzt mit -Os, nachbehandelt mit avr-strip, und die
Größen ermittelt mit avr-size.
Moby, hast du keine Angst vor einer Schadens-Ersatz Klage?
Und: Was denkst du bei welcher Art Menschen du Dich durch dein Verhalten
beliebt machst? Sind das die Menschen, die Du um Dich haben möchtest?
Was ist mit den anderen, hast du es nicht mehr nötig, mit normalen
Leuten zusammen zu leben?
Wie würdest du es finden, wenn ich immer wieder in Deiner Wohnung
auftauchen würde, obwohl du es untersagt hättest?
Sheeva P. schrieb:> C++-Version mit explizitem Setzen der Datenrichtung bei Inputs 146 Byte,> ohne explizites Setzen der Datenrichtung 140 Byte, äquivalente C-Version> 142 Byte.
Mcucpp-Version erzeugt für ATmega32 identische Werte.
Ist aber vom Device abhängig.
Wilhelm M. schrieb:> Besser: enum class byte : uint8_t {};
Da ich von C++ auch keinen blassen Schimmer habe, traue ich mich einfach
mal zu fragen: Warum soll "enum" an dieser Stelle besser sein? Hier gibt
es doch gar nichts zu enumerieren. Welchen technischen Grund gibt es,
möglichst nicht das zu schreiben, was man meint? Ich bin verwirrt!
Mox schrieb:> Hier gibt> es doch gar nichts zu enumerieren.
Nunja...
Eine Variable vom Type Byte kann halt nur eine begrenzte Anzahl Werte
annehmen.
Eine Grundeigenschaft von enum.
--------------
In der Arduino.h ist es so definiert:
typedef uint8_t byte;
Was natürlich für AVRs + ARM schon ok ist, aber bei Prozessoren, wo ein
Byte eben keine 8 Bit beinhaltet (sondern z.B. 9 Bit) eben doch einen
Fehler darstellt.
Typedefs wie "typedef uint8_t byte;" haben das Problem, dass nicht immer
auf den ersten Blick erkenntlich ist, welches Token denn jetzt neu
definiert wird.
OK, bei dem Typedef geht es noch. Aber es gibt auch erheblich komplexere
Definitions Anweisungen, wo dann das zu definierende Token irgendwo
mitten drin steckt.
Das modernere "using byte = uint8_t;" wäre da viel klarer und damit
lesbarer.
Mox schrieb:> Wilhelm M. schrieb:>> Besser: enum class byte : uint8_t {};>> Da ich von C++ auch keinen blassen Schimmer habe, traue ich mich einfach> mal zu fragen: Warum soll "enum" an dieser Stelle besser sein? Hier gibt> es doch gar nichts zu enumerieren. Welchen technischen Grund gibt es,> möglichst nicht das zu schreiben, was man meint? Ich bin verwirrt!
Datentypen sollten genau das ausdrücken können, wozu sie gedacht sind:
ein Datentyp besteht aus Werten und zulässigen Operationen. Und ein
Datentyp sollte eine eindeutige Semantik haben.
Meine ich eine vorzeichenlose Ganzzahl mit 8-Bit Breite, so nehme ich
ein uint8_t.
Meine ich nur eine Sammlung von 8-Bits, so nehme ich ein byte.
Für eine Ganzzahl sind typischerweise die arithmetischen Operationen
möglich und sinnvoll. Bei einem Byte aber ist das aber zunächst einmal
sinnlos: die Bits einem Status- oder Port-Register sind meistens nicht
als ganze Zahl zu interpretieren. Deswegen ist es nicht gut, wenn man
derartige arithmetische Operationen möglich sind. Mit einem byte wie
oben kann man erst gar nichts ausser kopieren: ein byte ist keine Zahl
oder ein Zeichen.
Die eingebauten (primitiven) Datentypen bezeichnet man deswegen auch als
unspezifische Datentypen, auch Typen der stdlibc++ sind unspezifisch
(sie müssen es sein). Ein std::string ist ein Zeichenkette, sonst
nichts, aber zunächst einmal kein device-id. Ein double ist eine
Fließkommazahl, aber keine Spannung/Volt. Dazu definieren ich mir, und
gerade darin ist C++ unglaublich stark, einen eigenen Typen. Und wenn
ich eine Spannung/Volt durch einen Widerstandswert/Ohm dividiere,
erhalte ich einen Strom/Ampere. Auf diese Weise kann ich mir ein ganzes
SI-Sytem definieren: in diesem System werden Skalierungen und
Umrechnungen berücksichtigt und das ganz ohne Overhead zu dem direkten
double/int-Mix. Aber ich beschränke das ganze auf zulässige Operationen.
So ist es auch mit dem Byte: möchte ich, dass Bitoperationen möglich
sind, so definiere ich sie kostenfrei. Und um ein Byte als eine Zahl
oder ein Zeichen zu interpretieren, sollte ich es explizit und ggf. mit
Prüfungen wandeln. Das macht den ganzen Code auch expressiver.
Datentypen bzw. das Typsystem für ein Stück Software sind eine ganz
wichtigte Voraussetzung, damit der Compiler zur Compilezeit unzulässigen
Code abweisen kann und auch damit er soviel wie möglich optimieren kann.
Arduino F. schrieb:> Nunja...> Eine Variable vom Type Byte kann halt nur eine begrenzte Anzahl Werte> annehmen.
Sicher, wie jeder andere Datentyp auch. Ich verstehe nicht, was Du damit
sagen möchtest.
Arduino F. schrieb:> Das modernere "using byte = uint8_t;" wäre da viel klarer und damit> lesbarer.
Aber doch falsch, da Du, wie Du sagst, auch auf Prozessoren mit Byte !=
8 Bit unterwegs bist. Da brauchst Du zwingend enum.
Irgendeine Information scheint mir noch zu fehlen...
BTW: Welche Prozessoren sind denn das eigentlich mit Byte != 8 Bit?
Wilhelm M. schrieb:> Viel sinnvolles
So ergibt das Sinn, vielen Dank für die Klarstellung. Wobei ich zugeben
muss, auch ein Byte erst einmal nur als Zahl betrachtet tu haben. :-)
Mox schrieb:> da Du, wie Du sagst, auch auf Prozessoren mit Byte !=> 8 Bit unterwegs bist
Habe ich nicht gesagt!
Wollte ich auch nicht zum Ausdruck bringen.
Denn ich bin z.Zt. ausschließlich auf Systemen unterwegs, welche auf
8Bit (oder vielfache davon) basieren.
Mox schrieb:> BTW: Welche Prozessoren sind denn das eigentlich mit Byte != 8 Bit?
Findet man bei historischen Dingern recht häufig.
Heute noch bei Signalprozessoren.
Oder bei Exoten wie dem GA144 von GreenArrays.
Arduino F. schrieb:> Das modernere "using byte = uint8_t;" wäre da viel klarer und damit> lesbarer.
Auch das ist ein Typ-Alias und löst das Problem nicht, dass ein byte
nicht eine ganze, vorzeichenlose Zahl ist.
Bitte wendet euch an den Autor des Headers. Ich kann dessen Intentionen
anhand von "byte" bestenfalls erraten.
Ich hatte byte definiert, weil es einen COMPILERFEHLER gab, nämlich weil
es NICHT DEFINIERT war.
Wer hier Code postet, darf auch damit rechnen, dass man ihn ausprobiert,
und in PortDefinition.h gibt es eben keine Festlegung von "byte".
Weiters fehlen Definitionen von PORTD etc., der Header sollte also auch
avr/io.h o.ä. includen, weil er ohne avr/io.h ihnehin nicht
funktionieren kann.
Arduino F. schrieb:> Typedefs wie "typedef uint8_t byte;" haben das Problem, dass nicht immer> auf den ersten Blick erkenntlich ist, welches Token denn jetzt neu> definiert wird.
Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.
Wenn man nicht weiß, was
> enum class byte : uint8_t {};
genau macht, steht man da genauso erkenntnislos da.
Warum es "besser" ist weiß ich auch nicht, und Wilhelm schweigt sich ja
gerne darüber aus.
> using byte = uint8_t;
Also noch eine 3. Möglichkeit, und dann gibt es auch noch
> #define byte uint8_t
Mit "enum class byte : uint8_t {};" bekomm ich übrigens:
1
(viele fehlermeldungen, die in einem Dateianhang besser untergebracht sind)
Arduino F. schrieb:> Nunja...> Eine Variable vom Type Byte kann halt nur eine begrenzte Anzahl Werte> annehmen.> Eine Grundeigenschaft von enum.
Das ist eine Grundeigenschaft jedes Datentypen, denn eine unbegrenzte
Anzahl Werte wäre nur mit unendlich viel Speicher möglich. Die
Besonderheit von enums ist, dass man den einzelnen Werten Namen geben
kann, was hier aber nicht gemacht wird.
Arduino F. schrieb:> In der Arduino.h ist es so definiert:> typedef uint8_t byte;>> Was natürlich für AVRs + ARM schon ok ist, aber bei Prozessoren, wo ein> Byte eben keine 8 Bit beinhaltet (sondern z.B. 9 Bit) eben doch einen> Fehler darstellt.
Dann könnte man auch einfach
1
typedefunsignedcharbyte;
nehmen. char ist in C per Definition ein Byte groß.
> Das modernere "using byte = uint8_t;" wäre da viel klarer und damit> lesbarer.
Das finde ich nun gerade nicht, insbesondere bei komplexeren Sachen. Mit
typedef schreibe ich es exakt so hin, wie ich eine Variablendefinition
mit dem gleichen Typ schreiben würde, nur eben mit "typedef" davor. Mit
using muss ich es ganz anders hinschreiben:
1
intA[100];// Definiere Variable als Array aus 100 int
2
typedefintA[100];// Mache A zu alternativem Namen für genau so ein Array
3
usingA=int[100];// Durcheinandergebrachte Schreibweise für das gleiche
4
5
void(*func)(intarg);// Zeiger auf Funktion
6
typedefvoid(*func)(intarg);// Typedef für genau diesen Zeiger
7
usingfunc=void(*)(intarg);// Wieder durcheinandergebrachte Schreibweise
Wenn man schon die Schreibweise will, dass links immer der Name steht
und rechts der Typ, dann hätte man konsequenterweise auch für Variablen
sowas einführen müssen. Irgendwas wie:
Johann L. schrieb:> Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.> Wenn man nicht weiß, was>>> enum class byte : uint8_t {};>> genau macht, steht man da genauso erkenntnislos da.
Mmh, wenn man nicht weiß, was
1
intmain();
ist, kommt man auch nicht weiter ;-)
>> Warum es "besser" ist weiß ich auch nicht, und Wilhelm schweigt sich ja> gerne darüber aus.
Steht doch ein paar Posts oben drüber.
Rolf M. schrieb:> Wenn man schon die Schreibweise will, dass links immer der Name steht> und rechts der Typ, dann hätte man konsequenterweise auch für Variablen> sowas einführen müssen.
Auf den Vorgang ist mein Einfluss gering!
Ich muss fressen, was mir die Compiler Fuzzies vorwerfen!
Und das Beste/Schönste draus machen.
Rolf M. schrieb:> typedef void(*func)(int arg); // Typedef für genau diesen Zeiger> using func = void(*)(int arg); // Wieder durcheinandergebrachte> Schreibweise
Siehste, das meine ich damit!
Der typepdef ist ein Suchbildchen: "Was wird denn da nu definiert?"
Es gibt ja noch viel komplexere typdef Anweisungen, wo es noch schwerer
zu erkennen ist. Hier geht das ja noch.
Ich bevorzuge die using Schreibweise!
Obwohl ich mit der typedef Schreibweise groß geworden bin.
> durcheinandergebrachte
Ansichtssache, Geschmacksfrage, Umlernblockade, Religion, oder was auch
immer.
Habe viel mit Anfängern zu tun und da wirkt die Typdef Syntax oftmals
wie ein Stock in den Speichen.
Ich sachs mal so:
Um die typedef Schreibweise besser zu finden, muss man die typedef Kröte
geschluckt und verstanden haben. Und auch noch stolz darauf sein.
> #define byte uint8_t
Jedes #define, auf welches man verzichtet, ist ein gutes #define
Dein Chef sagt:
Wir machen ein neues Projekt, die Firmware kannst du vom alten Projekt
übernehmen, übermorgen bekommst du erste Hardware Muster und in 2 Wochen
bist du ferig. Du schaust dir die Firmware vom Kollegen an, alles in
Assembler und der ist nicht mehr in der Firma. Die Hardware kommt aber
es ist ein völlig anderer MC drauf weil der Alte nicht mehr erhältlich
oder der Neue 2 cent billiger ist.
Du wirst dabei viel Spass mit Assembler haben aber auch wenn dein
Kollege für sich eine wunderbare template-meta Library geschrieben hat.
Unrealistisch ?
Wilhelm M. schrieb:> Johann L. schrieb:>>> Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.>> [...]> Mmh, wenn man nicht weiß, was>
1
>intmain();
2
>
> ist, kommt man auch nicht weiter ;-)
Genau so ist es.
Das unterscheiden einen Code, der praktisch eingesetzt wird, von Code,
der bestimmte Features einer Sprache erklären will und einer Didaktik
folgt:
Bei praktischen Einsatz wird man nicht erklären, was "main" ist, dass es
eine Funktion ist, was "()" soll oder "{" etc.
Ich geh mal davon aus, dass mcu/avr8.h sowie mcu/ports.h aus mehreren
100 Zeilen Code bestehen. Der Autor dieser Zeilen wird diese verstanden
haben und nicht wert finden, diese entsprechend aufzubereiten. Wenn
diese Zeilen dann hier gepostet werden, und jemand versteht 90% davon,
dann wird er sich die restlichen 10% vermutlich ohne weitere
Unterstützung erarbeiten können. Wenn jamend aber nur 30% versteht,
dann hat er irgendwann das Gefühl, gegen eine Wand zu laufen weil er die
präsentierten Puzzle-Teile irgendwann nicht mehr zusammen bekommt. Und
das erzeugt dann Frust, der mit C++ assoziiert wird.
Das Ziel kann ja nicht sein, eine imaginierte Gegenseite unter C++
"Futter" zu begraben.
Abgesehen von wenigen neurotischen Ausnahmen geh ich mal davo aus, dass
die Leser solcher Threads grundsätzlich daran interessiert sind, mehr
über C++ und Verwendung zu lernen. Aber Du kannst auch davon ausgehen,
dass diese Leser weniger Erfahrung haben als C++ Experten, und die Kunst
besteht dann darin, diesen Graben zu überwinden. Inwieweit
nicht-aufbereiteter Bestandscode dazu in der Lage ist, lässt sich an den
Reaktionen ermessen.
>> ist, kommt man auch nicht weiter ;-)>> Genau so ist es.>
Das blöde an der class-enum-byte Variante ist, daß man nur die
Eigenschaft "kann 8-Bit Werte enthalten" abbildet, aber sämmtliche
Rechen-Operatoren von uint8_t verliert und auch keine automatische
Konvertierung von/nach Nicht-Byte-Werten mehr hat. Eigentlich geht es
eher darum, dedizierte Werte in genau 8 Bit abzulegen.
All das fehlende kann man natürlich wieder nachrüsten (bin mir aber
nicht 100% sicher, ob wirklich Alles), aber dann fragt man sich, warum
es überhaupt POD geben sollte.
Carl D. schrieb:> Das blöde an der class-enum-byte Variante ist, daß man nur die> Eigenschaft "kann 8-Bit Werte enthalten" abbildet, aber sämmtliche> Rechen-Operatoren von uint8_t verliert und auch keine automatische> Konvertierung von/nach Nicht-Byte-Werten mehr hat.
Für die einen ist das vllt. "blöde" und für die anderen ist es genau
das, was erreicht werden soll.
mh schrieb:> Carl D. schrieb:>> Das blöde an der class-enum-byte Variante ist, daß man nur die>> Eigenschaft "kann 8-Bit Werte enthalten" abbildet, aber sämmtliche>> Rechen-Operatoren von uint8_t verliert und auch keine automatische>> Konvertierung von/nach Nicht-Byte-Werten mehr hat.>> Für die einen ist das vllt. "blöde" und für die anderen ist es genau> das, was erreicht werden soll.
Wenn ich aber einen 8-Bit Integer Wert möchte, dann bleibt der Verlust
sämtlicher Rechenoperatoren "blöd". Der Sinn eines enum ist mir dagegen
sehr wohl bekannt. Und wenn man lieber "byte" als "uint8_t" schreibt,
dann benutzt man einen Type-Alias und keine Enumeration.
Carl D. schrieb:> mh schrieb:>> Für die einen ist das vllt. "blöde" und für die anderen ist es genau>> das, was erreicht werden soll.>> Wenn ich aber einen 8-Bit Integer Wert möchte, dann bleibt der Verlust> sämtlicher Rechenoperatoren "blöd".
Und wenn nicht? Also, wenn Du keinen 8-Bit Integer möchtest? ;-)
Sheeva P. schrieb:> Carl D. schrieb:>> mh schrieb:>>> Für die einen ist das vllt. "blöde" und für die anderen ist es genau>>> das, was erreicht werden soll.>>>> Wenn ich aber einen 8-Bit Integer Wert möchte, dann bleibt der Verlust>> sämtlicher Rechenoperatoren "blöd".>> Und wenn nicht? Also, wenn Du keinen 8-Bit Integer möchtest? ;-)
Wer schrieb noch mal in einem Parallelthread, daß es hier sehr
"konstruktiv" zu geht?
Johann L. schrieb:> Ich geh mal davon aus, dass mcu/avr8.h sowie mcu/ports.h aus mehreren> 100 Zeilen Code bestehen.
Auf besonderen Wunsch von SheevaPlug habe ich mal quickNdirty das
notwendige für das Beispiel zusammen gestellt. Ich hätte wesentlich mehr
entfernen können, aber das war mir jetzt zu viel Aufwand:
> Unterstützung erarbeiten können. Wenn jamend aber nur 30% versteht,> dann hat er irgendwann das Gefühl, gegen eine Wand zu laufen weil er die> präsentierten Puzzle-Teile irgendwann nicht mehr zusammen bekommt. Und> das erzeugt dann Frust, der mit C++ assoziiert wird.
Das liegt aber nicht an dem hier Gezeigten: ich habe schon oft gesagt,
dass man für das Verständnis sich zunächst einmal mit TMP beschäftigen
muss. Zwar sind die eingesetzten Mittel nicht super kompliziert, aber
eben nicht geeignet, einem TMP beizubringen. Dass muss man anderswo
selbst machen (z.B. in diesem Forum - da wäre ich ja mal gespannt).
Die meisten Kommentare, gehen eher in die Richtung: C++ ist Mist, das
geht doch mit C oder Asm besser. Wer damit zufrieden ist, kann es ja
auch bleiben. Es ist ja kein Muss, sich damit auseinander zu setzen. Und
dann macht es auch keinen Spaß mehr, darauf konstruktiv zu antworten.
> Abgesehen von wenigen neurotischen Ausnahmen geh ich mal davo aus, dass> die Leser solcher Threads grundsätzlich daran interessiert sind, mehr> über C++ und Verwendung zu lernen. Aber Du kannst auch davon ausgehen,> dass diese Leser weniger Erfahrung haben als C++ Experten, und die Kunst> besteht dann darin, diesen Graben zu überwinden.
Diese Leute sollten aber auch so viel Selbstreflexion mitbringen, dass
sie selbst anfangen zu lernen. Dann ergeben sich kleine Fragen, die auch
konkret beantwortet werden können: wie etwa oben zu dem Thema std::byte
statt uint8_t.
>>> ist, kommt man auch nicht weiter ;-)>>>> Genau so ist es.>>>> Das blöde an der class-enum-byte Variante ist, daß man nur die> Eigenschaft "kann 8-Bit Werte enthalten" abbildet, aber sämmtliche> Rechen-Operatoren von uint8_t verliert und auch keine automatische> Konvertierung von/nach Nicht-Byte-Werten mehr hat.
Genau das ist ja das Ziel: mit einer Sammlung von 8-Bits macht es erst
mal keinen Sinn, Arithmetik zu betreiben. Es sei dann, man macht diese
Bits (vielleicht sind es ja auch nur 6 signifikante und ein spezieller
NaN) explizit zu einer Ganzzahl.
In meinen Augen sind viele impliziten Typkonvertierungen (gerade bei
UDT) wenig sinnvoll, deswegen sollte man die ctor'en oft erst mal
explicit machen, und nur wenn es einen ganz besonderen Grund gibt, die
Typwandlung zulassen, und auch nicht beliebig Typwandlungs-Op. einbauen.
Aber wenn Du möchtest, kannst Du natürlich die gewünschten Ops wieder
hinzufügen. Bei byte z.B. die Bitoperatoren. Wenn es noch mehr DT als
byte gibt, für die man das machen möchte, kann man das auch via
Funktions-templates machen, die man für den Typ dann per SFINAE
einschalten kann.
Ok, ich versuch mich mal an einer Lösung. Hier gibt's ja genug, die
positiver Kritik fähig sind :-)
Zunächst werden enums für Ports und Pads festgelegt (ich verwende "Pad"
um Verwechslung mit "Pin" (Port Input Register) zu vermeiden):
1
#include<avr/io.h>
2
#include<type_traits>
3
4
enumclassPortName:uint8_t{A,B,C,D};
5
enumclassPad:uint8_t{Pad0=0,Pad1=1,Pad2=2};
Da es nicht möglich ist, PORTB oder &PORTB als Template-Parameter zu
verwenden, muss man es irgendwie anders machen. In ein Array möchte ich
die Ports nicht legen, da u.U. selbst constexpr instanziiert werden,
siehe unten mehr.
Die enums werden dann dazu verwendet, die Ports zu beschreiben indem sie
als Template-Parameter verwendet werden:
Ziel ist, alle Zugriffe in statischen Methoden zu haben, so dass keine
Objekte instanziiert werden müssen. Die einzelnen Ports werden dann
dargestellt, indem das Port-Template für den jeweiligen Port
spezialisiert wird. "High" und "Low" sind Abkürzungen, so dass ich
nicht immer Value::High und Value::Low schreiben muss.
1
// FIXME: "inline" is not strong enough, use GNU magic.
// FIXME: Neither GCC nor the language standard express any warranty of
11
// FIXME: atomicy, hence the following code may compile to a non-atomic
12
// FIXME: sequence. This applies even to optimized code and when
13
// FIXME: SBI and CBI are available, and the behavior may
14
// FIXME: depend on unrelated code found in the context of usage.
15
if(High==value)
16
PORTB|=PadMask::value;
17
else
18
PORTB&=PadNotMask::value;
19
}
20
21
template<>
22
INLINEPort_B1::ValuePort_B1::get()
23
{
24
returnPINB&PadMask::value?High:Low;
25
}
Die Anwendung definiert sich dann Anhand der Pad-Belegungen neue
Bezeichner, z.B. Led an Port B1:
1
usingLED=Port_B1;
2
#pragma GCC poison Port_B1
3
4
boolval;
5
6
voidtest1()
7
{
8
LED::set(LED::High);
9
LED::set(LED::High);
10
LED::set(LED::Low);
11
if(LED::get()==LED::High)
12
LED::set(LED::Low);
13
LED::set(val?LED::High:LED::Low);
14
}
15
16
intmain(){}
Probleme:
Problem 1)
Es lässt sich nicht übersetzen, denn der Linker meckert:
1
foo.cpp:(.text+0x6): undefined reference to `Port<(PortName)1, (Pad)1>::Low'
2
foo.cpp:(.text+0xa): undefined reference to `Port<(PortName)1, (Pad)1>::High'
3
collect2.exe: error: ld returned 1 exit status
Grund ist, dass die constexpr nicht aufgelöst wurden und eine
Instanziierung erwartet wird. Eine explizite Instanziierung funktioniert
nicht:
1
templateconstPort_B1::ValuePort_B1::High;
2
templateconstPort_B1::ValuePort_B1::Low;
der Linkerfehler bleibt. Erst eine explizite Instanziierung von Port_B1
löst das Problem:
1
templateclassPort<PortName::B,Pad::Pad1>;
Seltsamerweise verschwinden durch die explizite Instanziierung auch die
Instanziierungen der constexpr, und der Code ist danach wie erwartet.
Allerdings werden auch alle statischen inline-Fnuktionen instanziiert!
Das ergibt dann bei vielen Ports bis zu ~100 Funktionen pro Modul. Da
die Port-Header in mehreren Modulen Verwendung finden sollen und jedes
Modul alle Instanziierungen triggert, ist bei großen Projekten mit
tausenden von unnötigen Funktionen zu rechnen. Mit Linker-Optimierung
werden die zwar wieder entfernt, schön ist das aber trotzdem nicht.
Fazit: Generierten Code inspizieren! Beim nächsten Versuch werde ich
möglichst auf constexpr verzichten.
Problem 2)
Für jede Port / Pad Kombination und für jede Funktionalität wie "set",
"get", "toggle" ist das Template zu spezialisieren. Das gibt dann einen
Zoo von Spezialisierungen, die alle fast identisch aussehen. Keine
Ahnung wie man das besser löst.
Fazit: Ports und Pads als Template-Parameter und Spezialisierung sind
ein schlechter Ansatz.
Problem 3)
Die strikten enums nerven. Es erfordert einen Zoo von Casts, um Fehler
zu vermeiden: Fehlende Operatoren wie "&=", fehlende Casts von und nach
bool oder int etc. Das ist zwar gewollt, aber ich hab nicht
rausgefunden, wie man in so einem enum Operatoren überlädt. Und selbst
wenn: Man verwendet dann x Aufwand darauf, die kanonischen Operatoren
abermals zu implementieren, nur um seine "eigene" Byte-Arithmetik zu
haben.
Fazit: Beim nächsten Versuch werde ich auf Trallala wie "High" und "Low"
verzichten. Viel Aufwand ohne erkennbaren nutzen. Ohne Abkürzungen
ertrunkt der Code in den immergleichen enum-Referenzierungen.
Problem 4)
Selbst mit "inline" werden Funktionen nicht geinlinet, teilweise erzeugt
der Compiler sogar Calls mit nur 1 einzigen Instruktion.
Fazit: Generierten Code inspizieren! always_inline verwenden löst das
Problem bislang.
Problem 5)
Alles was mit Port_B1 assoziiert ist, müsste in ein #ifdef Device so
dass B1 nur für die Devices verfügbar ist, die auch hardwaremäßig über
B1 verfügen. Diese Klammer wird benötigt um jedes Tierchen im
Spezialisierungs-Zoo. Auto-generierte Header könnten ein Ansatz sein,
aber diesen Weg will ich definitiv nich verfolgen.
Fazit: Irgendwie bekomm ich was hin, das funktioniert. Aber zünden tut
der Ansatz nicht. Es ist zwar "schön" Objektorientiert, aber nicht so,
dass ich er verwenden möchte.
Sheeva P. schrieb:> Wilhelm M. schrieb:>> Mmh, das wird in Summe wohl recht viel ... aber ich schaue mal, wie ich>> das zusammenkoche, so dass es ein nachvollziehbares Beispiel bleibt. Wie>> aber nicht morgen sein ... (und ich ahne schon, welche Kommentare dazu>> kommen ;-))>> Das hat keine Eile -- aber um Beispiele nachvollziehen und sie mit> anderen Implementierungen vergleichen zu können, sollten sie natürlich> vollständig sein. Dies insbesondere auch deswegen, weil Du IMHO einer> der erfahrensten und fähigsten C++-Entwickler hier im Forum bist, und> ich deswegen geradezu darauf brenne zu sehen, wie Du die Sache angehst.> ;-)
Hier nochmal das Beispiel etwas anders formuliert:
...also den Code der 1. Versuchs in die Tonne gekloppt und nochmal bei 0
angefangen.
1
#include<avr/io.h>
2
3
template<classP>
4
structBase
5
{
6
staticvoidset(boolval)
7
{
8
if((uintptr_t)P::port()-__AVR_SFR_OFFSET__<0x20)
9
{
10
if(val)
11
*P::port()|=(uint8_t)P::padMask::value;
12
else
13
*P::port()&=(uint8_t)P::padNotMask::value;
14
}
15
else
16
{
17
__asm(".error \"Use ATOMIC_BLOCK from util/atomic.h\"");
18
}
19
}
20
};
Eine Basisklasse implementiert den Port-Zugriff. Die genaue
Port-Adresse wird erst durch eine Spezialisierung geliefert. Die obige
Implementierung verwendet einen potentiell nicht-atomaren Ansatz.
Strikt atomaren Code hab ich nicht ausgeführt, da es nicht primäres Ziel
der Übung ist.
Eine Klasse, die von Base ableitet, stellt dann die Port-Adresse zur
Verfügung:
Auch hier erweisen sich die strikten Enums als eher nervig. Die
Implementierung sieht insgesamt schlanker aus, und die Applikation kann
dann verwenden
1
usingLED=PortB<7>;
2
usingLED2=PortB<2>;
3
4
intvar;
5
6
voidfoo()
7
{
8
LED::set(1);
9
LED::set(1);
10
LED::set(1);
11
LED::set(1);
12
LED::set(0);
13
LED2::set(var&2);
14
}
15
intmain(){}
Fazit:
Die Implementierung sieht kompakter aus als die erste, insbesondere
reduzieren sich die Spezialisierungen auf die Anzahl der Ports (z.B.
PORTA, ... PORTF) und damit auf ein erträgliches Maß.
Die strikten Enums würde ich eher wieder entfernen, durch die
Cast-Origiern erhalte ich kein Mehrwerte. Wenn überhaupt, wären diese
Enums besser in "Base" aufgehoben.
Der generierte Code sieht aus wie erwartet, auch ohne always_inline. Ob
dies in allen optimierenden Übersetzungen und Kontexten der Falls
bleiben wird, weiß ich nicht — der Compiler gibt jedenfalls keine
Garantie; dito für Atomarität.
Es wäre schön, Template-parameter "Pad" in "Base" zur Verfügung zu
haben. Irgendwie geht das bestimmt, keine Ahnung wie.
Auch hab ich keine Idee wie ein Toggle zu behandeln wäre. Auf Devices,
wo dies durch Schrieb auf PIN realisiert werden kann, will man dieses
Feature auch nutzen. Früher oder später wird dies auf ein #ifdef
hinauslaufen, und die Frage ist, wo es am wenigsten hässlich ist.
Johann L. schrieb:>> Fazit: Irgendwie bekomm ich was hin, das funktioniert. Aber zünden tut> der Ansatz nicht. Es ist zwar "schön" Objektorientiert, aber nicht so,> dass ich er verwenden möchte.
Es zündet, wenn man PinSets definieren kann, die dann zusammenspielen.
Man definiert die einzelnen Pins und kann diese dann zusammenfassen.
Alle Inputs, alle Outputs, die 4Bit eines LCD-Daten-"Worts". Dann kann
man irgendwo im Programm alle Outputs initialisieren, ala
"Outputs::setMode(Pin::Mode::OUTPUT)" ohne wissen zu müssen welche das
bei der aktuellen Konfiguration genau sind. Oder dem erwähnte
LCD-Datenwort einfach einen Wert zuzuweisen, ohne wissen zu müssen,
welches Bit durch welches Pin an welchem Port abgebildet wird.
Wobei ich leicht gefrustet feststellen muß, daß man hier noch nicht mal
bei der Frage "wie definiere ich byte, wenn ich nicht im Arduino-Umfeld
arbeite" eine normale Diskussion führen kann ;-(
Es ging dabei nicht um Bitfelder, sondern um die 8Bit breiten AVR
Port-Register.
Und nein Johann, dich meine ich damit nicht.
Johann L. schrieb:> Ok, ich versuch mich mal an einer Lösung. Hier gibt's ja genug, die> positiver Kritik fähig sind :-)>> Zunächst werden enums für Ports und Pads festgelegt (ich verwende "Pad"> um Verwechslung mit "Pin" (Port Input Register) zu vermeiden):>
1
>#include<avr/io.h>
2
>#include<type_traits>
3
>
4
>enumclassPortName:uint8_t{A,B,C,D};
5
>enumclassPad:uint8_t{Pad0=0,Pad1=1,Pad2=2};
6
>
Oft ist es besser, statt enums Typen zu nehmen: also etwa Tag-structs
(leer).
> Da es nicht möglich ist, PORTB oder &PORTB als Template-Parameter zu> verwenden, muss man es irgendwie anders machen.
Man kann auch lvalue-references auf constexpr-Objekte verwenden. Aber
PORTB ist volatile.
> In ein Array möchte ich> die Ports nicht legen, da u.U. selbst constexpr instanziiert werden,> siehe unten mehr.
Du kannst eine Typ-List stattdessen verwenden.
> Die enums werden dann dazu verwendet, die Ports zu beschreiben indem sie> als Template-Parameter verwendet werden:>
>> Ziel ist, alle Zugriffe in statischen Methoden zu haben, so dass keine> Objekte instanziiert werden müssen.
Genau, das ist m.E. der richtige Weg. Dann hat man zur Laufzeit nicht
das Problem, das Objekte mit unsinnigen Paramterierungen erzeugt werden.
So kann man alles zur Compilezeit prüfen: static_assert (und concepts).
> Die einzelnen Ports werden dann> dargestellt, indem das Port-Template für den jeweiligen Port> spezialisiert wird. "High" und "Low" sind Abkürzungen, so dass ich> nicht immer Value::High und Value::Low schreiben muss.
Das ist eigentlich nicht nötig. Du kannst über die Port-Tag-Typen (oder
die enums wie bei Dir), die Port-Adressen berechnen.
>
1
>// FIXME: "inline" is not strong enough, use GNU magic.
>// FIXME: Neither GCC nor the language standard express any warranty
11
>of
12
>// FIXME: atomicy, hence the following code may compile to a
13
>non-atomic
14
>// FIXME: sequence. This applies even to optimized code and when
15
>// FIXME: SBI and CBI are available, and the behavior may
16
>// FIXME: depend on unrelated code found in the context of usage.
17
>if(High==value)
18
>PORTB|=PadMask::value;
19
>else
20
>PORTB&=PadNotMask::value;
21
>}
22
>
23
>template<>
24
>INLINEPort_B1::ValuePort_B1::get()
25
>{
26
>returnPINB&PadMask::value?High:Low;
27
>}
28
>
>> Die Anwendung definiert sich dann Anhand der Pad-Belegungen neue> Bezeichner, z.B. Led an Port B1:>
1
>usingLED=Port_B1;
2
>#pragmaGCCpoisonPort_B1
3
>
4
>boolval;
5
>
6
>voidtest1()
7
>{
8
>LED::set(LED::High);
9
>LED::set(LED::High);
10
>LED::set(LED::Low);
11
>if(LED::get()==LED::High)
12
>LED::set(LED::Low);
13
>LED::set(val?LED::High:LED::Low);
14
>}
15
>
16
>intmain(){}
17
>
>> Probleme:>> Problem 1)> Es lässt sich nicht übersetzen, denn der Linker meckert:
Seit C++17 ist constexpr auch inline. Vorher muss Du es explizit zu
einer inline Definition machen.
> Seltsamerweise verschwinden durch die explizite Instanziierung auch die> Instanziierungen der constexpr, und der Code ist danach wie erwartet.> Allerdings werden auch alle statischen inline-Fnuktionen instanziiert!
Das liegt daran, dass template-Elementfunktionen anders als konkrete
Elementfunktionen behandelt werden. Ein vollständig spezialisierter Typ
/ Funktion ist wie ein konkrete Funktion.
> Das ergibt dann bei vielen Ports bis zu ~100 Funktionen pro Modul. Da> die Port-Header in mehreren Modulen Verwendung finden sollen und jedes> Modul alle Instanziierungen triggert, ist bei großen Projekten mit> tausenden von unnötigen Funktionen zu rechnen.
Das sollte auf keinen Fall passieren.
>> Problem 2)> Für jede Port / Pad Kombination und für jede Funktionalität wie "set",> "get", "toggle" ist das Template zu spezialisieren. Das gibt dann einen> Zoo von Spezialisierungen, die alle fast identisch aussehen. Keine> Ahnung wie man das besser löst.
Anmerkung: s.o.
> Fazit: Ports und Pads als Template-Parameter und Spezialisierung sind> ein schlechter Ansatz.
Template-Parameter ja, aber besser als Tag-Typen statt enums.
Eine vollständige Spezialisierung ist manchmal schlecht, aber partielle
Spezialisierungen wesentlich.
> Problem 3)> Die strikten enums nerven. Es erfordert einen Zoo von Casts, um Fehler> zu vermeiden: Fehlende Operatoren wie "&=", fehlende Casts von und nach> bool oder int etc. Das ist zwar gewollt, aber ich hab nicht> rausgefunden, wie man in so einem enum Operatoren überlädt.
Freie Funktionen
> Und selbst> wenn: Man verwendet dann x Aufwand darauf, die kanonischen Operatoren> abermals zu implementieren, nur um seine "eigene" Byte-Arithmetik zu> haben.
Macht man einmal als Template. Kann man dann via SFINAE einschalten.
Johann L. schrieb:> Auch hier erweisen sich die strikten Enums als eher nervig.
Verwende Tag-Typen.
Genereller Hinweis: verwende Typen wenn Du kannst, verwende Werte, wenn
Du musst.
> Der generierte Code sieht aus wie erwartet, auch ohne always_inline. Ob> dies in allen optimierenden Übersetzungen und Kontexten der Falls> bleiben wird, weiß ich nicht — der Compiler gibt jedenfalls keine> Garantie; dito für Atomarität.
Das könntest Du z.B. mit static_assert sicherstellen bzw. bei etwa AVR
mit constexpr-if prüfen, ob das Zielregister sbi/cbi-fähig ist oder
anderweitig ein echtes RMW verwendet werden kann.
> Es wäre schön, Template-parameter "Pad" in "Base" zur Verfügung zu> haben. Irgendwie geht das bestimmt, keine Ahnung wie.
Das hast Du doch schon in Deiner CRTP-Basisklasse ... als class P
MitLeserin schrieb:> Wilhelm M. schrieb:>> ich habe schon oft gesagt,>> dass man für das Verständnis sich zunächst einmal mit TMP beschäftigen>> muss. Zwar sind die eingesetzten Mittel nicht super kompliziert, aber>> eben nicht geeignet, einem TMP beizubringen.>> Eine brauchbare Einführung in TemplateMetaProgramming>> http://www.codeproject.com/Articles/257589/An-Idiots-Guide-to-Cplusplus-Templates-Part> http://www.codeproject.com/Articles/268849/An-Idiots-Guide-to-Cplusplus-Templates-Part
Habe da gerade mal hinein geschaut: das ist eine Einführung in
Templates, aber keine Einführung in die Template-Meta-Programmierung -
ohne Deine oder die Mühe des Autors schmälern zu wollen ;-)
Leider kenne ich auch kein kurzes und knappes HowTo, obgleich hier
Beitrag "Informationen zu C vs C++ / aka Futter für die Diskussion"
viele Themen angesprochen werden.
(Template) Meta-Funktionen sind "Funktionen", die (wie alle eigentlichen
Funktionen) Abbildungen vornehmen. Als Meta-Funktionen werden
üblicherweise Konstrukte bezeichnet, die nicht mit Werten / Objekten
operieren, sondern mit Typen. Also eine Abbildung Typ -> Typ oder
Typ-Liste -> Typ oder Typ-Liste -> Type-Liste oder ... vornehmen.
Natürlich zählen auch Abbildungen Konstante -> Typ, Typ -> Konstante,
Konstante -> Konstante dazu (obgleich man für Konstante -> Konstante
heute eher constexpr-Funktionen nutzen wird). (So ist ja auch die
Turing-Vollständigkeit des Template-"Mechanismus" in C++ "entdeckt"
worden: Fibonacci-Berechnung zur Compilezeit ;-))
Meta-Funktionen werden in C++ durch Klassen-Templates realisiert. Etwa
um aus einem vorzeichenbehafteten Typ den korrespondierenden
vorzeichelosen zu berechnen:
1
template<typenameT>
2
structUnsignedFor;
3
4
template<>
5
structUnsignedFor<int8_t>{
6
typedefuint8_ttype;
7
};
8
9
// weitere Mappings
Und dann:
1
template<typenameT>
2
typenameUnsignedFor<T>::typefoo(T){
3
...
4
}
Nur mal so als Hinweis, um die Idee der Meta-Funktionen klar zu machen
;-)
Johann L. schrieb:> Ziel ist, alle Zugriffe in statischen Methoden zu haben, so dass keine> Objekte instanziiert werden müssen.
Vielleicht habe ich da etwas nicht richtig verstanden, aber IMHO gibt es
keinen Grund, auf Instanziierungen zu verzichten, solange der Compiler
das alles weg optimiert wie in meinem Beispiel oben. Was spricht
dagegen, den Compiler einfach seinen Job machen zu lassen?
Mikro 7. schrieb:> Sheeva P. schrieb:>> Vielleicht habe ich da etwas nicht richtig verstanden>> Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:
Vielen Dank, aber mir ging es eher um die Frage, warum man etwas
(nämlich: die Instanziierung von Objekten) vermeiden wollte, das der
Optimierer des Compilers ohnehin eliminiert und das daher nichts kostet.
Mikro 7. schrieb:> Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:>>
1
>template<unsignedintn>
2
>structfactorial{
3
>enum{value=n*factorial<n-1>::value};
4
>};
5
>
6
>template<>
7
>structfactorial<0>{
8
>enum{value=1};
9
>};
10
>
11
>// Usage examples:
12
>// factorial<0>::value would yield 1;
13
>// factorial<4>::value would yield 24.
14
>
Dies ist der "klassische" Weg, Meta-Funktionen (Konstante -> Konstante)
zu verwenden. Wie ich oben aber schon gesagt habe, würde man dies
allerdings heute mit einer constexpr-Funktion machen: hier könnte man
auch auf die Rekursion verzichten (falls man das möchte). Im Effekt ist
es aber dasselbe: man eine Compilezeit-Konstante.
Sheeva P. schrieb:> Mikro 7. schrieb:>> Sheeva P. schrieb:>>> Vielleicht habe ich da etwas nicht richtig verstanden>>>> Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:>> Vielen Dank, aber mir ging es eher um die Frage, warum man etwas> (nämlich: die Instanziierung von Objekten) vermeiden wollte, das der> Optimierer des Compilers ohnehin eliminiert und das daher nichts kostet.
Dazu gibt es m.E. mehrere Gründe:
1) unbegrenzte Instanziierungen: lässt man Instanziierungen (Laufzeit)
zu, so kann man die Anzahl der erzeugten Objekte nur zur Laufzeit
kontrollieren. Man generiert also bei unsinnigen Instanziierungen
Laufzeitfehler (etwa Assertionsfehler). Bei Typen, die HW- Ressourcen
repräsentieren sollen, ist eine unbegrenzte Instanziierung wegen der
begrenzten Anzahl und begrenzten Ressourcen der HW-Instanzen m.E. nicht
sinnvoll. Stattdessen sollte man Compile-Zeit Instanziierungen
(Template-Instanziierungen) verwenden, dann ergeben unsinnige
Instanziierungen Compilezeit-Fehler.
Also etwa
1
usingusart0=Usart<0>;
2
usingusart1=Usart<1>;
3
usingusart2=Usart<2>;// Compilezeitfehler.
Dagegen:
1
Usartusart{0};
2
Usartusart{1};
3
Usartusart{2};// Laufzeitfehler
Compilezeitfehler sind "besser" als Laufzeitfehler.
2) möchte man trotzdem 1) machen, so müssten die zu instanziierenden
Typen eigentlich alle Monostates sein.
Usart usarta{0};
Usart usartb{1};
Usart usartc{0}; // hat denselben Zustand wie usarta
Monostates bedeuten aber (meistens) Laufzeitkosten.
3) Jedes Objekt (auch leere) belegt mindestens 1 Byte (schreibt die
Sprache vor). Natürlich können Objekte gemäß der as-if-Rule oder z.B.
der jetzt verbindlichen RVO wegoptimiert werden. Allerdings stellt sich
die Frage, warum etwas als Objekt zur Laufzeit instanziiert werden soll,
was dann ggf. wieder wegoptimiert werden kann. Außerdem ist es dann
eigentlich Pflicht, alles als Monostate zu realisieren (s.a. 2)).
1
Usart<0>usart0;
2
Usart<0>usart1;// template muss auch als Monostate realisiert sein
3
Usart<1>usart2;
4) Compilezeit-Instanzieerungen (das Bilden von Typen) ermöglichen es
besser, Berechnungen, die man zur Compilezeit machen kann, auch durch
constexpr-Funktionen oder TMP zur Compilezeit zu machen (machen zu
können). Frei nach dem Motto: mache alles zur Compilezeit, was möglich
ist, und nur das zur Laufzeit, was Du musst.
5) Oft (nicht immer) sind Compilezeit-Instanziierungen
(Template-Instanziierungen) besser zu "verkraften" (ggf. erhöhter
Flash-Verbrauch) als Laufzeit-Instanziierungen (erhöhter RAM-Verbrauch
und Rechenkosten).
6) Letztlich ist es auch eine Stilfrage: m.E. drücken statische
Compilezeit-Instanziierungen die Natur von in ihrer Anzahl und
Ausprägung physisch begrenzten HW-Ressourcen wesentlich besser aus. Das
trifft auch wieder grundsätzlich zu, dass nämlich die Modellierungen so
präzise wie möglich sein sollten, etwa auch: nicht alles ist einfach ein
String, nicht alles ist ein int.
Bei Typen, die die o.g. Bedingungen nicht erfüllen, gelten natürlich die
üblichen Modellierungsüberlegungen.
Will man das o.g. Umsetzen, benötigt man neue / andere
Programmierparadigmen wie constexpr-Kontexte und wie TMP. Und dann kommt
selbstverständlich das "Totschlagargument", dass diese Paradigmen doch
kein Mensch versteht. Das ist vollkommen verständlich, aber m.E. sehr
kurzsichtig: etwa so, wie beim Übergang in den 1980er von der
prozeduralen Programmierung zur klassischen OOP. Auch ist es vollkommen
legitim darauf zu vertrauen, dass der Compiler das irgendwie alles
optimiert und leistungsfähige HW zur geringen Kosten die Notwendigkeit
zu o.g. Überlegungen ad absurdum führt. Aber das wäre dann nicht mein
Stil (s.a. 6))
>> Dies ist der "klassische" Weg, Meta-Funktionen (Konstante -> Konstante)> zu verwenden. Wie ich oben aber schon gesagt habe, würde man dies> allerdings heute mit einer constexpr-Funktion machen: hier könnte man> auch auf die Rekursion verzichten (falls man das möchte). Im Effekt ist> es aber dasselbe: man eine Compilezeit-Konstante.
Etwas idiomatischer, aber trotzdem ohne constexpr-functions wäre m.E.:
Carl D. schrieb:> Johann L. schrieb:>>>> Fazit: Irgendwie bekomm ich was hin, das funktioniert. Aber zünden tut>> der Ansatz nicht. Es ist zwar "schön" Objektorientiert, aber nicht so,>> dass ich er verwenden möchte.>> Es zündet, wenn man PinSets definieren kann, die dann zusammenspielen.
Das löst aber kein einziges der angesprochenen Probleme. Und das Ziel
war auch garnicht, Bitgruppen zu beschreiben sondern ersma einzelne
(SFR) Bits.
Wilhelm M. schrieb:> Verwende Tag-Typen.> Genereller Hinweis: verwende Typen wenn Du kannst, verwende Werte, wenn> Du musst.
Mal ganz konkret: Wie verwende ich &PORTB ?
&PORTB ist "uint8_t volatile*" und damit nicht-volatile (volatile wäre
es als "uint8_t* volatile", was es nicht ist).
Die Adresse von PORTB ist zur Compilezeit bekannt, aber weder bekomm ich
es in ein constexpr rein noch als Template-Parameter.
Was ich definitiv nicht will sind magische Zahlen wie "56" wie in
Deinen Headern.
1
staticinlineconstexpruint8_t*addr=&PORTB;
1
port.cpp:31:44: error: invalid conversion from 'volatile uint8_t* {aka volatile unsigned char*}' to 'uint8_t* {aka unsigned char*}' [-fpermissive]
2
static inline constexpr uint8_t *addr = &PORTB;
3
^
4
port.cpp:31:44: error: 'reinterpret_cast<volatile uint8_t* {aka volatile unsigned char*}>(56)' is not a constant expression
Konstanter als "56" wird's nicht...
Stell Dir ein Device mit > 20.000 SFRs und > 100.000 Bitsfeldern vor,
mögliche (Konfigurations-) Werte nochmal um Größenordnungen mehr. Wenn
es nicht möglich ist, ohne magische Zahlen auszukommen und man sich
nicht auf Device-Header berufen kann, kann scheidet jeglicher
Template-Ansatz für mich aus da er nicht mit der Device-Größe skaliert.
p.s.
Und riesige Template-Monster und -Header sind auch nicht unbedingt
praktikabel. Auf einem AVR spielen Compilezeiten keine Rolle, denn die
Devices sind so winzig, dass Applikationen zwangsläufig klein sind.
Wenn es aber eine Firmware mit > 2000 Modulen ist, und jedes includet
zig riesige Header, dann wirkt sich das sowohl empfindlich auf
(akkumulierte) Compile-Zeit aus als auch auf Objektgrößen (zumindest
wenn man mit Debug-Info generiert), so dass auch die Link-Zeiten
explodieren.
Meine Lektion hab ich gelernt, als ich mal versuchte, ein Lookup-Array
per Templates zu generieren. Die Compile-Zeit ging durch die Decke und
stieg von Sekundenbruchteilen auf über 10 Sekunden.
Hätte mir eigentlich vorher klar sein müssen: Template-Programmierung
bedeutet im Endeffekt, den Compiler zu skripten; und Compiler sind so
ziemlich die ineffizientesten Scripting-Engines, die man sich vorstellen
kann.
Es stimmt eben einfach nicht, dass alles, was zur Compilezeit
berechenbar ist, auch zur Compilezeit berechnet werden sollte: Teilweise
kann es auch vor der Compilierung berechnen werden, etwa durch
(auto)-generierten Code. Von der Effizienz des Produkt genau gleich,
Compile-Zeit drastisch kleiner und der erzeugende Code wesentlich besser
verständlich und wartbarer als Templates.
Die auto-gen Option sollte man also auch immer auf dem Radar haben,
insbesondere auch weil man Freiheit bei der Wahl der Sprache hat.
error: '(volatile uint8_t*)(24 + 32)' is not a valid template argument for 'volatile uint8_t* {aka volatile unsigned char*}' because it is not the address of a variable
2
S<(&PORTB)> s{};
3
^
Warum?
Weil ein Template-Argument eine Linkage haben muss wegen Name-Mangling?
Mit template<auto p> ist die Meldung i.w. die gleiche.
Das einzige was mit dazu einfallen würde, ist ein Array wie oben von
ArdiunoBoy angesetzt, aber dessen Inhalt wäre auch nicht constexpr.
Und noch ne Frage: Warum genügt constexpr nicht um einen Ausdruck zur
Compilezeit zu bestimmen, etwa wie in
Beitrag "Re: AVR GPIOR Bit Verwaltung C++"
Konstanter als constexpr wird's nicht.
Es wurde auch keine Adresse des constexpr gezogen. Mit einem enum
passiert sowas nicht. Wäre das Objekt aus anderen Gründen instanziiert
worden, dann fällt das doch nicht auf dass der constexpr im Static
Storage angelegt wird und dann indirekt drauf zugegriffen wird.
D.h. per constexpr gibt man die Zusicherung, dass ein Ausdruch
prinzipiell zur Compile-Zeit berechenbar ist. Aber ein Compiler gibt
keinerlei Garantie whatsoever dass das Zeug auch wirklich zur
Compile-Zeit berechnet wird.
Johann L. schrieb:> Die Adresse von PORTB ist zur Compilezeit bekannt, aber weder bekomm ich> es in ein constexpr rein noch als Template-Parameter.
Der Grund steht in meinem Code drin als auch in der untigen
Fehlermeldung als auch im C++-Standard: reinterpret_cast macht alles
non-constexpr.
> Was ich definitiv nicht will sind magische Zahlen wie "56" wie in> Deinen Headern.
Nun, das brauchst Du auch nicht. Du kannst genauso PORTB verwenden. Ich
habe das nur nicht gemacht, weil ich keinen CPP mehr verwenden will -
das geht zwar nicht vollkommen, weil die Device-Typ nur aus einem
CPP-Macro erkennbar ist. Deswegen ist der CPP bei mir nur noch auf ein
paar einsame Stelle reduziert, an denen ich bedingte Compilation aus
besagtem Grund brauche.
>
1
staticinlineconstexpruint8_t*addr=&PORTB;
>>
1
> port.cpp:31:44: error: invalid conversion from 'volatile uint8_t* {aka
2
> volatile unsigned char*}' to 'uint8_t* {aka unsigned char*}'
> unsigned char*}>(56)' is not a constant expression
>> Konstanter als "56" wird's nicht...
Ja klar, s.o. reinterpret_cast
> Es stimmt eben einfach nicht, dass alles, was zur Compilezeit> berechenbar ist, auch zur Compilezeit berechnet werden sollte: Teilweise> kann es auch vor der Compilierung berechnen werden, etwa durch> (auto)-generierten Code.
Das gehört logisch ja auch zur Compilezeit ;-)
Für mich ist es eben sehr praktisch, den Compiler dazu zu verwenden,
weil ich dann auch andere Tools verzichten kann. Wenn Du das nicht
möchtest: it's up to you.
> Compile-Zeit drastisch kleiner und der erzeugende Code wesentlich besser> verständlich und wartbarer als Templates.
Kommt immer auf den Standpunkt des Betrachters an. Wie ich schon mal
geschrieben habe: wenn Du für Dich entschieden hast, dass so nicht zu
wollen, dass lass es doch einfach ;-)
Wilhelm M. schrieb:>> Es stimmt eben einfach nicht, dass alles, was zur Compilezeit>> berechenbar ist, auch zur Compilezeit berechnet werden sollte: Teilweise>> kann es auch vor der Compilierung berechnen werden, etwa durch>> (auto)-generierten Code.>> Das gehört logisch ja auch zur Compilezeit ;-)>> Für mich ist es eben sehr praktisch, den Compiler dazu zu verwenden,> weil ich dann auch andere Tools verzichten kann. Wenn Du das nicht> möchtest: it's up to you.>>> Compile-Zeit drastisch kleiner und der erzeugende Code wesentlich besser>> verständlich und wartbarer als Templates.>> Kommt immer auf den Standpunkt des Betrachters an. Wie ich schon mal> geschrieben habe: wenn Du für Dich entschieden hast, dass so nicht zu> wollen, dass lass es doch einfach ;-)
Ich habe nicht geschrieben, dass ich irgendwas "nicht möchte", sondern
nur, dass man sich der möglichen Werkzeuge in seinem Werkzeugkasten
bewusst sein soll, nebst Hinweis auf ein spezielles Werkzeug.
Auf die Patzigkeit möchte ich nicht weiter eingehen und lieber bei der
Sache bleiben.
error: '(volatile uint8_t*)(24 + 32)' is not a valid template
2
> argument for 'volatile uint8_t* {aka volatile unsigned char*}' because
3
> it is not the address of a variable
4
> S<(&PORTB)> s{};
5
> ^
6
>
> Warum?
s.a. http://en.cppreference.com/w/cpp/language/template_parameters
Ein Non-Typ-Templateparameter braucht static-storage und linkage.
> Und noch ne Frage: Warum genügt constexpr nicht um einen Ausdruck zur> Compilezeit zu bestimmen, etwa wie in>> Beitrag "Re: AVR GPIOR Bit Verwaltung C++"
s.o. reinterpret_cast
Die Bedeutung von constexpr bei einer Funktion ist, das sie - sofern
möglich - auch in einem constexpr-Kontext ausgewertet werden kann.
Aber ich bin nicht sicher was Du meinst.
> D.h. per constexpr gibt man die Zusicherung, dass ein Ausdruch> prinzipiell zur Compile-Zeit berechenbar ist. Aber ein Compiler gibt> keinerlei Garantie whatsoever dass das Zeug auch wirklich zur> Compile-Zeit berechnet wird.
Wenn die Voraussetzungen für constexpr erfüllt sind, wird die Compiler
die Funktion auch als constexpr auswerten. Das muss (!) er tun, wenn das
Resultat eine constexpr-Objekt sein soll.
Johann L. schrieb:> Wilhelm M. schrieb:>>> Es stimmt eben einfach nicht, dass alles, was zur Compilezeit>>> berechenbar ist, auch zur Compilezeit berechnet werden sollte: Teilweise>>> kann es auch vor der Compilierung berechnen werden, etwa durch>>> (auto)-generierten Code.>>>> Das gehört logisch ja auch zur Compilezeit ;-)>>>> Für mich ist es eben sehr praktisch, den Compiler dazu zu verwenden,>> weil ich dann auch andere Tools verzichten kann. Wenn Du das nicht>> möchtest: it's up to you.>>>>> Compile-Zeit drastisch kleiner und der erzeugende Code wesentlich besser>>> verständlich und wartbarer als Templates.>>>> Kommt immer auf den Standpunkt des Betrachters an. Wie ich schon mal>> geschrieben habe: wenn Du für Dich entschieden hast, dass so nicht zu>> wollen, dass lass es doch einfach ;-)>> Ich habe nicht geschrieben, dass ich irgendwas "nicht möchte", sondern> nur, dass man sich der möglichen Werkzeuge in seinem Werkzeugkasten> bewusst sein soll, nebst Hinweis auf ein spezielles Werkzeug.
In fast jedem Post zur der Sache betonst Du, dass das alles zu
kompliziert, zu umständlich und schwer verständlich für Dich sei.
Deswegen meine ich, dass Du Dich ja dazu nicht gezwungen fühlen musst.
> Auf die Patzigkeit möchte ich nicht weiter eingehen und lieber bei der> Sache bleiben.
Oh Patzigkeit: dann liest doch Deine eigenen Posts und Dein obiges P.S.
einfach nochmal.
Johann L. schrieb:> Ich habe nicht geschrieben, dass ich irgendwas "nicht möchte", sondern> nur, dass man sich der möglichen Werkzeuge in seinem Werkzeugkasten> bewusst sein soll, nebst Hinweis auf ein spezielles Werkzeug.
eventuell ein einfacherer Weg zum Ziel:
**************************************
A) einen funktionierenden Werkzeugkasten anschauen und im Detail
studieren wie alles funktioniert.
B) mit den Erkenntnissen eigene Versuche starten.
C) Konzept von Wilhelm M studieren und mit A) vergleichen,
Ähnlichkeiten und Differenzen analysieren und Schlussfolgerungen ziehen.
****************************************************
A) erfordert Beschäftigung mit C++, Lerneffekt ist garantiert.
C) zeigt dann auch die Anwendung von neuen Ergänzungen der Sprache.
Weil das ganze an konkreten Beispielen erfolgen kann, ist die Freude
hoffentlich grösser als der Frust.
Ein relativ abstraktes Beispiel für Ports und Pins.
**************************************************
Das Know-How steckt in den Headern ..
https://github.com/KonstantinChizhov/Mcucpp/blob/master/examples/GPIO_GCC_AVR/GPIO_sample.cpp
Wilhelm M. schrieb:> Johann L. schrieb:>> Auf die Patzigkeit möchte ich nicht weiter eingehen und lieber bei der>> Sache bleiben.>> Oh Patzigkeit: dann liest doch Deine eigenen Posts und Dein obiges P.S.> einfach nochmal.
Ok, also doch eine Nachlese...
Johann L. schrieb:> p.s.>> Und riesige Template-Monster und -Header sind auch nicht unbedingt> praktikabel. Auf einem AVR spielen Compilezeiten keine Rolle, denn die> Devices sind so winzig, dass Applikationen zwangsläufig klein sind.> Wenn es aber eine Firmware mit > 2000 Modulen ist, und jedes includet> zig riesige Header, dann wirkt sich das sowohl empfindlich auf> (akkumulierte) Compile-Zeit aus als auch auf Objektgrößen (zumindest> wenn man mit Debug-Info generiert), so dass auch die Link-Zeiten> explodieren.
Hier weise ich auf eine konkrete Erfahrung / Beobachtung hin, die ich
mal gemacht hatte, und dass es Kontexte gibt (auf welche ich keinen
Einfluss habe), in denen Compile-Zeit eine Rolle spielt.
Das "Monster" ist wertend, hätte ich unterlassen sollen.
Ich bezog mich darin auch nicht auf Deinen Code. Der "p.s." Absatz
folgt direkt auf ein Problem mit den von Dir erwähnten Tag-Typen und
befindet sich damit in unmittelbarer textlicher Nähe, von daher ist es
nicht verunderlich, dass Du Dich dadurch angesprochen fühltest. Das war
nicht meine Absicht, und das Absetzen qua "p.s." war wohl nicht klar
ganug. Sorry dafür.
> Meine Lektion hab ich gelernt, als ich mal versuchte, ein Lookup-Array> per Templates zu generieren. Die Compile-Zeit ging durch die Decke und> stieg von Sekundenbruchteilen auf über 10 Sekunden.
Eine konkrete Erfahrung mit Templates, ohne Anspruch auf Allgemeinheit.
Der weitere Text ist ein eher unmotivierter Verweis auf eine mögliche
Alternative weit außerhalt des aktuellen inhaltlichen Rahmens. Werde ich
in Zukunft unterlassen.
Wilhelm M. schrieb:> Kommt immer auf den Standpunkt des Betrachters an. Wie ich schon mal> geschrieben habe: wenn Du für Dich entschieden hast, dass so nicht zu> wollen, dass lass es doch einfach ;-)
Dieser Punkt scheint Dir wichtig zu sein. Zunächst habe ich eine
konkrete Erfahrung wiedergegeben — zugegeben unmotiviert — keinen
unverückbaren Standpunkt. Es kommt eben auch auf das konkret zu lösende
Poblem an. Und auch darauf, welche Erfahrung man mit bestimmten
Werkzeugen hat; und da bist Du bei C++ klar im Voraus.
Ich betrachte mich als Lernenden, der die Wahl seiner Tools auch an
seinem Wissen darüber sowie an der Kritikalität der jeweiligen
Applikation orientiert, als jemanden, der lernfähig und -willig ist und
der nicht unverückbaren Standpunkten anhaften oder sich irgendwelchen
Lagern zugehörig fühlt.
Wilhelm M. schrieb:> In fast jedem Post zur der Sache betonst Du, dass das alles zu> kompliziert, zu umständlich und schwer verständlich für Dich sei.> Deswegen meine ich, dass Du Dich ja dazu nicht gezwungen fühlen musst.
Einen Zwang, C++ besser beherrschen zu müssen, habe ich tatsächlich
nicht. Vielleicht hinterfrage ich es deshalb auch intensiver — zugegeben
nicht immer föderlich für eine inhaltliche Auseinandersetzung.
Wilhelm M. schrieb:> Wenn die Voraussetzungen für constexpr erfüllt sind, wird die Compiler> die Funktion auch als constexpr auswerten. Das muss (!) er tun, wenn das> Resultat eine constexpr-Objekt sein soll.
Konkret: Der Linkerfehler im folgenden Testfall hat seine Ursache in
einem Compiler-Bug, weil er die constexpr nicht zur Compile-Zeit
auswertet:
1
unsignedvolatilePORTB;
2
3
template<intp>
4
structPort
5
{
6
enumclassValue:unsigned{High,Low};
7
staticinlinevoidset(Value);
8
staticconstexprValueHigh=Value::High;
9
staticconstexprValueLow=Value::Low;
10
};
11
12
template<>constexprPort<7>::ValuePort<7>::High;
13
template<>constexprPort<7>::ValuePort<7>::Low;
14
15
template<>
16
inlinevoidPort<7>::set(Port<7>::Valuevalue)
17
{
18
if(High==value)
19
PORTB|=2;
20
else
21
PORTB&=~2;
22
}
23
24
intmain()
25
{
26
Port<7>::set(Port<7>::Low);
27
Port<7>::set(PORTB?Port<7>::Low:Port<7>::High);
28
}
Übersetzt z.B. mit
1
g++ tmp.cpp -std=c++11 -Os
ergibt:
1
...
2
tmp.cpp:(.text.startup+0x3): undefined reference to `Port<7>::Low'
3
tmp.cpp:(.text.startup+0x1a): undefined reference to `Port<7>::High'
4
collect2: error: ld returned 1 exit status
Und auch ohne Optimierung wäre der Compiler dazu verpflichtet, constexpr
zur Compile-Zeit auszuwerten.
Johann L. schrieb:> Wilhelm M. schrieb:> Konkret: Der Linkerfehler im folgenden Testfall hat seine Ursache in> einem Compiler-Bug, weil er die constexpr nicht zur Compile-Zeit> auswertet:
Kurz auf die Schnelle ;-)
Compilerbug ist es nicht. Korrekt müsste Dein Code folgendermaßen
aussehen:
1) scoped-enum Werte müssen vollständig qualifiziert werden. Bei Dir im
Code sehr unglücklich, weil Werte und Member gleich heissen.
2) ich bin grad selbst nicht im klaren, warum die unnötigerweise
eingefügten, out-of-class Definitionen der Datenelemente die in-class
inline-Deklarationen (constexpr ist implizit inline) die verdecken bzw.
nicht zu den in-class passen ... Vermutung: an der Stelle wird ein
Variablen-Template deklariert ... müsste ich aber mal in Ruhe
nachvollziehen.
Die Out-of-class Definition kann man sich durch inline sparen. Ab C++17
ist constexpr implizit inline;
3) Insgesamt ist mir nicht klar, warum Du überhaupt spezialisierst. Ist
eigentlich unnötig, zumindest für das Beispiel. Einfacher:
Wilhelm M. schrieb:> Johann L. schrieb:>> Wilhelm M. schrieb:>>> Konkret: Der Linkerfehler im folgenden Testfall hat seine Ursache in>> einem Compiler-Bug, weil er die constexpr nicht zur Compile-Zeit>> auswertet:>> Kurz auf die Schnelle ;-)> Compilerbug ist es nicht. Korrekt müsste Dein Code folgendermaßen> aussehen:>> [...]> static constexpr Value High = Value::High;> static constexpr Value Low = Value::Low;> };>> //template<>> //constexpr Port<7>::Value Port<7>::High;> //template<>> //constexpr Port<7>::Value Port<7>::Low;
Verwirrt ich bin.
I.d.R. ist es doch ein Fehler, die out-of-class Definition eines static
wegzulassen? — auch wenn das oft nicht auffällt weil die Compiler gut
optimieren. Oder ist das bei constexpr anders?
...andererseits kann der Header nicht "wissen", wie High und Low
verwendet werden, z.B.:
1
constPort<7>::Value*get()
2
{
3
return&Port<7>::High;
4
}
Überhaupt ist es verwirrend, dass constexpr hier static sein muss...
> Insgesamt ist mir nicht klar, warum Du überhaupt spezialisierst.
Es ist erstmal ein Testfall, der einen Linkerfehler produziert. Wenn
der Testfall korrekt ist (also nicht ill-formed oder undefined behaviour
o.ä.), dann ist es eine C++11 Eingabe, die einen Bug explizit macht.
Mein Ziel war nicht, diesen potenziellen g++ Bug zu umgehen, sondern
einen kleinen Testfall dafür zu haben.
Johann L. schrieb:>> Verwirrt ich bin.>> I.d.R. ist es doch ein Fehler, die out-of-class Definition eines static> wegzulassen? — auch wenn das oft nicht auffällt weil die Compiler gut> optimieren. Oder ist das bei constexpr anders?
Ja, constexpr ist es anders: wenn die lvalue-Natur nicht benötigt wird,
fällt es nicht auf. Das hat man m.E. in C++17 verbessert, indem
constexpr immer implizit inline ist (Achtung: Bedeutung einer
inline-Variablen!).
>> ...andererseits kann der Header nicht "wissen", wie High und Low> verwendet werden, z.B.:>
1
constPort<7>::Value*get()
2
>{
3
>return&Port<7>::High;
4
>}
>> Überhaupt ist es verwirrend, dass constexpr hier static sein muss...
Warum?
Der Grund des Linker-Fehlers war (habe es gefunden):
1
An explicit specialization of a static data member of a template or an explicit specialization of a static data member template is a definition if the declaration includes an initializer; otherwise, it is a declaration. [ Note: The definition of a static data member of a template that requires default-initialization must use a braced-init-list:
2
3
template<> X Q<int>::x; // declaration
4
template<> X Q<int>::x (); // error: declares a function
5
template<> X Q<int>::x { }; // definition
6
— end note ]
> Es ist erstmal ein Testfall, der einen Linkerfehler produziert. Wenn> der Testfall korrekt ist (also nicht ill-formed oder undefined behaviour> o.ä.), dann ist es eine C++11 Eingabe, die einen Bug explizit macht.
Wie gesagt und jetzt erläutert: es ist keine Bug im Compiler!
Edit:
Es gibt wie oben gezeigt zwei Stellen in Standard, die gewissermaßen
gegeneinander sprechen: ein static constexpr member muss initialisiert
werden und dass ist dann eine Definition. Andereseits kann man bei g++
offensichtlich ein Member spezialisieren, ohne das die Spezialisierung
des Typ vorliegt, was dann zu einer Verdeckung führt. Versucht man die
out-of-class initialiserung, um die Dekalration zu einer Definition zu
machen, bekommt man auch einen Fehler vom g++ -> und das ist
inkonsistent!
Der clang++ macht es anders.
Also vielleicht doch ein Fall von Bugreport ...
Wilhelm M. schrieb:> Der Grund des Linker-Fehlers war (habe es gefunden):
Ok, ich versuch mich an einer Aufdröselung...
> An explicit specialization of a static data member of a template...
Trifft hier zu für
template<> constexpr Port<7>::Value Port<7>::High;
> ...or an explicit specialization of a static data member template...
trifft auf den Testfall nicht zu, da High selbst kein Template ist
> is a definition if the declaration includes an initializer;
Trifft hier zu, denn die Deklaration im Template hat einen Initializer:
static constexpr Value High = Value::High;
Damit stellt die explizite Spezialisierung eine Definition dar. Der
Compiler muss also ein zu dieser Definition gehörendes Objekt anlegen
(oder darf das Anlegen bei globaler Optimierung auch weglassen, wenn
sichergestellt ist, dass die Instanz von High nirgends referenziert wird
— was hier nicht der Fall ist, denn g++ erzeugt Code, der High
referenziert).
Damit ist es doch ein Bug, das Objekt Port<7>::High nicht anzulegen?
> otherwise, it is a declaration.
"otherwise" wäre hier:
* Keinen Initializer an der Deklaration => g++ wirft
error: constexpr static data member ‘High’ must have an initializer
* Initializer oder "{}" an der Spezialisierung => g++ wirft
error: duplicate initialization of ‘Port<7>::High’
Johann L. schrieb:> Wilhelm M. schrieb:>> Der Grund des Linker-Fehlers war (habe es gefunden):>> Ok, ich versuch mich an einer Aufdröselung...>>> An explicit specialization of a static data member of a template...>> Trifft hier zu für>> template<> constexpr Port<7>::Value Port<7>::High;>>> ...or an explicit specialization of a static data member template...>> trifft auf den Testfall nicht zu, da High selbst kein Template ist>>> is a definition if the declaration includes an initializer;>> Trifft hier zu, denn die Deklaration im Template hat einen Initializer:
Sehe ich nicht so: denn gemeint ist m.E. die Spezialisierung, die keinen
Initializer hat. Insofern hat wie nur eine Deklaration und deswegen
undefined-reference.
Fügt man aber eine Initialiserung hinzu, so - hast Du richtig bemerkt -
gibt es einen Fehler für ein doppelte Initialiserung (hätte auch im
Linker eine Verletzung der ODR geben können).
Das Ganze ist m.E. deswegen inkonsistent, weil der g++ es als eine
Verdeckung des members im einen Fall interpretiert, und im anderen Fall
als dasselbe member.
clang++ ist da konsistent(er), und lässt die Verdeckung ohne
Initialisierung nicht zu, und mit Initialiserung in der Spezailisierung
ist alles ok.
> Damit ist es doch ein Bug, das Objekt Port<7>::High nicht anzulegen?
Ich denke auch (hatte ja auch oben schon meine Meinung geändert).
Wobei Du - glaube ich - auf eine Ecke im Standard gestoßen bist, die
noch klargestellt werden sollte.
>> otherwise, it is a declaration.>> "otherwise" wäre hier:>> * Keinen Initializer an der Deklaration => g++ wirft> error: constexpr static data member ‘High’ must have an initializer
Nein, (s.o.) das bezieht sich auf den Initializer in der Spezialisierung
(jedenfalls würde ich den Standard so lesen).
>> * Initializer oder "{}" an der Spezialisierung => g++ wirft> error: duplicate initialization of ‘Port<7>::High’
Was auch inkonsistent ist, weil der g++ es andersherum als Verdekung
interpretiert.
Nun ja ... ich muss gestehen, dass ich wohl noch nie ein einzelnes(!)
non-template Member eines Templates spezialisiert habe, was ich auch als
nicht besonders lesbar erachte ... und es erstaunt mich sogar, dass das
möglich ist.
Man lernt immer dazu ;-)
Ok, habe nochmal nen paar Minuten investiert und es nachgelesen:
http://en.cppreference.com/w/cpp/language/template_specialization
Danach hat g++ einen Bug, clang++ ist korrekt.
Die volle Spezialisierung eines einzelnen(!) static (constexpr oder
nicht) members ist natürlich möglich:
1
template<int>
2
structX{
3
staticconstexprintx=0;
4
};
5
6
template<>
7
constexprintX<1>::x=1;
8
9
intmain()
10
{
11
returnX<1>::x;
12
}
Man muss aber tatsächlich beachten, dass man einen Initializer
hinzufügt, was bei constexpr sowie sein muss (andernfalls wäre es z.B.
bei einem non-const member keine Definition).
@Johann: schreibst Du ein Bug-Report (Du hast es ja schließlich entdeckt
;-))
error: incomplete type 'X<1>' used in nested name specifier
3
return X<1>::x;
4
^
Wieso ist X<1> inclomplete? Irgendwie überschreibt das "template<>
struct X<1>;" das Struct aus dem Template, und danach ist X<1>
incomplete so wie wie Y in "struct Y;" inclomplete ist.
PR83484