> 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.
Ich vermute, Du hast jetzt folgendes:
1
template<int>
2
structX{
3
staticconstexprintx=0;
4
};
5
6
template<>
7
structX<1>;// Deklaration der vollständigen Spezialisierung
8
9
intmain(){
10
X<0>a;// Instanziierung, parametriert mit 0
11
returnX<1>::x;// Instanziierung, parametriert mit 1
12
}
Hier hast Du das allg. Template definiert, das spezielle aber nur
deklariert. Deswegen kannst Du das spezielle Template nicht
instanziieren, denn
1
template<>
2
structX<1>;
ist keine Template-Instanziierung, sondern die Deklaration einer
Spezialisierung.
Fügst Du nun die Template-Definition ein (die Deklaration könntest Du
nun weglassen), ist es ok:
1
template<int>
2
structX{
3
staticconstexprintx=0;
4
};
5
6
template<>
7
structX<1>;// Deklaration der vollständigen Spezialisierung (hier nicht notwendig)
8
9
template<>
10
structX<1>{// Template-Definition
11
staticconstexprintx=1;
12
};
13
14
//template<>
15
//constexpr int X<1>::x;
16
17
intmain(){
18
X<0>a;// Instanziierung, parametriert mit 0
19
returnX<1>::x;// Instanziierung, parametriert mit 1
X<1> ist eine Klasse, und von dieser will man ein statisches Member
verwenden, hier X<1>::x. Das Layout von Objekten im Speicher darf nicht
von deren Verwendung abhängen (modulo as-if Rule). Für X<1>::x bedeutet
dies, dass diese Komponente eine Speicherinstanz haben muss für den
Fall, dass irgebdwo die Adresse von x genommen wird. Das
template<> constexpr int X<1>::x;
soll die entsprechende Definition sein, wie sie für andere statische
Member auch nötig ist.
Um dass zu errechen bzw. überhaupt machen zu dürfen, muss erst X<1> als
explizite Spezialisierung vorhanden sein wie von Dir angemerkt; damit
ist PR83484 ungültig weil es keine explizite Spezialisierung hat, und
der korrekte Code (inclusive Addressnahme von x) ist:
1
template<int>structX{staticconstexprintx=0;};
2
3
template<>structX<1>{staticconstexprintx=0;};
4
5
// template<> constexpr int X<1>::x;
6
7
intmain()
8
{
9
constint*volatilep=&X<1>::x;
10
returnX<1>::x;
11
}
1
In function 'int main()':
2
-.o: In function `main':
3
-.cpp:(.text.startup+0xa): undefined reference to `X<1>::x'
Der Linkerfehler ist ok und beruht darauf, dass X<1>::x nicht definiert
wurde. Entfernen des Kommentars, so dass eine Instanz von X<1>::x
existiert, ergibt:
1
-:3:47: warning: too many template headers for 'X<1>::x' (should be 0)
2
template<> struct X<1> { static constexpr int x = 0; };
3
^
4
-.cpp:3:47: note: members of an explicitly specialized class are defined without a template header
Oder ist der Linkerfehler wie in PR83484, nur diesmal mit der nötigen
expliziten Spezialisierung, und die Version mit Kommentar (also ohne
"template<> constexpr int X<1>::x;") ist die korrekte, denn die
Definition von ::x erfolgt bereits mit der Deklatation?
Johann L. schrieb:> X<1> ist eine Klasse,
sagen wir, es ist ein konkreter Typ ...
> und von dieser will man ein statisches Member> verwenden, hier X<1>::x. Das Layout von Objekten im Speicher darf nicht> von deren Verwendung abhängen (modulo as-if Rule). Für X<1>::x bedeutet> dies, dass diese Komponente eine Speicherinstanz haben muss für den> Fall, dass irgebdwo die Adresse von x genommen wird.
Ja.
> Das>> template<> constexpr int X<1>::x;>> soll die entsprechende Definition sein, wie sie für andere statische> Member auch nötig ist.
Nein, es ist die vollständige Spezialisierung eines einzelnen(!) members
von X, und damit ohne Initialisierer nur eine Deklaration (s.a. den
textausschnitt den ich gepostet hatte oder den Link in cppreference).
> Um dass zu errechen bzw. überhaupt machen zu dürfen, muss erst X<1> als> explizite Spezialisierung vorhanden sein wie von Dir angemerkt;
Nein, das hast Du falsch verstanden: man sollte auch einzelnen static
member spezialisieren dürfen, und da hat der gcc den Bug. Das darf man
allerdings nur, wenn noch keine Spezialisierung des gesamten Templates
vorliegt.
Bin jetzt nich ganz sicher, auf was Du Dich genau beziehst, deswegen am
besten immer vollständigen Code.
Ausserdem scheint es von der Version abzuhängen. Mit gcc-7.2.1. ist
folgendes ok (und richtig), Dein g++ (Version?) scheint es nicht zu
mögen:
//constexpr int X<1>::x; // überflüssig, da schon eine vollst. Spezialisierung deklariert wurde, warnung bei g++, error bei clang++
13
14
//template<>
15
//constexpr int X<1>::x{}; // unsinnig (ill-formed), da schon eine vollst. Spezialisierung als Definition vorliegt, dies wäre dann die zweite Definition. error bei g++, error bei clang++
Nun kommt hinzu, dass der clang auch m.E. einen Bug hat, der Linker
liefert nämlich dann undefined-reference.
(Daran kann man evtl. ablesen, dass die Möglichkeit, einzelne(!) Member
zu spezialisieren, kaum von Anwendern genutzt wird ...).
Wilhelm M. schrieb:> (Daran kann man evtl. ablesen, dass die Möglichkeit, einzelne(!) Member> zu spezialisieren, kaum von Anwendern genutzt wird ...).
Das war garnicht das Ziel! Sondern es ging darum, einen Linkerfehler zu
beheben wie in
1
structX{staticconstintx=0;};
2
// const int X::x;
3
4
constint*px(){return&X::x;}
5
6
intmain(){returnX::x;}
Hier muss X::x definiert werden, und das geschieht per "const int
X::x;". Der Linkerfehler entsteht also dadurch, dass die
auskommentierte Zeile fehlt. Hinzufügen der Zeile ist die Lösung des
Problems.
Allerdings war es keine normale Klasse, sondern ein (komplexeres)
Template und mit constexpr anstatt const:
1
template<int>structX{staticconstintx=0;};
2
template<>constintX<1>::x;
3
4
constint*px(){return&X<1>::x;}
5
6
intmain(){returnX<1>::x;}
Mir war nicht klar, dass dadurch, dass die Klasse einen
Template-Parameter erhält, Zeile 2 plötzlich keine Definition mehr ist,
sondern ill-formed sofern ich die Kommentare zu PR83484 recht verstehe.
Die alles entstand in einem Kontext, wo ich der immer wieder — auch in
diesem Thread — gegeben Empfehlung folgte, einen Wert, der zur
Compile-Zeit bekannt ist, in einem constexpr oder als einen Typen
darzustellen.
Als zur Compilezeit bekannten Wert wählte ich &PORTD — um näher am Thema
des Threads zu bleiben hätte ich auch &GPIOR0 wählen können.
Diese zur Compile-Zeit bekannten Adressen sind grundlegende Basis jeder
Implementierung, die sich mit SFRs befasst; zumindest in einem Kontext,
in der "Standard"-Header wie avr/io.h diese Werte zur Verfügung stellen.
Die avr-libc enthält ca. 48000 so zur Verfügung gestellter Adressen. Es
ist also nicht komplett abwegig, diese Resource nutzen zu wollen.
Alternative wäre, diesen Zoo magischer Zahlen neu zu erfinden.
Dass &GPIOR0 zur Compile-Zeit bekannt ist, sieht man an folgendem Code:
1
#include<avr/io.h>
2
3
externvolatileuint8_t*constaddr;
4
volatileuint8_t*constaddr=&GPIOR0;
5
6
volatileuint8_t*f()
7
{
8
if(__builtin_constant_p(&GPIOR0))
9
__asm("; yes");
10
else
11
__asm("; no");
12
__asm("; addr = %0"::"n"(&GPIOR0));
13
return&GPIOR0;
14
}
Übersetzt für einen ATmega168 und unabhängig von Optimierung, erzeugt
avr-gcc daraus (hier mit -O0):
1
.section .rodata
2
addr:
3
.word 62
4
5
_Z1fv:
6
...
7
; foo.cpp:9: __asm ("; yes");
8
; yes
9
; foo.cpp:12: __asm ("; addr = %0" :: "n" (&GPIOR0));
10
; addr = 62 ;
11
; foo.cpp:13: return &GPIOR0;
12
ldi r24,lo8(62)
13
ldi r25,0
14
...
Der Code compiliert ohne Warnung, insbesondere gibt es keine Warnung
gegen Constraint "n". Der Init-Wert von addr ist zur Compilezeit
bekannt — sowohl im Code als auch in Initialzer für Daten im Static
Storage. Der Code enthält KEIN "; no" sondern nur ein "; yes".
Dein Code war keine Hilfe, da er aus didaktischen Gründen avr/io.h
meidet und magische Zahlen verwendet.
Im Code von mcucpp hab ich versucht, die entspechende Stelle zu finden.
Es werden Makros oder Funktionen verwendet, ohne die entsprechenden
Header zu includen, etc. Es ist also nicht ganz einfach sich da zu
orientieren. Und das runterzuladen und zu debuggen wollte ich mir nicht
antun. Mit Code, den man nur durch Debuggen verstehen kann, ist m.E.
etwas falsch gelaufen; auch wenn er funktioniert, ist es wohl keine gute
Quelle, um Best Practices zu sehen.
Und schließlich geht es nur darum, eine Compile-Zeit Konstante in
constexpr oder in einen Typen zu bekommen, und das Paradigma dazu sollte
in wenigen Zeilen darstellbar sein. Auf der Suche nach diesem Paradigma
bin ich über den Linkerfehler gestolpert (und wollte ihn verstehen
anstatt irgendwie zu umgehen).
So langsam schwant mir aber, dass C++ als Designziel hat, bestimmte
Compile-Zeit Konstanten dediziert als solche zu verbieten, insbesondere
zur Compile-Zeit bekannte Adressen.
Ich gehe mal davon aus, dass constexp User-defined Literals oder
constexpr Überladung von Cast-Operator mich dem Ziel auch nicht näher
bringt da unerreichbar...
Johann L. schrieb:> Diese zur Compile-Zeit bekannten Adressen sind grundlegende Basis jeder> Implementierung, die sich mit SFRs befasst;
Da aber immer ein reinterpret_cast notwendig ist bzw. literale keine
lvalues sind, kann man sie so nicht verwenden (s.a. ein paar Post weiter
oben von mir).
> zumindest in einem Kontext,> in der "Standard"-Header wie avr/io.h diese Werte zur Verfügung stellen.> Die avr-libc enthält ca. 48000 so zur Verfügung gestellter Adressen. Es> ist also nicht komplett abwegig, diese Resource nutzen zu wollen.> Alternative wäre, diesen Zoo magischer Zahlen neu zu erfinden.
Nein.
Warum ich die Präprozessor-Makros nicht verwende, habe ich oben bereits
geschrieben.
Aber natürlich kann man die Info aus den AVR-Headern verwenden:
1) Man erzeugt sich mit etwas Scripting alternative Header, in denen die
Adressen der SFRs - wie es meiner Meinung nach am besten wäre - als
uintptr_t dargestellt sind.
2) Man setzt sich etwas über die Macros hinweg (s.u.)
Die Wurzel des Übels liegt leider in der "falschen" Wahl der Macros für
die SFR-Adresswerte. Allerdings kann man es nicht wirklich den Urhebern
anlasten, denn sie hatten vermutlich nie eine C++-Lösung im Sinn ;-)
Hier ein Beispiel:
1
#include<avr/io.h>
2
3
// um den C-style cast auf volatile T* los zu werden
So sollte alles so sein, wie Du möchtest. Eine analoge Variante verwende
ich in dem Code, den ich zur Verfügung gestellt hatte, allerdings ohne
weder 1) noch 2) zu verwenden. Das hat einfach historische Gründe ;-)
Ich habe prinzipiell nichts gegen C++ und nutze es auch selbst. Aber
diese Templates sind mir ein Buch mit 7 Siegeln (zu abstrakt, zu
kryptische Syntax). Glücklicherweise ist man nicht gezwungen, sie zu
benutzen.
[OT]
Es ist mir schon öfters untergekommen, dass Leute vehement gegen etwas
sind, nur weil sie es nicht verstehen. Aber damit lässt sich denke ich
noch gut leben. Was aber gar nicht geht ist, wenn dann alles schlecht
geredet wird, was das Unbekannte betrifft. Das zeugt meiner Meinung nach
von höchster Unprofessionalität.
Aber es geht noch schlimmer: Wenn es destruktiv wird auf unsachliche
oder gar persönliche Weise (wahrscheinlich um zu provozieren).
Irgendwie führt das bei Grundsatzdiskussionen hier häufig zu letzterem,
was dann eher wenig Spaß macht.
Aber ich finde es gut, dass hier durch unsere Admins so etwas schnellst
möglich entfernt wird. Danke Euch dafür. Wobei es wiederum doch etwas
traurig ist wie viel in solchen Threads wie diesem hier entfernt werden
muss.
Grüße Oliver
Oliver J. schrieb:> Aber ich finde es gut, dass hier durch unsere Admins so etwas schnellst> möglich entfernt wird. Danke Euch dafür.
Auch von mir.
> Wobei es wiederum doch etwas traurig ist wie viel in solchen> Threads wie diesem hier entfernt werden muss.
Nur ein Scheinriese, wie in Jim Knopf.
Es sind nicht viele Beiträge, die gelöscht werden, sondern der
immergleiche "Beitrag", den ein einziger "User" immer wieder postet oder
posten lässt.
Von daher wird lediglich immer und immer wieder ein einziger "Beitrag"
entfernt.
Also kein Riese, sondern nur ein Zwerg.
Johann L. schrieb:> Es sind nicht viele Beiträge, die gelöscht werden, sondern der> immergleiche "Beitrag", den ein einziger "User" immer wieder postet oder> posten lässt.>> Von daher wird lediglich immer und immer wieder ein einziger "Beitrag"> entfernt.
Jepp. Das lässt sich übrigens hervorragend mit Skripten automatisieren.
Mit entsprechenden Heuristiken sind auch Abweichungen davon problemlos
handhabbar. Wofür hat man denn seine Dipl.-Arbeit über Text- und
Mustererkennung geschrieben :-}
Wer sich übrigens an dem Hinweis auf gelöschte Beiträge stört, kann
diese auch ausblenden. Im Firefox sollte das so gehen:
Beitrag "3) FF - Gelöschte Beiträge ausblenden"
>
Was mir jetzt daran nicht so sehr gefällt ist:
uintptr_t hätte ich gerne eingeschränkt auf die im Chip tatsächlich
vorhanden Port Adressen ebenfalls die Pins.
gp0::set<1>(); ist mir ehrlich gesagt zu kryptisch ...
gp0::set<0>(); da muss man auch erst mal nachdenken was gemeint ist.
Was machen die beiden Funktionen setzen die jetzt einen ganzen Port auf
1 oder 0 oder doch nur beide einen Pin auf 1 ich finde das nicht
eindeutig.
Für Leute, die immer die neuesten Regeln von C++ im Kopf haben mag das
eindeutig sein und für den Compiler ist es das auch.
>>>> Was mir jetzt daran nicht so sehr gefällt ist:>> uintptr_t hätte ich gerne eingeschränkt auf die im Chip tatsächlich> vorhanden Port Adressen ebenfalls die Pins.
Schau Dir den Code an, den ich hier mal als Anhang gepostet hatte. Dort
sind Meta-Funktionen drin, die tatsächlich nur für die vorhandenen
internen Peripherie-Einheiten Adressen liefern. Für nicht vorhandenes
liefern sie einen Compilezeit-Fehler.
Das obige war nur ein zusammengekochtes Beispiel für die "Anforderung"
von Johann, die #defines aus der avr-libc als Template-Argument zu
verwenden.
> gp0::set<1>(); ist mir ehrlich gesagt zu kryptisch ...
kannst ja ein
1
gpior0::setBit<1>();
oder
1
gpior0::set(Bit1);
oder, oder oder ... draus machen.
> Was machen die beiden Funktionen setzen die jetzt einen ganzen Port auf> 1 oder 0 oder doch nur beide einen Pin auf 1 ich finde das nicht> eindeutig.
Wie oben schon gesagt: es ist auch nur eine Erklärung für eine spezielle
Frage gewesen, in der es gar nicht um die Elementfunktion set() ging,
sondern um die Template-Argument.
Johann L. schrieb:> Nur ein Scheinriese, wie in Jim Knopf.
Hmmm...
Wenn mich meine Erinnerung nicht trügt, war das doch ein gutmütiges,
zurückhaltendes, Kerlchen.
Das möchte ich von unserem Priester, hier, nicht behaupten wollen.
Arduino F. schrieb:> Johann L. schrieb:>> Nur ein Scheinriese, wie in Jim Knopf.>> Wenn mich meine Erinnerung nicht trügt, war das doch ein> gutmütiges, zurückhaltendes, Kerlchen.
Es ging mir dabei um das Gleichnis eines Scheinriesen:
Ein Problem oder eine Sache, die riesig erscheint. Aber je mehr man
sich ihm nähert (oder je mehr man darüber erfährt oder sich damit
befasst) desto mehr schrumpft es zusammen, bis es schließlich nur noch
ein kleiner Zwerg ist.
Johann L. schrieb:> Arduino F. schrieb:>> Johann L. schrieb:>>> Nur ein Scheinriese, wie in Jim Knopf.>>>> Wenn mich meine Erinnerung nicht trügt, war das doch ein>> gutmütiges, zurückhaltendes, Kerlchen.>> Es ging mir dabei um das Gleichnis eines Scheinriesen:>> Ein Problem oder eine Sache, die riesig erscheint. Aber je mehr man> sich ihm nähert (oder je mehr man darüber erfährt oder sich damit> befasst) desto mehr schrumpft es zusammen, bis es schließlich nur noch> ein kleiner Zwerg ist.
Wobei "unser" Scheinriese schon aus der Ferne ein Zwergchen ist.
Hallo zusammen,
ich versuche gerade, in TMP reinzuschnuppern und mit
https://github.com/kvasir-io/Kvasir etwas herumzuprobieren. Das ist echt
eine verdammt abgefahrene Sache und ich frage mich, warum das nicht mehr
verwendet wird.
Ist es zu kompliziert (sodass nur wenige Leute in der Lage sind, das zu
verstehen?), ist es schlecht zu debuggen oder was sind die Gründe?
Ich hab mal eine ganz praktische Frage: Wie definiert ihr ein sauberes
Interface in C++ (mit viel TMP oder zumindest class templates)? Immerhin
liegt die gesamte Template Implementation öffentlich in den .hpp
Headern?!
In C habe ich eine eine foo.h und eine foo.c Datei, in dem Header stehen
nur öffentliche Funktionsprototypen drin, meistens mit einigen
Kommentaren. Damit ist das öffentliche Interface schon fertig und
übersichtlich. In (TMP) C++ erkenne ich aber häufig auf Anhieb gar
nicht, welche Methoden und Typen es überhaupt gibt, weil eben viel
Implementation im Header steht (stehen muss..).
In "klassischem" C++ geht die Datenkapselung/Information hiding
eigentlich noch weiter, wird aber durch die Templates ad absurdum
geführt, oder?
Weitere Frage an die C++ Benutzer: Wie definiert ihr Interfaces im
Embedded Umfeld? Beispielsweise für einen Beschleunigungssensor und ein
Gyroskop. Beide Sensoren werden regelmäßig gesampelt, können Daten
zurückgeben, haben eine Schnittstelle über die sie angeschlossen sind
und so weiter. Vereinheitlicht ihr solche Dinge in einem (virtuellen)
Interface, das später im Code verwendet wird oder verzichtet ihr (aus
Performancegründen?) komplett darauf? Wenn nicht, spielt das mit TMP
zusammen?
Danke :)
Jan K. schrieb:> Hallo zusammen,>> ich versuche gerade, in TMP reinzuschnuppern und mit> https://github.com/kvasir-io/Kvasir etwas herumzuprobieren. Das ist echt> eine verdammt abgefahrene Sache und ich frage mich, warum das nicht mehr> verwendet wird.>> Ist es zu kompliziert (sodass nur wenige Leute in der Lage sind, das zu> verstehen?), ist es schlecht zu debuggen oder was sind die Gründe?
Ich will nicht sagen, dass es übermäßig kompliziert ist, allerdings ist
es wohl sehr viel anders als üblicher OO-Code. Und ich glaube auch
nicht, dass viele sich zunächst sofort vorstellen können, wozu man
Meta-Funktionen, also Funktionen die mit Typen statt mit Objekten/Werten
rechnen, braucht.
> Ich hab mal eine ganz praktische Frage: Wie definiert ihr ein sauberes> Interface in C++ (mit viel TMP oder zumindest class templates)? Immerhin> liegt die gesamte Template Implementation öffentlich in den .hpp> Headern?!
Dir geht es bei der Frage um die Code-Strukturierung? Also kein
<interface> im OOP-Sinne? Wenn ein template instanziiert (also ein
konkreter Typ gebildet) werden soll, muss es vollständig bekannt sein.
Das ist der Grund, warum templates (typischerweise) vollständig in
Header-Files stehen. Dies wird sich erst mit C++20 und modules ändern.
> In C habe ich eine eine foo.h und eine foo.c Datei, in dem Header stehen> nur öffentliche Funktionsprototypen drin, meistens mit einigen> Kommentaren. Damit ist das öffentliche Interface schon fertig und> übersichtlich. In (TMP) C++ erkenne ich aber häufig auf Anhieb gar> nicht, welche Methoden und Typen es überhaupt gibt, weil eben viel> Implementation im Header steht (stehen muss..).
Das hat gar nichts mit TMP, sondern einfach mit templates zu tun (s.o.).
Du kannst aber nach wie vor, die Deklaration und die Definition von
template-Elementen trennen. Ist zwar immer noch alles in der
Header-Datei, aber abgesetzt voneinander und deswegen ggf.
übersichtlicher.
> In "klassischem" C++ geht die Datenkapselung/Information hiding> eigentlich noch weiter, wird aber durch die Templates ad absurdum> geführt, oder?
Nein. Datenkapselung bedeutet ja nicht, wie man etwas auf Dateien
aufteilt. Da hat man ja in C++ und C völlige Freiheit (anders als etwa
bei Java). Auch in einem Template ist private, protected und public das
was es in normalen Klassen auch ist. Möchte man die
Template-Realisierung verstecken, so muss man auf vorinstanziierte
Templates ausweichen, was aber ziemlicher Aufwand (und Murks) m.E. ist.
> Weitere Frage an die C++ Benutzer: Wie definiert ihr Interfaces im> Embedded Umfeld? Beispielsweise für einen Beschleunigungssensor und ein> Gyroskop. Beide Sensoren werden regelmäßig gesampelt, können Daten> zurückgeben, haben eine Schnittstelle über die sie angeschlossen sind> und so weiter. Vereinheitlicht ihr solche Dinge in einem (virtuellen)> Interface, das später im Code verwendet wird oder verzichtet ihr (aus> Performancegründen?) komplett darauf?
Das wäre der klassiche OOP Ansatz, also Laufzeit-Polymorphie.
> Wenn nicht, spielt das mit TMP> zusammen?
Ich glaube, Du meinst hier nicht TMP, sondern einfach templates. Mit
templates hat man statische Polymorphie. Das kann man mit
Laufzeit-Polymorphie kombinieren, natürlich. Weil man aber die
Laufzeitkosten der davon vermeiden möchte, bedient man sich der stat.
Polymorphie.
TMP = template-meta-programmierung.
Darunter wird im allgemeinen das Erstellen von Meta-Funktionen
verstanden. Die haben als Operanden i.A. Typen und bilden diese wieder
auf Typen ab. Normale Funktionen operieren mit Werten. Daher der Name
Meta-Funktion. Mit Meta-Funktionen "berechnet" man also Typ-Abbildungen
Typ->Typ (oder Typ->Konstante, Konstante->Typ).
Wilhelm M. schrieb:>> Ich will nicht sagen, dass es übermäßig kompliziert ist, allerdings ist> es wohl sehr viel anders als üblicher OO-Code. Und ich glaube auch> nicht, dass viele sich zunächst sofort vorstellen können, wozu man> Meta-Funktionen, also Funktionen die mit Typen statt mit Objekten/Werten> rechnen, braucht.
okay :)
>>> Ich hab mal eine ganz praktische Frage: Wie definiert ihr ein sauberes>> Interface in C++ (mit viel TMP oder zumindest class templates)? Immerhin>> liegt die gesamte Template Implementation öffentlich in den .hpp>> Headern?!>> Dir geht es bei der Frage um die Code-Strukturierung? Also kein> <interface> im OOP-Sinne? Wenn ein template instanziiert (also ein> konkreter Typ gebildet) werden soll, muss es vollständig bekannt sein.> Das ist der Grund, warum templates (typischerweise) vollständig in> Header-Files stehen. Dies wird sich erst mit C++20 und modules ändern.
Ja, kein Interface im Sinne einer pure virtual Klasse oder einem
"interface" in C#. Ich meine hier die öffentliche Schnittstelle.
Warum Templates im Header stehen müssen verstehe ich, es ist ja nur eine
Art Bauplan und die durch das Template generierte Klasse eines
speziellen Typs ist eben noch nicht erstellt.
>> In C habe ich eine eine foo.h und eine foo.c Datei, in dem Header stehen>> nur öffentliche Funktionsprototypen drin, meistens mit einigen>> Kommentaren. Damit ist das öffentliche Interface schon fertig und>> übersichtlich. In (TMP) C++ erkenne ich aber häufig auf Anhieb gar>> nicht, welche Methoden und Typen es überhaupt gibt, weil eben viel>> Implementation im Header steht (stehen muss..).>> Das hat gar nichts mit TMP, sondern einfach mit templates zu tun (s.o.).
Ja, korrekt, sorry. Es geht hier allgemein um Templates.
> Du kannst aber nach wie vor, die Deklaration und die Definition von> template-Elementen trennen. Ist zwar immer noch alles in der> Header-Datei, aber abgesetzt voneinander und deswegen ggf.> übersichtlicher.
Also z.B. oben im Header irgendwo die (dokumentierten) Prototypen der
Klasse auflisten und die Implementation weiter hinten.
>>> In "klassischem" C++ geht die Datenkapselung/Information hiding>> eigentlich noch weiter, wird aber durch die Templates ad absurdum>> geführt, oder?>> Nein. Datenkapselung bedeutet ja nicht, wie man etwas auf Dateien> aufteilt. Da hat man ja in C++ und C völlige Freiheit (anders als etwa> bei Java). Auch in einem Template ist private, protected und public das> was es in normalen Klassen auch ist. Möchte man die> Template-Realisierung verstecken, so muss man auf vorinstanziierte> Templates ausweichen, was aber ziemlicher Aufwand (und Murks) m.E. ist.
Okay, also aus OOP Sicht ist die Kapselung vorhanden, weil instanziierte
Objekte einer (templatebasierten) Klasse dennoch nicht ihre Interna
preisgeben?
Ich dachte bislang, das Verstecken der Implementation gehöre dazu, so im
Sinne, "mir ist egal, wie das implementiert ist, ich muss nur die
Prototypen kennen".
>>> Weitere Frage an die C++ Benutzer: Wie definiert ihr Interfaces im>> Embedded Umfeld? Beispielsweise für einen Beschleunigungssensor und ein>> Gyroskop. Beide Sensoren werden regelmäßig gesampelt, können Daten>> zurückgeben, haben eine Schnittstelle über die sie angeschlossen sind>> und so weiter. Vereinheitlicht ihr solche Dinge in einem (virtuellen)>> Interface, das später im Code verwendet wird oder verzichtet ihr (aus>> Performancegründen?) komplett darauf?>> Das wäre der klassiche OOP Ansatz, also Laufzeit-Polymorphie.
Genau. Hast du oder jmd. anders hier Polymorphie zur Laufzeit produktiv
eingesetzt? In C++ gibt es sonst keine anderen Möglichkeiten, Interfaces
(diesmal wirklich im klassischen OOP Sinn, pure virtual Basisklasse in
C++) zu definieren, oder? Vielleicht ist das aber auch eine Frage für
einen eigenen Thread.
>>> Wenn nicht, spielt das mit TMP>> zusammen?>> Ich glaube, Du meinst hier nicht TMP, sondern einfach templates. Mit> templates hat man statische Polymorphie. Das kann man mit> Laufzeit-Polymorphie kombinieren, natürlich. Weil man aber die> Laufzeitkosten der davon vermeiden möchte, bedient man sich der stat.> Polymorphie.
Die Frage war zu weit gefasst, als dass hier klar geworden wäre was ich
meinte. Bin mir gerade auch selbst nicht mehr sicher.
Mein Hauptanliegen ist: ich möchte - im embedded Umfeld - ein public
interface als "Vertrag" für alle Entwickler definieren, gegen das sie
dann programmieren können. So kann einer das Modul für den Gyro und der
andere das Modul für den Beschleunigungssensor erstellen. Wie würdest
du/ihr solch eine Schnittstelle definieren?
> TMP = template-meta-programmierung.> ...
Werde mir das nochmal ganz genau zu Gemüte führen. Bisher habe ich das
eher als Mechanismus zum Erstellen von zur Kompilierzeit bekannten
(teils sehr komplexen, inklusive Fehlerbehandlung etc) Ausdrücken
verstanden. Aber das ist möglicherweise nur eine Anwendung.
Vielen Dank für die Antworten!
Jan K. schrieb:> Also z.B. oben im Header irgendwo die (dokumentierten) Prototypen der> Klasse auflisten und die Implementation weiter hinten.
Ja.
>> Nein. Datenkapselung bedeutet ja nicht, wie man etwas auf Dateien>> aufteilt. Da hat man ja in C++ und C völlige Freiheit (anders als etwa>> bei Java). Auch in einem Template ist private, protected und public das>> was es in normalen Klassen auch ist. Möchte man die>> Template-Realisierung verstecken, so muss man auf vorinstanziierte>> Templates ausweichen, was aber ziemlicher Aufwand (und Murks) m.E. ist.> Okay, also aus OOP Sicht ist die Kapselung vorhanden, weil instanziierte> Objekte einer (templatebasierten) Klasse dennoch nicht ihre Interna> preisgeben?
Instanziierte Objekte? Du meinst instanziierte Templates? Oder das
Exemplar einer Template-Klasse?
Wenn ein Klassentemplate instanziiert wird, wird ein konkreter Typ
erstellt. Dies nennt man dann Templateklasse, um es von
nicht-generischen Klassen zu unterscheiden. In dieser Templateklasse
verhalten sich die Zugriffsarten private, protected und public ganz
normal.
> Ich dachte bislang, das Verstecken der Implementation gehöre dazu, so im> Sinne, "mir ist egal, wie das implementiert ist, ich muss nur die> Prototypen kennen".
Man kann bei templates die Implementierung durch Code-Strukturierung
nicht wirklich sinnvoll verstecken wie bei normalen Klassen. Das ist
dann so ähnlich wie bei Java: sehe ich die Schnittstelle, sehe ich auch
die Implementierung.
>>> Weitere Frage an die C++ Benutzer: Wie definiert ihr Interfaces im>>> Embedded Umfeld? Beispielsweise für einen Beschleunigungssensor und ein>>> Gyroskop. Beide Sensoren werden regelmäßig gesampelt, können Daten>>> zurückgeben, haben eine Schnittstelle über die sie angeschlossen sind>>> und so weiter. Vereinheitlicht ihr solche Dinge in einem (virtuellen)>>> Interface, das später im Code verwendet wird oder verzichtet ihr (aus>>> Performancegründen?) komplett darauf?
Du möchtest eine C++-Interface-Klasse schreiben? Der Weg geht ja über
rein-virtuelle Elementfunktionen in einer Basisklasse. Damit haben wir
ja in jedem Fall ein vtable ....
Aber: verwendest Du diese Typen auch tatsächlich beliebig polymorph?
Oder ist das Interface nur eine Implementierungsvorgabe? Zudem: möchtest
Du wirklich, das beliebig viele Objekte Deiner Klassen erstellt werden
können, oder nur so viele, wie Du tatsächlich an dem µC angeschlossen
hast (an konkreten Pins bzw. int. Peripherie).
Dann kannst Du dyn. Polymorphie durch stat. Polymorphie ersetzen (dazu
kann man im übrigen auch heterogene Container, etwa std::tuple<>
verwenden).
Wenn Du ein template hast (Dein periodischer Sampler), stellst Du
implizite Typanforderungen an die Typ-Parameter (Dein Sensor). Erfüllt
ein Typ-Parameter das nicht, scheitert die Compilierung mit "hübschen"
Fehlermeldungen. Das kennen wir alle. Möchte man stattdessen die
Anforderungen an einen Typ-Parameter explizit machen (wie ein
Interface), so kann man das ab C++11 mit static_asserts und type-traits
machen (umständlich) oder ab C++20 bzw. jetzt schon mit dem GCC ab
Version 6.3 mit Constraints und Concepts. Man kann Concepts und die
damit gebildeten Constraints als "Interface"-Anforderung für
Template-Parameter auffassen.
>> Das wäre der klassiche OOP Ansatz, also Laufzeit-Polymorphie.> Genau. Hast du oder jmd. anders hier Polymorphie zur Laufzeit produktiv> eingesetzt?
Natürlich - alles ganz normal. Es gibt Situationen, damit kann/möchte
man auf die dyn. Polymorphie nicht verzichten.
> In C++ gibt es sonst keine anderen Möglichkeiten, Interfaces> (diesmal wirklich im klassischen OOP Sinn, pure virtual Basisklasse in> C++) zu definieren, oder?
Doch: s.o.
> Mein Hauptanliegen ist: ich möchte - im embedded Umfeld - ein public> interface als "Vertrag" für alle Entwickler definieren, gegen das sie> dann programmieren können. So kann einer das Modul für den Gyro und der> andere das Modul für den Beschleunigungssensor erstellen. Wie würdest> du/ihr solch eine Schnittstelle definieren?
Ich würde ein Sampler-Template schreiben, das mit den Sensor-Typen
parametrieren und die Anforderungen an die Sensor-Typen mit Constraints
beschreiben, und diese Constraints können aus Concepts zusammengesetzt
sein.
> Werde mir das nochmal ganz genau zu Gemüte führen. Bisher habe ich das> eher als Mechanismus zum Erstellen von zur Kompilierzeit bekannten> (teils sehr komplexen, inklusive Fehlerbehandlung etc) Ausdrücken> verstanden.
Ausdrücke berechnen in C++ Werte (also Objekte, auch ein "int" ist ein
Objekt). Dies kann man auch zur Compilezeit machen, wen möglich. Dies
kann man in der Tat auch mit TMP durchführen, ist aber old school (so
ist übrigens die turing-Vollständigkeit des template mechanismus
"zufällig" entdeckt worden). Ab C++11/14/17 benutzt man dafür
constexpr-functions bzw. callables, die durch constexpr
lambda-expressions erzeugt werden.