Forum: Mikrocontroller und Digitale Elektronik AVR GPIOR Bit Verwaltung C++


von Randy B. (rbrecker)


Lesenswert?

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<auto Bit> class A{};
2
template<auto Bit> class B{};
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
A a(reg);
4
B b(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?

von Stefan F. (Gast)


Lesenswert?

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:
1
void alarm_led_aus()
2
{
3
    PORTB &= (1<<PB4);
4
}
5
6
void alarm_led_aus()
7
{
8
    PORTB &= (1<<PB4);
9
}

von Vincent H. (vinci)


Lesenswert?

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<auto Bit> class A{};
2
> template<auto Bit> class B{};
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.

Beitrag #5242378 wurde von einem Moderator gelöscht.
von Randy B. (rbrecker)


Lesenswert?

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
> void alarm_led_aus()
2
> {
3
>     PORTB &= (1<<PB4);
4
> }
5
> 
6
> void alarm_led_aus()
7
> {
8
>     PORTB &= (1<<PB4);
9
> }
10
>

Und es geht nicht um Pins, sondern um bspw. GPIOR0

von Randy B. (rbrecker)


Lesenswert?

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?

: Bearbeitet durch User
Beitrag #5242390 wurde von einem Moderator gelöscht.
von Randy B. (rbrecker)


Lesenswert?

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?

Beitrag #5242402 wurde von einem Moderator gelöscht.
von Randy B. (rbrecker)


Lesenswert?

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?

Beitrag #5242416 wurde von einem Moderator gelöscht.
von Randy B. (rbrecker)


Lesenswert?

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.

von Carl D. (jcw2)


Lesenswert?

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.

Beitrag #5242431 wurde von einem Moderator gelöscht.
Beitrag #5242433 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

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<int T> class A_ {};
2
template<int T> class B_ {};
3
4
typedef A_<0> A;
5
typedef B_<1> B;
6
7
int main(void) {
8
    A a;
9
    B b;
10
    
11
    return 0;
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...

Beitrag #5242439 wurde von einem Moderator gelöscht.
Beitrag #5242444 wurde von einem Moderator gelöscht.
Beitrag #5242448 wurde von einem Moderator gelöscht.
Beitrag #5242449 wurde von einem Moderator gelöscht.
Beitrag #5242453 wurde von einem Moderator gelöscht.
Beitrag #5242459 wurde von einem Moderator gelöscht.
Beitrag #5242461 wurde von einem Moderator gelöscht.
Beitrag #5242537 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

[OT]
Der Bursche ist ja schlimmer, als Kurt...
[/OT]

Beitrag #5242556 wurde von einem Moderator gelöscht.
von Carl D. (jcw2)


Lesenswert?

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.

Beitrag #5242567 wurde von einem Moderator gelöscht.
Beitrag #5242582 wurde von einem Moderator gelöscht.
Beitrag #5242600 wurde von einem Moderator gelöscht.
Beitrag #5242634 wurde von einem Moderator gelöscht.
Beitrag #5242648 wurde von einem Moderator gelöscht.
Beitrag #5242670 wurde von einem Moderator gelöscht.
Beitrag #5242720 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

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.

: Bearbeitet durch User
Beitrag #5242732 wurde von einem Moderator gelöscht.
Beitrag #5242745 wurde von einem Moderator gelöscht.
von Nop (Gast)


Lesenswert?

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.

von Sheeva P. (sheevaplug)


Lesenswert?

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
int main(void) {
4
    
5
    Motor       motor       (&DDRB, &PORTB, &PINB, PB1);
6
    Beleuchtung beleuchtung (&DDRB, &PORTB, &PINB, PB2);
7
8
    Startknopf  startknopf  (&DDRD, &PORTD, &PIND, PD1);
9
    Bremse      bremse      (&DDRD, &PORTD, &PIND, PD2);
10
    
11
    Getriebe    gang        (&DDRD, &PORTD, &PIND, PD3);
12
13
    while(1) {
14
        if (startknopf.gedrueckt()) { 
15
            beleuchtung.ein();
16
            if (gang.eingelegt() and bremse.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.

von apr (Gast)


Lesenswert?

Sheeva P. schrieb:
> Aber dafür ist der "Algorithmus" so sprechend und offensichtlich, daß
> sogar die beste Ehefrau von allen ihn sofort versteht.

https://www.reddit.com/r/orlybooks/comments/4i00t7/casual_sexism/

von Carl D. (jcw2)


Lesenswert?

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.

Beitrag #5242822 wurde von einem Moderator gelöscht.
Beitrag #5242830 wurde von einem Moderator gelöscht.
von Randy B. (rbrecker)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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.

: Bearbeitet durch User
von Randy B. (rbrecker)


Lesenswert?

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.

Beitrag #5242850 wurde von einem Moderator gelöscht.
von Nop (Gast)


Lesenswert?

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?

von Nop (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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<typename T, typename Flag>
3
struct A final {
4
    A() = delete;
5
    static void f(){
6
        Flag::set();
7
    }
8
    inline static T mData{};
9
};
10
11
// Beispiel einer Ressource, die ein Flag benötigt
12
template<typename Flag, typename T>
13
struct B final {
14
    B() = delete;
15
    static void f() {
16
        if (Flag::isSet()) {
17
            Flag::reset();
18
        }
19
    }
20
    inline static T mData{};
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<typename Flag>
26
using AF = A<uint8_t, Flag>;
27
28
// diese Meta-Funktionen bildet den Typ B<...> auf BF<Flag> für den Controller ab
29
template<typename Flag>
30
using BF = B<Flag , uint16_t>;
31
32
// registrieren der Ressourcen Typen (templates) im Controller
33
using controller = Controller<flagRegister, AF, BF>;
34
35
// ermitteln der durch den Controller konkret erzeugten Typen
36
using a = controller::get<AF>::type;
37
using b = controller::get<BF>::type;
38
39
int main() {
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 ....

von temp (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Einer K. (Gast)


Lesenswert?

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.

Beitrag #5243063 wurde von einem Moderator gelöscht.
Beitrag #5243143 wurde von einem Moderator gelöscht.
Beitrag #5243209 wurde von einem Moderator gelöscht.
Beitrag #5243317 wurde von einem Moderator gelöscht.
von Vincent H. (vinci)


Lesenswert?

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
using QspiPins = 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.

von Nop (Gast)


Lesenswert?

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?

von Vincent H. (vinci)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

Vincent H. schrieb:

> Ja.

Uih. Na, danke. (:

von Vincent H. (vinci)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
1
using clockPin = Spi::Clock<Pin<PortB, 1>>;
2
using dataPin = Spi::Data<Pin<PortC, 2>>;
3
4
5
using spi = Spi<dataPin, clockPin>;

oder
1
using spi = Spi<clockPin, dataPin>;

von Noch einer (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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).

Beitrag #5243527 wurde von einem Moderator gelöscht.
Beitrag #5243536 wurde von einem Moderator gelöscht.
von Carl D. (jcw2)


Lesenswert?

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 ;-)

: Bearbeitet durch User
Beitrag #5243702 wurde von einem Moderator gelöscht.
Beitrag #5243735 wurde von einem Moderator gelöscht.
Beitrag #5243770 wurde von einem Moderator gelöscht.
Beitrag #5243801 wurde von einem Moderator gelöscht.
Beitrag #5243814 wurde von einem Moderator gelöscht.
von Jörn S. (splash)


Lesenswert?

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.

Beitrag #5243835 wurde von einem Moderator gelöscht.
von Christopher J. (christopher_j23)


Lesenswert?

Jörn S. schrieb:
> 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.

Das XPCC Projekt vom Roboterclub Aachen verwendet solche Konzepte:
https://github.com/roboterclubaachen/xpcc

Es gibt jede Menge Beispiele. Unter anderem kommen diese Vincents 
Beispiel sehr nahe:
https://github.com/roboterclubaachen/xpcc/blob/develop/examples/stm32f4_discovery/spi/main.cpp

von MitLeserin (Gast)


Lesenswert?

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

Beitrag #5244013 wurde von einem Moderator gelöscht.
Beitrag #5244247 wurde von einem Moderator gelöscht.
Beitrag #5244296 wurde von einem Moderator gelöscht.
Beitrag #5244315 wurde von einem Moderator gelöscht.
Beitrag #5244448 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

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.

Beitrag #5244472 wurde von einem Moderator gelöscht.
Beitrag #5244479 wurde von einem Moderator gelöscht.
Beitrag #5244488 wurde von einem Moderator gelöscht.
von Stefan F. (Gast)


Lesenswert?

> 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.

von Sheeva P. (sheevaplug)


Angehängte Dateien:

Lesenswert?

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. ;-)

Beitrag #5244597 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
using namespace AVR;
5
6
using PortB = Port<AVR::ATMega328P::PortRegister, B>;
7
using PortD = Port<AVR::ATMega328P::PortRegister, D>;
8
9
using motor = Pin<PortB, 1>;
10
using beleuchtung = Pin<PortB, 2>;
11
using startknopf = Pin<PortD, 1>;
12
using bremse = Pin<PortD, 2>;
13
using gang = Pin<PortD, 3>;
14
15
int main() {
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() and bremse::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:
1
00000080 <main>:
2
  80:   21 9a           sbi     0x04, 1 ; 4
3
  82:   22 9a           sbi     0x04, 2 ; 4
4
  84:   51 98           cbi     0x0a, 1 ; 10
5
  86:   52 98           cbi     0x0a, 2 ; 10
6
  88:   53 98           cbi     0x0a, 3 ; 10
7
  8a:   49 9b           sbis    0x09, 1 ; 9
8
  8c:   07 c0           rjmp    .+14            ; 0x9c <main+0x1c>
9
  8e:   2a 9a           sbi     0x05, 2 ; 5
10
  90:   4b 9b           sbis    0x09, 3 ; 9
11
  92:   fb cf           rjmp    .-10            ; 0x8a <main+0xa>
12
  94:   4a 9b           sbis    0x09, 2 ; 9
13
  96:   f9 cf           rjmp    .-14            ; 0x8a <main+0xa>
14
  98:   29 9a           sbi     0x05, 1 ; 5
15
  9a:   f7 cf           rjmp    .-18            ; 0x8a <main+0xa>
16
  9c:   29 98           cbi     0x05, 1 ; 5
17
  9e:   2a 98           cbi     0x05, 2 ; 5
18
  a0:   f4 cf           rjmp    .-24            ; 0x8a <main+0xa>

: Bearbeitet durch User
Beitrag #5244684 wurde von einem Moderator gelöscht.
Beitrag #5244688 wurde von einem Moderator gelöscht.
von MitLeserin (Gast)


Lesenswert?

Wilhelm M. schrieb:
> #include "mcu/avr8.h"
> #include "mcu/ports.h"

Damit das compilierbar wäre, müssten die beiden #includes verfügbar 
sein.
Quelle?

von Einer K. (Gast)


Angehängte Dateien:

Lesenswert?

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)
1
#include "PinDefinition.h"
2
3
using namespace PinDefinition;
4
5
TasterGND<2>  taster;
6
OutputPin<13> led;
7
8
void setup(void) 
9
{
10
   taster.initPullup();
11
   led.init();
12
}
13
14
void loop(void) 
15
{
16
    led = taster;
17
}

Beitrag #5244733 wurde von einem Moderator gelöscht.
Beitrag #5244773 wurde von einem Moderator gelöscht.
Beitrag #5244776 wurde von einem Moderator gelöscht.
Beitrag #5244780 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Angehängte Dateien:

Lesenswert?

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.

Beitrag #5244796 wurde von einem Moderator gelöscht.
Beitrag #5244797 wurde von einem Moderator gelöscht.
Beitrag #5244815 wurde von einem Moderator gelöscht.
Beitrag #5244818 wurde von einem Moderator gelöscht.
Beitrag #5244819 wurde von einem Moderator gelöscht.
Beitrag #5244823 wurde von einem Moderator gelöscht.
Beitrag #5244824 wurde von einem Moderator gelöscht.
Beitrag #5244858 wurde von einem Moderator gelöscht.
Beitrag #5244867 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

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.

von Sheeva P. (sheevaplug)


Lesenswert?

Wilhelm M. schrieb:
> Alternative:
>
>
1
> #include "mcu/avr8.h"
2
> #include "mcu/ports.h"
3
>

Shiqq, könntest Du vielleicht bitte noch die Header beifügen? THX!

Beitrag #5244898 wurde von einem Moderator gelöscht.
Beitrag #5244901 wurde von einem Moderator gelöscht.
Beitrag #5244922 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

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.

Beitrag #5244933 wurde von einem Moderator gelöscht.
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Arduino F. schrieb:
> PinDefinition.h
1
PinDefinition.h:5:33: error: ISO C++ forbids declaration of 'type name' with no type [-fpermissive]
2
       using Register = volatile byte *;
3
                                 ^~~~
Fehlt

Also z.B.
1
#include <cstdint>
2
3
#pragma once
4
5
namespace PinDefinition 
6
{ 
7
      typedef uint8_t byte;
8
      typedef volatile byte *Register;
9
...

Wenn ich ein kleines Beispiel übersetze, bekomm ich gerne mal für fast 
jeden Zugriff eine Funktion (Übersetzt mit avr-g++ v8 -Os ...)
1
#include <avr/io.h>
2
#include "PinDefinition.h"
3
4
bool val;
5
6
using Pad12 = PinDefinition::OutputPin<12>;
7
8
void test ()
9
{
10
    Pad12{}.set(val);
11
    Pad12{}.set(val^1);
12
    Pad12{}.set(1);
13
    Pad12{}.set(0);
14
    Pad12{}.set(0);
15
    Pad12{}.set(0);
16
}
17
18
Pad12 pad12{};
19
20
void test2 ()
21
{
22
    pad12.set(val);
23
    pad12.set(val^1);
24
    pad12.set(0);
25
    pad12.set(0);
26
    pad12.set(0);
27
    pad12.set(1);
28
}
 
Wird da zu
 
1
  .type  _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1, @function
2
_ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1:
3
  cbi 0x5,4
4
  ret
5
  .size  _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1, .-_ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
6
  .type  _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2, @function
7
_ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2:
8
  tst r24
9
  breq .L3
10
  sbi 0x5,4
11
  ret
12
.L3:
13
  jmp _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
14
  .size  _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2, .-_ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2
15
.global  _Z4testv
16
  .type  _Z4testv, @function
17
_Z4testv:
18
  lds r24,val
19
  call _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2
20
  lds r25,val
21
  ldi r24,lo8(1)
22
  eor r24,r25
23
  call _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2
24
  sbi 0x5,4
25
  call _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
26
  call _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
27
  jmp _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
28
  .size  _Z4testv, .-_Z4testv
29
.global  _Z5test2v
30
  .type  _Z5test2v, @function
31
_Z5test2v:
32
  lds r24,val
33
  call _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2
34
  lds r25,val
35
  ldi r24,lo8(1)
36
  eor r24,r25
37
  call _ZN13PinDefinition9OutputPinILh12EE3setEb.isra.2
38
  call _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
39
  call _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
40
  call _ZN13PinDefinition9OutputPinILh12EE6setLowEv.isra.1
41
  sbi 0x5,4
42
  ret

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]':
2
PinDefinition.h:94:24:   required from 'void PinDefinition::OutputPin<arduinoPin>::set(bool) [with unsigned char arduinoPin = 12]'
3
PinDefinition.h:84:31: warning: conversion from 'int' to 'PinDefinition::byte {aka unsigned char}' may change value [-Wconversion]
4
       *getOutPort(arduinoPin)  |=  getMaske(arduinoPin);
5
       ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
6
PinDefinition.h: In instantiation of 'void PinDefinition::OutputPin<arduinoPin>::setLow() [with unsigned char arduinoPin = 12]':
7
PinDefinition.h:94:39:   required from 'void PinDefinition::OutputPin<arduinoPin>::set(bool) [with unsigned char arduinoPin = 12]'
8
PinDefinition.h:89:31: warning: conversion from 'int' to 'PinDefinition::byte {aka unsigned char}' may change value [-Wconversion]
9
       *getOutPort(arduinoPin)  &=  ~getMaske(arduinoPin);
10
       ~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~

: Bearbeitet durch User
von MitLeserin (Gast)


Angehängte Dateien:

Lesenswert?

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.

Beitrag #5244951 wurde von einem Moderator gelöscht.
Beitrag #5244989 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

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.

Beitrag #5245026 wurde von einem Moderator gelöscht.
Beitrag #5245049 wurde von einem Moderator gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

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"?

von Wilhelm M. (wimalopaan)


Lesenswert?

Sheeva P. schrieb:
> Wilhelm M. schrieb:
>> Alternative:
>>
>>
1
>> #include "mcu/avr8.h"
2
>> #include "mcu/ports.h"
3
>>
>
> 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 ;-))

von Sheeva P. (sheevaplug)


Lesenswert?

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:
1
#include <avr/io.h>
2
3
/* outputs */
4
#define MOTOR       PB1
5
#define BELEUCHTUNG PB2
6
7
/* inputs */
8
#define STARTKNOPF  PD1
9
#define BREMSE      PD2
10
#define GANG        PD3
11
12
int main(void) {
13
14
    DDRB |= ((1 << MOTOR) | (1 << BELEUCHTUNG));
15
16
    while(1) {
17
        if(PIND & (1 << STARTKNOPF)) { /* startknopf.gedrueckt */
18
            PORTB |= (1 << BELEUCHTUNG); /* beleuchtung.ein */
19
            /* gang.eingelegt + bremse.gedrueckt */
20
            if( (PIND & (1 << PD3)) && (PIND & (1 << PD2)) ) {
21
                PORTB |= (1 << MOTOR); /* motor.ein */
22
            }
23
        } else {
24
            PORTB &= ~(1 << MOTOR); /* motor.aus */
25
            PORTB &= ~(1 << BELEUCHTUNG); /* beleuchtung.aus */
26
        }
27
    }
28
}

von Sheeva P. (sheevaplug)


Lesenswert?

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. 
;-)

von Sheeva P. (sheevaplug)


Lesenswert?

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.

Beitrag #5245288 wurde von einem Moderator gelöscht.
von Stefan F. (Gast)


Lesenswert?

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?

von MitLeserin (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:

> Also z.B.
>
1
>       typedef uint8_t byte;
2
>

Den Typ-Alias halte ich nicht für sinnvoll.

Besser:
1
    enum class byte : uint8_t {};

Beitrag #5245308 wurde von einem Moderator gelöscht.
Beitrag #5245316 wurde von einem Moderator gelöscht.
Beitrag #5245351 wurde von einem Moderator gelöscht.
von Mox (Gast)


Lesenswert?

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!

von Einer K. (Gast)


Lesenswert?

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.

Beitrag #5245372 wurde von einem Moderator gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Mox (Gast)


Lesenswert?

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?

von Mox (Gast)


Lesenswert?

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. :-)

Beitrag #5245383 wurde von einem Moderator gelöscht.
von Einer K. (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Johann L. (gjlayde) Benutzerseite


Angehängte Dateien:

Lesenswert?

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)

--

Meldungen ausgelagert
-rufus

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

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
typedef unsigned char byte;
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
int A[100]; // Definiere Variable als Array aus 100 int
2
typedef int A[100]; // Mache A zu alternativem Namen für genau so ein Array
3
using A = int[100]; // Durcheinandergebrachte Schreibweise für das gleiche
4
5
void (*func)(int arg); // Zeiger auf Funktion
6
typedef void(*func)(int arg); // Typedef für genau diesen Zeiger
7
using func = void(*)(int arg); // 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:
1
var A = int[100];

von Wilhelm M. (wimalopaan)


Lesenswert?

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
int main();
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.

von Einer K. (Gast)


Lesenswert?

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

Beitrag #5245440 wurde von einem Moderator gelöscht.
von Hans-Georg L. (h-g-l)


Lesenswert?

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 ?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wilhelm M. schrieb:
> Johann L. schrieb:
>
>> Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.
>> [...]
> Mmh, wenn man nicht weiß, was
>
1
> int main();
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.

von Carl D. (jcw2)


Lesenswert?

Johann L. schrieb:
> Wilhelm M. schrieb:
>> Johann L. schrieb:
>>
>>> Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.
>>> [...]
>> Mmh, wenn man nicht weiß, was
>>
1
>> int main();
2
>>
>> 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.

von mh (Gast)


Lesenswert?

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.

von Carl D. (jcw2)


Lesenswert?

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.

Beitrag #5246028 wurde von einem Moderator gelöscht.
Beitrag #5246120 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

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? ;-)

von Carl D. (jcw2)


Lesenswert?

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?

Beitrag #5246181 wurde von einem Moderator gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

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:
1
     42 text files.
2
      40 unique files.                              
3
      18 files ignored.
4
5
github.com/AlDanial/cloc v 1.74  T=0.13 s (186.6 files/s, 24153.6 lines/s)
6
-------------------------------------------------------------------------------
7
Language                     files          blank        comment           code
8
-------------------------------------------------------------------------------
9
C/C++ Header                    19            362            330           2194
10
Bourne Shell                     1             13              0             55
11
C++                              1              9             16             34
12
C                                1              6             11             16
13
make                             2             10             36             15
14
-------------------------------------------------------------------------------
15
SUM:                            24            400            393           2314
16
-------------------------------------------------------------------------------

> 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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Carl D. schrieb:
> Johann L. schrieb:
>> Wilhelm M. schrieb:
>>> Johann L. schrieb:
>>>
>>>> Das ist kein Problem des Konstrukts, sondern ein Problem des Lesers.
>>>> [...]
>>> Mmh, wenn man nicht weiß, was
>>>
1
>>> int main();
2
>>>
>>> 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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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
enum class PortName : uint8_t { A, B, C, D };
5
enum class Pad : 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:
1
template<PortName p, Pad b>
2
class Port
3
{
4
    typedef std::integral_constant<Pad,b> PadValue;
5
    typedef std::integral_constant<uint8_t, (uint8_t) (1u << (unsigned) PadValue::value)> PadMask;
6
    typedef std::integral_constant<uint8_t, (uint8_t) ~PadMask::value> PadNotMask;
7
    enum class Value : uint8_t { High, Low };
8
public:
9
    static void set (Value);
10
    static Value get();
11
    static constexpr Value High = Value::High;
12
    static constexpr Value Low = Value::Low;
13
};
14
15
using Port_B1 = Port<PortName::B, Pad::Pad1>;

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.
2
#define INLINE __inline__ __attribute((__always_inline__))
3
4
template<> constexpr Port_B1::Value Port_B1::High;
5
template<> constexpr Port_B1::Value Port_B1::Low;
6
7
template<>
8
INLINE void Port_B1::set (Port_B1::Value value)
9
{
10
    // 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
INLINE Port_B1::Value Port_B1::get()
23
{
24
    return PINB & PadMask::value ? High : Low;
25
}

Die Anwendung definiert sich dann Anhand der Pad-Belegungen neue 
Bezeichner, z.B. Led an Port B1:
1
using LED = Port_B1;
2
#pragma GCC poison Port_B1
3
4
bool val;
5
6
void test1 ()
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
int main() {}

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
template const Port_B1::Value Port_B1::High;
2
template const Port_B1::Value Port_B1::Low;
der Linkerfehler bleibt.  Erst eine explizite Instanziierung von Port_B1 
löst das Problem:
1
template class Port<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.

von Wilhelm M. (wimalopaan)


Angehängte Dateien:

Lesenswert?

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:
1
#include "mcu/avr8.h"
2
#include "mcu/ports.h"
3
4
using namespace AVR;
5
6
using PortB = Port<ATMega328P::PortRegister, B>;
7
using PortD = Port<ATMega328P::PortRegister, D>;
8
9
using engine       = ActiveHigh<Pin<PortB, 1>, Output>;
10
using lights       = ActiveHigh<Pin<PortB, 2>, Output>;
11
using startButton  = ActiveLow<Pin<PortD, 1>, Input>;
12
using brakes       = ActiveLow<Pin<PortD, 2>, Input>;
13
using gear         = ActiveLow<Pin<PortD, 3>, Input>;
14
15
template<typename... C>
16
struct IO final {
17
    IO() = delete;
18
    static void init() {
19
        (C::init(), ...);
20
    }
21
};
22
23
int main() {
24
    IO<engine, lights, startButton, brakes, gear>::init();
25
26
    while(true) {
27
         if (startButton::isActive()) {
28
             lights::activate();
29
             if (gear::isActive() && brakes::isActive()) {
30
                 engine::activate();
31
             }
32
         } else {
33
             engine::inactivate();
34
             lights::inactivate();
35
         }
36
     }
37
}

Und der Rest ist als Anhang (alles header-only).

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

...also den Code der 1. Versuchs in die Tonne gekloppt und nochmal bei 0 
angefangen.
1
#include <avr/io.h>
2
3
template<class P>
4
struct Base
5
{
6
    static void set (bool val)
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:
1
template<unsigned Pad>
2
struct PortB : Base<PortB<Pad>>
3
{
4
    static uint8_t volatile* port()
5
    {
6
        return &PORTB;
7
    }
8
    enum { pad = Pad };
9
    enum class padMask : uint8_t { value = 1u << pad };
10
    enum class padNotMask : uint8_t { value = (uint8_t) ~ (uint8_t) padMask::value };
11
};

Auch hier erweisen sich die strikten Enums als eher nervig.  Die 
Implementierung sieht insgesamt schlanker aus, und die Applikation kann 
dann verwenden
1
using LED = PortB<7>;
2
using LED2 = PortB<2>;
3
4
int var;
5
6
void foo()
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
int main() {}

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.

von Carl D. (jcw2)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
> enum class PortName : uint8_t { A, B, C, D };
5
> enum class Pad : 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:
>
1
> template<PortName p, Pad b>
2
> class Port
3
> {
4
>     typedef std::integral_constant<Pad,b> PadValue;
5
>     typedef std::integral_constant<uint8_t, (uint8_t) (1u << (unsigned) 
6
> PadValue::value)> PadMask;
7
>     typedef std::integral_constant<uint8_t, (uint8_t) ~PadMask::value> 
8
> PadNotMask;
9
>     enum class Value : uint8_t { High, Low };
10
> public:
11
>     static void set (Value);
12
>     static Value get();
13
>     static constexpr Value High = Value::High;
14
>     static constexpr Value Low = Value::Low;
15
> };
16
> 
17
> using Port_B1 = Port<PortName::B, Pad::Pad1>;
18
>
>
> 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.
2
> #define INLINE __inline__ __attribute((__always_inline__))
3
> 
4
> template<> constexpr Port_B1::Value Port_B1::High;
5
> template<> constexpr Port_B1::Value Port_B1::Low;
6
> 
7
> template<>
8
> INLINE void Port_B1::set (Port_B1::Value value)
9
> {
10
>     // 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
> INLINE Port_B1::Value Port_B1::get()
25
> {
26
>     return PINB & PadMask::value ? High : Low;
27
> }
28
>
>
> Die Anwendung definiert sich dann Anhand der Pad-Belegungen neue
> Bezeichner, z.B. Led an Port B1:
>
1
> using LED = Port_B1;
2
> #pragma GCC poison Port_B1
3
> 
4
> bool val;
5
> 
6
> void test1 ()
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
> int main() {}
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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von MitLeserin (Gast)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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<typename T>
2
    struct UnsignedFor;
3
    
4
    template<>
5
    struct UnsignedFor<int8_t> {
6
        typedef uint8_t type;
7
    };
8
9
    // weitere Mappings

Und dann:
1
template<typename T>
2
typename UnsignedFor<T>::type foo(T) {
3
 ...
4
}

Nur mal so als Hinweis, um die Idee der Meta-Funktionen klar zu machen 
;-)

: Bearbeitet durch User
Beitrag #5246702 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

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?

von Mikro 7. (mikro77)


Lesenswert?

Sheeva P. schrieb:
> Vielleicht habe ich da etwas nicht richtig verstanden

Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:
1
template <unsigned int n>
2
struct factorial {
3
  enum { value = n * factorial<n - 1>::value };
4
};
5
6
template <>
7
struct factorial<0> {
8
  enum { value = 1 };
9
};
10
11
// Usage examples:
12
// factorial<0>::value would yield 1;
13
// factorial<4>::value would yield 24.

von Sheeva P. (sheevaplug)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Mikro 7. schrieb:

> Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:
>
>
1
> template <unsigned int n>
2
> struct factorial {
3
>   enum { value = n * factorial<n - 1>::value };
4
> };
5
> 
6
> template <>
7
> struct factorial<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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
using usart0 = Usart<0>;
2
using usart1 = Usart<1>;
3
using usart2 = Usart<2>; // Compilezeitfehler.

Dagegen:
1
Usart usart{0};
2
Usart usart{1};
3
Usart usart{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))

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Wilhelm M. schrieb:
> Mikro 7. schrieb:
>
>> Ein anderes Beispiel (Wikipedia) erklärt es vielleicht:
>>
>>
1
>> template <unsigned int n>
2
>> struct factorial {
3
>>   enum { value = n * factorial<n - 1>::value };
4
>> };
5
>>
6
>> template <>
7
>> struct factorial<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.

Etwas idiomatischer, aber trotzdem ohne constexpr-functions wäre m.E.:
1
#include <cstddef>
2
#include <type_traits>
3
4
namespace detail {
5
    template<size_t N>
6
    struct factorial_impl {
7
        typedef std::integral_constant<size_t, N * factorial_impl<N - 1>::type::value> type;
8
    };
9
    template<>
10
    struct factorial_impl<0> {
11
        typedef std::integral_constant<size_t, 1> type;
12
    };
13
}
14
15
template<size_t N>
16
using factorial = typename detail::factorial_impl<N>::type;
17
18
static_assert(factorial<1>::value == 1);
19
static_assert(factorial<4>::value == 24);
20
21
int main() {}

Beitrag #5247281 wurde von einem Moderator gelöscht.
von Sheeva P. (sheevaplug)


Lesenswert?

Wilhelm M. schrieb:
> Dazu gibt es m.E. mehrere Gründe:

So gesehen hast Du natürlich Recht. Super erklärt, vielen Dank! ;-)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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
static inline constexpr uint8_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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

...oder
1
#include <avr/io.h>
2
template<uint8_t volatile * p>
3
struct S {};
4
5
S<(&PORTB)> s{};
1
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.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

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
static inline constexpr uint8_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*}' 
3
> [-fpermissive]
4
>    static inline constexpr uint8_t *addr = &PORTB;
5
>                                             ^
6
> port.cpp:31:44: error: 'reinterpret_cast<volatile uint8_t* {aka volatile 
7
> 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 ;-)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:
> ...oder
>
1
> #include <avr/io.h>
2
> template<uint8_t volatile * p>
3
> struct S {};
4
> 
5
> S<(&PORTB)> s{};
6
>
>
1
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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von MitLeserin (Gast)


Lesenswert?

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

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

Beitrag #5248294 wurde von einem Moderator gelöscht.
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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
unsigned volatile PORTB;
2
3
template<int p>
4
struct Port
5
{
6
  enum class Value : unsigned { High, Low };
7
  static inline void set (Value);
8
  static constexpr Value High = Value::High;
9
  static constexpr Value Low = Value::Low;
10
};
11
12
template<> constexpr Port<7>::Value Port<7>::High;
13
template<> constexpr Port<7>::Value Port<7>::Low;
14
15
template<>
16
inline void Port<7>::set (Port<7>::Value value)
17
{
18
  if (High == value)
19
    PORTB |= 2;
20
  else
21
    PORTB &= ~2;
22
}
23
24
int main()
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.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

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
unsigned volatile PORTB;
2
3
template<int p>
4
struct Port {
5
    enum class Value : unsigned { High, Low };
6
    static inline void set (Value);
7
    static constexpr Value High = Value::High;
8
    static constexpr Value Low = Value::Low;
9
};
10
11
//template<> 
12
//constexpr Port<7>::Value Port<7>::High;
13
//template<> 
14
//constexpr Port<7>::Value Port<7>::Low;
15
16
template<>
17
inline void Port<7>::set(Port<7>::Value value) {
18
    if (Value::High == value)
19
        PORTB |= 2;
20
    else
21
        PORTB &= ~2;
22
}
23
24
int main() {
25
    Port<7>::set(Port<7>::Value::Low);
26
    Port<7>::set(PORTB ? Port<7>::Value::Low : Port<7>::Value::High);
27
}

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:
1
unsigned volatile PORTB;
2
3
template<int p>
4
struct Port
5
{
6
  enum class Value : unsigned { High, Low };
7
  static inline void set (Value value) {
8
      if (Value::High == value)
9
        PORTB |= (1 << p);
10
      else
11
        PORTB &= ~(1 << p);
12
  }
13
  static constexpr Value High = Value::High;
14
  static constexpr Value Low = Value::Low;
15
};
16
17
18
int main() {
19
  Port<7>::set (Port<7>::Value::Low);
20
  Port<7>::set (PORTB ? Port<7>::Value::Low : Port<7>::Value::High);
21
}

Hier ist p auch constexpr, da ein template-nicht-typ-parameter.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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
const Port<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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
const Port<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 ...

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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’

von Wilhelm M. (wimalopaan)


Lesenswert?

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 ;-)

von Wilhelm M. (wimalopaan)


Lesenswert?

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
struct X {
3
    static constexpr int x = 0;
4
};
5
6
template<>
7
constexpr int X<1>::x = 1;
8
9
int main() 
10
{   
11
    return X<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 
;-))

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Ich versteh immer weniger.  Ein
1
template<> struct X<1>;
ist doch eine explizite Instanziierung.  Das wirft
1
In function 'int main()':
2
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

Beitrag #5249441 wurde von einem Moderator gelöscht.
Beitrag #5249476 wurde von einem Moderator gelöscht.
Beitrag #5249488 wurde von einem Moderator gelöscht.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.