Neuer Datentyp: 48-Bit Int
-
gibt es eigentlich eine garantie, dass sizeof(u48) tatsächlich nur 6byte ist (auf den üblichen plattformen) ? immerhin ist es kein POD. diesbzgl. ist mir sowieso nicht ganz klar, was gefordert ist:
a) der wertebereich des typs entspricht genau dem eines 48bit unsigned integers, also 0..2^^48-1
b) der typ beansprucht genau 48bit speicher und padding in arrays ist verboten.falls nur a) gefordert ist, könnte man, wie erwähnt, ohne weiters bitfelder benutzen, was den vorteil hat, dass der compiler damit direkt umgehen kann:
typedef unsigned long long u64; struct u48 { u64 value:48; };
das hat allerdings dieselben speicheranforderungen wie ein 64bit integer, also ist b) nicht erfüllt. zum rechnen muss man jetzt bezug auf value nehmen, oder man baut eben eine ganze klasse mit schönem interface wie oben drumherum - dann ist es aber kein POD mehr, was evtl. ungünstig ist.
-
@camper: Das Alignment ist bei allen guten Compilern einstellbar, somit sollte das keine 64 Bits benötigen, oder? Trotzdem wärs wohl sicherer die Klassenvariante zu benützen.
MfG SideWinder
-
camper schrieb:
gibt es eigentlich eine garantie, dass sizeof(u48) tatsächlich nur 6byte ist (auf den üblichen plattformen) ? immerhin ist es kein POD.
Ja, solange keine virtuellen Funktionen benutzt werden und der Compiler Padding verhindert, sollte es bzgl. der Grösse keine Probleme geben.
camper schrieb:
diesbzgl. ist mir sowieso nicht ganz klar, was gefordert ist:
a) der wertebereich des typs entspricht genau dem eines 48bit unsigned integers, also 0..2^^48-1
b) der typ beansprucht genau 48bit speicher und padding in arrays ist verboten.Also ich hab es so verstanden, dass b) gefordert war. Aber anstatt hier weiter zu raten, sollte Honolulu noch mal genau sagen, was er will. Mehrere Lösungsansätze hat er ja jetzt.
camper schrieb:
dann ist es aber kein POD mehr, was evtl. ungünstig ist.
Wie gesagt, ich sehe darin keine Probleme.
-
'sollte' war mir schon klar - die frage war nur, ob es irgendwelche garantien gibt.
bzgl. padding - alignment das kann man sicher einstellen, ich glaube mich aber dunkel zu erinnern, dass bitfelder immer den bereich des zugrundeliegenden typs benötigen - irgendwie muss der compiler nach all der bitschieberei und -maskiererei ja darauf zugreifen. ich lasse mich aber gern korrigieren
ein problem mit nicht-PODs ist, dass keine statische initilisierung stattfinden kann, das zumindest könnte unschön sein - wobei man ja problemlos auch zwei typen definieren könnte - eine als POD für solche zwecke und eine andere für das normale rechen.
-
camper schrieb:
'sollte' war mir schon klar - die frage war nur, ob es irgendwelche garantien gibt.
Was meinst du mit Garantien? So richtig versteh ich deine Bedenken nicht. Laut Standard, IIRC, werden Arrayelemente immer direkt hintereinander angeordnet, zB
unsigned short data[3];
sind 3 unsigned shorts und kein Bit mehr. Wenn dazu noch struct/class Padding durch den Compiler verhindert wird, wo soll also zusätzlicher Speicher verbraucht werden?
camper schrieb:
bzgl. padding - alignment das kann man sicher einstellen, ich glaube mich aber dunkel zu erinnern, dass bitfelder immer den bereich des zugrundeliegenden typs benötigen - irgendwie muss der compiler nach all der bitschieberei und -maskiererei ja darauf zugreifen. ich lasse mich aber gern korrigieren
Padding bezog sich im Kontext ja auch nicht auf Bitfelder.
camper schrieb:
ein problem mit nicht-PODs ist, dass keine statische initilisierung stattfinden kann, das zumindest könnte unschön sein - wobei man ja problemlos auch zwei typen definieren könnte - eine als POD für solche zwecke und eine andere für das normale rechen.
Mit statische initilisierung meinst du wahrscheinlich so eine initializer-list
uint48 a = {1, 2, 3};
Aber, da denkst du einfach zu umständlich. Ein uint48 (oder wie immer der Typen dann heissen mag) wird genauso gehandelt wie andere Ganzzahltypen auch. Du schreibst zB einfach
uint48 a = 10;
Wieviel und welche Member uint48 hat, hat dich überhaupt nicht zu interessieren.
-
mit statischer initialisierung meine ich statische intialisierung (die in form einer solchen initialisiererliste stattfindet, aber das ist unwesentlich) - welche vor der dynamischen initialisierung stattfindet. falls ich irgendwelche globalen objekte haben, die bei der konstruktion solche 48bit integer brauchen, kann ich sonst nicht sicher sein, dass diese intialisiert sind, wenn sie nicht in der selben ÜE und vor dem betreffenden objekt definiert sind. sonst müsste man noch tausende kniffe ala iostream anwenden - ein bisschen viel aufwand für den zweck.
Wenn dazu noch struct/class Padding durch den Compiler verhindert wird, wo soll also zusätzlicher Speicher verbraucht werden?
padding und alignment sind zwei verschiedene dinge - auch wenn ersteres i.d.r. letzterem dient. die frage ist, gibt es eine garantie, dass der compiler nicht noch irgendwelche versteckten felder benutzt für welchen zweck auch immer - einen gc zum beispiel. bereits das einstellen irgendwelchen alignments oder struct paddings ist ja genau genommen kein standard.
-
camper schrieb:
mit statischer initialisierung meine ich statische intialisierung (die in form einer solchen initialisiererliste stattfindet, aber das ist unwesentlich)
Ich hab dazu ja auch nur ein Beispiel konstruiert. Statische Intialisierung kann auch so aussehen
static foo bar;
Und foo muss hier kein POD sein.
camper schrieb:
falls ich irgendwelche globalen objekte haben, die bei der konstruktion solche 48bit integer brauchen, kann ich sonst nicht sicher sein, dass diese intialisiert sind, wenn sie nicht in der selben ÜE und vor dem betreffenden objekt definiert sind. sonst müsste man noch tausende kniffe ala iostream anwenden - ein bisschen viel aufwand für den zweck.
So richtig versteh ich hier nicht, was du uns damit sagen willst.
Um mal ein weiteres Besipiel zu bringenclass uint48 { //... }; struct unser_globaler_typ_der_bei_der_konstruktion_solche_48bit_integer_braucht { int a; uint48 b; long c; };
Und jetzt willst du davon ein globales Objekt erstellen, richtig?
unser_globaler_typ_der_bei_der_konstruktion_solche_48bit_integer_braucht x;
Wie du vielleicht weisst, werden Objekte mit static storage duration, was unser globales Objekt hat, nullinitialisiert. Willst du nun eine initializer-list benutzen, hast du natürlich ein Problem. Es gibt kein uint48 Literal.
Oder korrekt formuliert, ein non-aggregate (was unser uint48 ja ist) kann in so einer Liste nicht initialisiert werden. Man muss sich dann halt überlegen, ob man sowas braucht. Klar könnte man, wie du schon sagst, einen Typ als aggregate und einen als non-aggregate bereitstellen, so ähnlich wie bei DirectX
struct uint48plain { unsigned short value[3]; }; class uint48 : protected uint48plain { //... };
Halt ich aber für keine schöne Lösung, da somit Implementationsdetails offen gelegt werden. Bei uint48plain mag das ja noch gewollt sein, bei uint48 aber nicht. Zudem wäre das Handling von uint48plain etwas umständlich und nicht so intuitiv wie mit den elementaren Typen.
camper schrieb:
die frage ist, gibt es eine garantie, dass der compiler nicht noch irgendwelche versteckten felder benutzt für welchen zweck auch immer
Ja, die gibt es. Ansonsten würde ich meinen Compiler wegschmeissen. Das einzige Problem, was entstehen könnte, ist die Grösse der addressierbaren Einheiten. Wenn sich also 48 Bit nicht durch ein Vielfaches einer addressierbaren Einheit darstellen lässt. Da x86 kompatible Plattformen mit Vielfachen von 8 arbeiten, gibt es dort zB keine Probleme.
camper schrieb:
einen gc zum beispiel
Standard C++ hat keinen GC.
camper schrieb:
bereits das einstellen irgendwelchen alignments oder struct paddings ist ja genau genommen kein standard
Richtig, das wurde aber bereits erwähnt, das sich dies nur compiler-spezifisch realisieren lässt.
-
groovemaster schrieb:
Ich hab dazu ja auch nur ein Beispiel konstruiert. Statische Intialisierung kann auch so aussehen
static foo bar;
Und foo muss hier kein POD sein.
was soll das denn jetzt? das weisst du besser
das static keyword hat doch nichts mit der initialisierung zu schaffen (nur insofern als static storage voraussetzung für static initilisiation ist).
camper schrieb:
falls ich irgendwelche globalen objekte haben, die bei der konstruktion solche 48bit integer brauchen, kann ich sonst nicht sicher sein, dass diese intialisiert sind, wenn sie nicht in der selben ÜE und vor dem betreffenden objekt definiert sind. sonst müsste man noch tausende kniffe ala iostream anwenden - ein bisschen viel aufwand für den zweck.
So richtig versteh ich hier nicht, was du uns damit sagen willst.
...
... das meinte ich gar nicht, aber ist auch ein guter punkt. die aussage soll ja nur sein, dass eben weder eine POD variante noch eine mit schönem interface, aber eben als nichtPOD, nicht alle bedürfnisse befriedigen kann.
sinnfreies beispiel:
// a.h struct uint48 { /* schönes interface, kein POD ... */ }; extern const uint48 xyz; // a.cpp #include "a.h" const uint48 xyz = 10; // b.cpp #include "a.h" struct X { uint48 x; X() : x( xyz ) { } }; X x;
wird xyz vor oder nach x initialisiert? es ist zwar richtig, dass alle objekte mit static storage beim start zero-initialisiert werden, aber das ist für nicht aggregate völlig bedeutungslos. das objekt lebt dann erst, nachdem der konstruktor ausgeführt wurde.
camper schrieb:
die frage ist, gibt es eine garantie, dass der compiler nicht noch irgendwelche versteckten felder benutzt für welchen zweck auch immer
Ja, die gibt es.
und das steht wo?
camper schrieb:
einen gc zum beispiel
Standard C++ hat keinen GC.
der standard erlaubt ausdrücklich gc.
-
camper schrieb:
groovemaster schrieb:
Ich hab dazu ja auch nur ein Beispiel konstruiert. Statische Intialisierung kann auch so aussehen
static foo bar;
Und foo muss hier kein POD sein.
was soll das denn jetzt?
Das Statement war eher in Verbindung mit deiner Aussage bzgl.
camper schrieb:
ein problem mit nicht-PODs ist, dass keine statische initilisierung stattfinden kann
gedacht.
camper schrieb:
das weisst du besser
das static keyword hat doch nichts mit der initialisierung zu schaffen
Doch, das hat es. Aber wenn's dich stört, dann denk es dir einfach weg und static storage duration dazu. (und lass deine Klugscheisserei aussen vor
)
camper schrieb:
nur insofern als static storage voraussetzung für static initilisiation ist
Das ist zwar richtig, aber eher irrelevant, da man von statischer Initialisierung sowieso nur bei nicht-lokalen Objekten spricht. Viel wichtiger ist die Tatsache, dass es sich um statische Initialisierung bei Nullinitialisierung oder Initialisierung mit konstanten Werten handelt.
camper schrieb:
sinnfreies beispiel:
// a.h struct uint48 { /* schönes interface, kein POD ... */ }; extern const uint48 xyz; // a.cpp #include "a.h" const uint48 xyz = 10; // b.cpp #include "a.h" struct X { uint48 x; X() : x( xyz ) { } }; X x;
wird xyz vor oder nach x initialisiert?
Das ist aber nicht spezifisch für die eigentliche Problematik. Du sprichst hier ein generelles Problem beim Initialisieren von globalen Objekten an.
Auch wenn sich ein uint48 wie ein elementarer Typ benutzen lässt, so bleibt es doch ein non-POD mit all den verbundenen Konsequenzen. So wie es bei anderen Klassen auch der Fall ist. Das dürfte dem Op mittlerweile schon klar geworden sein.camper schrieb:
und das steht wo?
Nunja, ich hoffe, dass du noch nicht anfängst zu hyperventilieren.
Ansonsten bau dir ein static assert mit sizeof.
der standard erlaubt ausdrücklich gc.
Mag sein, diesbzgl. sind mir allerdings keine Quellen bekannt. Genauso wenig, dass die Implementation einfach so irgendwelche "unsichtbaren" Daten in die Klasse packen darf (bis auf Padding, um entsprechendes Alignment zu garantieren). Und wenn schon GC, dann werden benötigte Informationen mit Sicherheit an anderer Stelle gespeichert.
-
groovemaster schrieb:
camper schrieb:
groovemaster schrieb:
Ich hab dazu ja auch nur ein Beispiel konstruiert. Statische Intialisierung kann auch so aussehen
static foo bar;
Und foo muss hier kein POD sein.
was soll das denn jetzt?
Das Statement war eher in Verbindung mit deiner Aussage bzgl.
camper schrieb:
ein problem mit nicht-PODs ist, dass keine statische initilisierung stattfinden kann
gedacht.
camper schrieb:
das weisst du besser
das static keyword hat doch nichts mit der initialisierung zu schaffen
Doch, das hat es.
kapitel und vers?
Aber wenn's dich stört, dann denk es dir einfach weg und static storage duration dazu.
das wäre richtig.
camper schrieb:
nur insofern als static storage voraussetzung für static initilisiation ist
Das ist zwar richtig, aber eher irrelevant, da man von statischer Initialisierung sowieso nur bei nicht-lokalen Objekten spricht. Viel wichtiger ist die Tatsache, dass es sich um statische Initialisierung bei Nullinitialisierung oder Initialisierung mit konstanten Werten handelt.
kapitel 3.6.2 heisst zwar initialization of non-local objects, bezieht sich aber tatsächlich auf objekte mit static storage duration und das bezieht auch locale static objekte mit ein, (3.7. 1 vers 3 und 4). und weiter (6.7) wird jedes lokale objekt mit s.s.d. zero-initialisiert, bevor irgendeine andere init. stattfindet. und zero-init. (und init. mit konstanten ausdrücken) IST statische initialisierung (3.6.2). folgerung: statische initialisierung ist nicht auf nicht-lokale objekte beschränkt.
und ja, ich weiss, was statische initialisierung ist, das war nie die frage. und non-PODs werden eben NICHT statisch initialisiert; der speicher, den sie belegen wird zero-initialisiert, aber das ist wie gesagt, völlig bedeutungslos. auf ein nicht-POD, dessen konstruktor noch nicht ausgeführt wurde, zuzugreifen, ist undefiniertes verhalten.camper schrieb:
sinnfreies beispiel:
wird xyz vor oder nach x initialisiert?
Das ist aber nicht spezifisch für die eigentliche Problematik. Du sprichst hier ein generelles Problem beim Initialisieren von globalen Objekten an.
Auch wenn sich ein uint48 wie ein elementarer Typ benutzen lässt, so bleibt es doch ein non-POD mit all den verbundenen Konsequenzen. So wie es bei anderen Klassen auch der Fall ist. Das dürfte dem Op mittlerweile schon klar geworden sein.genau davon rede ich die ganze zeit. mit PODs hat man dieses problem nicht, sofern sie durch einen konstanten ausdruck initialisiert werden.
camper schrieb:
und das steht wo?
Nunja, ich hoffe, dass du noch nicht anfängst zu hyperventilieren.
Ansonsten bau dir ein static assert mit sizeof.
es war nur eine einfache frage. wenn es dich nervt, solltest du dir überlegen, ob du überhaupt antworten solltest. wenn es eben eine solche garantie gäbe, wäre das schlicht nützlich zu wissen.
Und wenn schon GC, dann werden benötigte Informationen mit Sicherheit an anderer Stelle gespeichert.
'mit sicherheit'.
-
camper schrieb:
kapitel und vers?
Nur mal als Beispiel
void foo() { int a; } void bar() { static int a; }
Beide Funktionen machen exakt das gleiche, bis auf den Unterschied static. Der sorgt dafür, dass aus automatischem statischer Speicher wird, und damit Auswirkungen auf die Initialisierung hat. Mehr dazu findest du zB unter 3.7.1.
camper schrieb:
kapitel 3.6.2 heisst zwar initialization of non-local objects, bezieht sich aber tatsächlich auf objekte mit static storage duration und das bezieht auch locale static objekte mit ein, (3.7. 1 vers 3 und 4). und weiter (6.7) wird jedes lokale objekt mit s.s.d. zero-initialisiert, bevor irgendeine andere init. stattfindet. und zero-init. (und init. mit konstanten ausdrücken) IST statische initialisierung (3.6.2). folgerung: statische initialisierung ist nicht auf nicht-lokale objekte beschränkt.
Nein, das hast du falsch verstanden. Mit nicht-lokal sprach ich nicht vom Scope, sondern vom Speicher selbst. Das betrifft ja nicht nur static Variablen in Funktionen, sondern auch static Member in Klassen.
camper schrieb:
und non-PODs werden eben NICHT statisch initialisiert; der speicher, den sie belegen wird zero-initialisiert, aber das ist wie gesagt, völlig bedeutungslos
Mag sein, dass zero-initialization bei Nicht-PODs nicht als statische Initialisierung bezeichnet wird. Auch wenn du mir noch keine Quellen diesbzgl. genannt hast. Letztendlich ist es nur eine relativ unbedeutende Begriffsdefinition, an die du dich klammerst. Wichtig ist nur, dass zero-initialisiert wird. Deshalb versteh ich immer noch nicht deinen Besserwisser-Einwand gegen die initializer-list, denn die ist das Einzige, was du bei einem Nicht-POD nicht verwenden kannst.
camper schrieb:
auf ein nicht-POD, dessen konstruktor noch nicht ausgeführt wurde, zuzugreifen, ist undefiniertes verhalten
Das ist richtig. Steht aber im Widerspruch wozu?
camper schrieb:
es war nur eine einfache frage.
Und eine einfache Antwort. Ich kenne zumindest keine Textstelle, die explizit erlaubt, irgendwelche zusätzlichen, dem Anwender nicht sichtbaren, Daten in eine Klasse zu packen. Ausnahmen hierbei sind virtuelle Basisklassen, virtuelle Funktionen und Padding (aufgrund Alignment). Deshalb bin ICH mir sicher, dass nur die Daten in die Klasse gepackt werden, die ich explizit angebe. Wenn DU dir nicht sicher bist, dann mach dir, wie bereits erwähnt, ein static assert.
'mit sicherheit'
So ist das. Siehe oben.
-
hier die vorläufige offizielle endversion meiner int48-klasse
int48.h
#ifndef INT48_H #define INT48_H class int48 { unsigned int lo32; // intel format signed short hi16; public: int48 () : lo32(0), hi16(0) {} int48 (__int64 x) : lo32(x), hi16(x>>32) {} operator __int64 () const { return (__int64)lo32 | ((__int64)hi16)<<32; } }; class uint48 { unsigned int lo32; // intel format unsigned short hi16; public: uint48 () : lo32(0), hi16(0) {} uint48 (unsigned __int64 x) : lo32(x), hi16(x>>32) {} operator unsigned __int64 () const { return (unsigned __int64)lo32 | ((unsigned __int64)hi16)<<32; } }; #endif // INT48_H
kompakter gehts nicht mehr.
hier noch der iostream-ausgabe-operator (keine ahnung ob der so jetzt standard-konform ist):
iostream_int64.h
#ifndef IOSTREAM_INT64_H #define IOSTREAM_INT64_H #include<iostream> using namespace std; ostream &operator<< (ostream &os, unsigned __int64 x); ostream &operator<< (ostream &os, __int64 x); #endif // IOSTREAM_INT64_H
iostream_int64.cpp
#include<iostream> #include<cstring> #include "iostream_int64.h" using namespace std; static char *u64toa (unsigned __int64 x, int b, bool uc) { static char buf[21]; char *cp; cp=buf+22; *--cp='\0'; do { *--cp=(uc ? "0123456789ABCDEF" : "0123456789abcdef")[x%b]; x=x/b; } while(x); return cp; } static void write_uint64 (ostream &os, unsigned __int64 x, bool sign) { int base,w; char *sb,*cp; ios::fmtflags f=os.flags(); if((f & (ios::left|ios::right|ios::internal))==0) f|=ios::left; if(f & ios::oct) { base= 8; sb="0"; } else if(f & ios::hex) { base=16; sb="0x"; } else { base=10; sb=""; } cp=u64toa(x,base,(f & ios::uppercase)!=0); w=os.width()-strlen(cp)-strlen(sb); if(sign || (f & ios::showpos)) --w; if(w>0 && (f & ios::left)) while(w--) os.put(os.fill()); if(sign || (f & ios::showpos)) os.put(sign ? '-' : '+'); if(w>0 && (f & ios::internal)) while(w--) os.put(os.fill()); while(*sb) os.put(static_cast<unsigned char>(*sb++)); while(*cp) os.put(static_cast<unsigned char>(*cp++)); if(w>0 && (f & ios::right)) while(w--) os.put(os.fill()); os.width(0); } ostream &operator<< (ostream &os, unsigned __int64 x) { write_uint64(os,x,false); return os; } ostream &operator<< (ostream &os, __int64 x) { write_uint64(os,(x<0 ? -x : x),x<0); return os; }
funktioniert mit VC++6 einwandfrei. für gcc muß man __int64 natürlich durch long long ersetzen.
euere diskussion zum thema kompatibilität halte ich für überflüssig. zumindest der groovemaster-ansatz, über 64-bit-integers zu arbeiten, ist sowieso nicht portabel, da 64-bit-integers nicht standardisiert sind.
es fehlen allerdings immer noch die iostream-eingabe-operatoren für 64-bit-integers. falls sie nicht schon in der standardbibliothek des compilers vorhanden sind. bei VC++6 sind sie es jedenfalls nicht.
so und jetzt könnt ihr meinen code nach allen regeln der kunst verreißen
-
kompatibilität ist nie gänzlich überflüssig. und bis zu diesem zeitpunkt bist du ja auf die zielplattform nicht eingegangen (oder hab ich das übersehen?).
eine kleine anmerkung bzgl. int48:int48 (__int64 x) : lo32(x), hi16(x>>32) {}
das macht möglicherweise nicht das, was du willst, falls du mit überläufen zu tun hast (keine modulo 2^n arithmetik). auf x86 prozessoren ist modulo immer vorzeichen-bewahrend und bietet sich daher an:
int48 (__int64 x) : lo32(x), hi16((x%(1LL<<47))>>32) {}
-
natürlich ist kompatibilität nicht überflüssig. aber erstens ist sie, wenn 64-bit-integers verwendet werden, sowieso nicht gegeben, und zweitens ist die int48-klasse so klein, daß man sich mit der anpassung auch keinen zacken aus der krone bricht.
das macht möglicherweise nicht das, was du willst
es macht sogar genau das, was ich will. und das sogar platform-unabhängig. die höchstwertigen 16 bit werden abgeschnitten. das vorzeichen ist nur beim erweitern aber nicht beim abschneiden ein problem. aber deswegen ist hi16 in der int48-klasse auch signed short und nicht unsigned short. damit klappt dann bei operator __int64 auch die vorzeichenbehaftete erweiterung.
-
stimmt, denkfehler von mir.
-
kein problem
im übrigen ist es ja auch nur eine stilfrage. ich benutze gerne casts während du lieber mit mathematischen operationen arbeitest, die heutzutage jeder halbwegs brauchbare compiler in genauso effizienten code übersetzt.
-
Hmmm....
Mal ne frage.
Wenn die Ganzzahl zu gross ist fuer long int, wieso nicht einfach long long int nehmen? Und sich die sach mit 48bits sparen?static
In meinem Buch steht geschrieben, man sollte static vermeiden,
wenn man mit namespace arbeiten kann.Ghost
-
Green_Ghost schrieb:
static
In meinem Buch steht geschrieben, man sollte static vermeiden,
wenn man mit namespace arbeiten kann.Ghost
static hat zwei bedeutungen:
in funktionen und klassen bewirkt static, dass nur eine einzige variable existiert - und nicht wie im normalfall eins für jedes objekt, bzw. für jeden neuen funktionsaufruf. in diesem falle ändert sich nichts an der sichtbarkeit für den linker, statische klassenmember haben die selbe sichtbarkeit wie die klasse selbst, statische variablen in funktionen sind nach aussen nicht sichtbar (ausser im falle von inline funktionen - vague oder weak linkage).in verbindung mit globalen objekten oder funktionen bewirkt static dagegen, dass die betreffende instanz für den linker unsichtbar ist (internal linkage) und damit in jeder ÜE neu definiert werden darf - sie existiert dann aber eben auch mehrfach. und diese zweite form sollte nicht mehr verwendet werden - statt dessen gibt es unbenannte namespaces.