Protokoll: Pakete mit automatischer Serialisierung



  • @Sone
    Er will über das Makro ja nicht nur die struct definieren, sondern auch eine dazupassende serialize/deserialize Funktion.
    Und dazu musst du die Member-Liste nochmal wiederholen, aber halt nur die Namen.

    Oder eine Möglichkeit finden den Namen zu extrahieren.

    Oder Typ und Name im Makro-Aufruf trennen, so wie in meinem Vorschlag.

    EDIT: OK, hat sich erledigt während ich gelesen/getippt habe 😉



  • TyRoXx schrieb:

    struct simple_struct
    {
    	RXN_REFLECT((SZN_AUTO_MEMBERS) (SZN_ITERATE),
    		(a, be16) (int), //C++-Typ kann angegeben werden, ansonsten wird einer automatisch gewählt
    		(b, be32),
    		(c, bytes<be8>),
    		(d, bytes<be8>) (std::string),
    		(v, vector<be8, be16>)
    	)
    };
    

    Bekommst du bei (v, vector<be8, be16>) kein Problem mit dem Komma zwischen be8 und be16 ? Falls nicht, wie löst man das?

    Und verträgt sich diese Lösung mit MSVC?
    EDIT: Huch! Ich sehe gerade MSVC kann ab 2005 variadic Makros. Wusste ich gar nicht. Damit erübrigt sich die Frage wohl 🙂

    EDIT 2: Mann, so viel sinnvoll gemeinte aber bei näherer Betrachtung dann doch sinnlose Beiträge wie hier schreib ich sonst nicht. Hoffe ich zumindest. Erst denken, dann posten. Man vergebe mir bitte.



  • Wow, danke fuer die vielen Antworten! 😋

    knivil schrieb:

    Aber normalerweise ist es so, dass bei einem benutzerdefiniertem Typ auch die Serialisierungsroutine benutzerdefiniert ist. Mit Reflektions waere es einfacher.

    Fuer benutzerdefinierte Typen muss man das auch. Pakete sind aber nunmal fest vorgegeben und es gib zig verschiedene, dafuer will ich keine Serialisierungsroutinen schreiben muessen.

    knivil schrieb:

    Der naechste Punkt: Warum sollen die Attributtypen nach aussen hin sichtbar sein. Ich kann Pakete mit unterschiedlichem Inhalt in keinen Container packen. Deinem Paket zu hause siehst du auch nicht an, was drin ist. Die Kennzeichnung uebernimmt dene ID.

    Das brauche ich nicht. Sobald das Paket hereinkommt, kenne ich den Typen und kann es direkt bearbeiten. Die Paketklassen dienen einfach nur als Daten-structs.

    @hustbaer: Den Ansatz mit 1 File pro Paket hatte ich schon, finde ich nicht gut. Fuer jedes Paket ein File includen zu muessen ist der reinste Horror.
    Es handelt sich um das Protokoll von Minecraft, die Pakete werden sich also stetig aendern. Wenn moeglich, moechte ich bei C++ bleiben.

    Sone schrieb:

    Das Paket soll einfach diese Member haben und gleichzeitig will ich irgendeine Form von Typinformation haben, mit der ich serialisieren kann, z.B. eine Liste an Member-Pointern. Leider habe ich keine Ahnung, ob und wie ich das mit Makros hinbekommen koennte.

    Ich glaube, ich missverstehe dich... aber trifft das hier es nicht:

    ...
    

    Nein, das trifft es eigentlich sehr gut. Nur brauche ich eben noch Serialisierung dazu.

    Sone schrieb:

    eine Liste an Member-Pointern

    Was genau soll den die Liste so halten? void -Pointer? Mit Type-Erasure ginge was.

    Na Member-Pointer, also die Dinger mit ::*. Ich haette mir vorgestellt, jedem Paket eine statische Liste an Member-Pointern zu verpassen.

    Sone schrieb:

    dass man einerseits automatische Serialisierung bekommt, andererseits moechte ich auf die Attribute per Name zugreifen koennen.

    Jetzt verstehe ich, warum der Name schwierigkeiten macht.... 💡 💡
    Und worüber camper und hustbaer reden.
    Ist eine Alternative Syntax a la

    DEFINE_PACKET(TwoWayTag, Foo, 0x20, ((int, x), (double, d), (std::string, s)) );
    

    Verkraftbar? Das ginge dann. Sonst, nur mit Leerzeichen dazwischen, wird's schwer.

    Wenn nicht anders geht. Schoener waere es natuerlich wie von mir gezeigt. Ich dachte, du liebst Challenges 😉

    @TyRoXx: Sieht interessant aus, danke! Wenn ich etwas mehr Zeit habe, werde ich mir deine Lib genauer ansehen.
    Insebesondere auf einen Visitor bin ich noch gar nicht gekommen.



  • Ich dachte, du liebst Challenges 😉

    Das tue ich! Ich habe schon viel darüber nachgedacht. Die beste Variante wäre, einen durch ein Leerzeichen getrennten Makro-Parameter P Irgendwie zu konkatenieren:

    #define GetSecond(N) ... ## N ## ...
    
    GetSecond(int a) // wird zu a
    

    Nur ist die entscheidende Frage wie...


  • Mod

    EIgentlich brauchen wir den Namen gar nicht.
    Wenn das Ergebnis so aussehen kann
    DEFINE_PACKET((int x, double y, std::string s)) =>

    template <typename T> struct fun_arg_;
    template <typename T, typename A> struct fun_arg_<T(A)> {
        using type = A;
    };
    template <typename T>
    using  fun_arg = typename fun_arg<T>::type;
    
    struct PacketXYZ
    {
        union {
            int x;
            std::remove_pointer<fun_arg<decltype(void(int x[1]))>>::type _1;
        };
        union {
            double y;
            std::remove_pointer<fun_arg<decltype(void(double y[1]))>>::type _2;
        };
        union {
            std::string s;
            std::remove_pointer<fun_arg<decltype(void(std::string s[1]))>>::type _3;
        };
    };
    

    Die Serialisierungsfunktion kann dann _1, _2 usw. verwenden.
    Das Makroschreiben überlasse ich anderen. Funktioniert nicht mit Arrays.



  • @TyRoXx: MessagePack
    @Kellerautomat: proto buffer wenn Pakete auch als Typ vorliegen, ansonsten MessagePack.

    Es sieht so in etwa in Code aus:

    packer p( Byte blob als paket)
    p.pack(irgend ne id).pack(123.f).pack("jeha").pack(128). ....
    

    Kann gerne mit Streamoperatoren verfeinert werden. Dazu bedarf es auch keines extra Typs um Pakete zu spezifizieren, sondern nur, wie sie gepackt und entpackt werden, d.h. welche Member und in welcher Reihenfolge.

    Ich bin meist nie fuer Selbstbauen wenn die Bibliothek alzuviel mehr anbietet. Da man Spezifikation und Dokumentation auch machen muss. Obengenannte waren Kandidaten fuer Serialisierung von RPC-Aufrufen mittels ZeroMQ. Habe mich fuer MessagePack mit eigener Implementation entschieden (keine externen Tools, kein Framework).



  • knivil schrieb:

    @TyRoXx: MessagePack

    Damit hat man wieder die Duplizierung beim Lesen und Schreiben. Meine Strukturen sollen unter anderem genau das verhindern. Außerdem wird Platz verschwendet, weil die Datentypen mit gespeichert werden.
    Man könnte meine Bibliothek allerdings auch so umbauen, dass alternative Backends möglich sind, zum Beispiel MessagePack. Oder JSON. Oder ganz andere Einsatzgebiete wie SQL.



  • Kellerautomat schrieb:

    @hustbaer: Den Ansatz mit 1 File pro Paket hatte ich schon, finde ich nicht gut. Fuer jedes Paket ein File includen zu muessen ist der reinste Horror.

    Du kannst ja trotzdem viele structs in ein File packen.
    Nur musst du das Ding irgendwo mehrfach includen. Lässt sich aber auch relativ elegant lösen:

    // Packets1Def.h:
    
    BEGIN_PACKET(SomePacket)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
    END_PACKET()
    
    BEGIN_PACKET(OtherPacket)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
    END_PACKET()
    
    BEGIN_PACKET(YetAnotherPacket)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
    END_PACKET()
    
    // ...
    
    // Packets2Def.h:
    
    // s.o.
    
    // IncludePacketDefinition.h:
    
    #define BEGIN_PACKET ... Passend zum Erstellen der Klassendeklaration ...
    #define PACKET_MEMBER ... Passend zum Erstellen der Klassendeklaration ...
    #define END_PACKET ... Passend zum Erstellen der Klassendeklaration ...
    
    #include PACKET_DEFINITION
    
    #undef BEGIN_PACKET
    #undef PACKET_MEMBER
    #undef END_PACKET
    #define BEGIN_PACKET ... Passend zum Erstellen der Serialisierungs-Funktion ...
    #define PACKET_MEMBER ... Passend zum Erstellen der Serialisierungs-Funktion ...
    #define END_PACKET ... Passend zum Erstellen der Serialisierungs-Funktion ...
    
    #include PACKET_DEFINITION
    
    #undef BEGIN_PACKET
    #undef PACKET_MEMBER
    #undef END_PACKET
    #define BEGIN_PACKET ... Passend zum Erstellen der Deserialisierungs-Funktion ...
    #define PACKET_MEMBER ... Passend zum Erstellen der Deserialisierungs-Funktion ...
    #define END_PACKET ... Passend zum Erstellen der Deserialisierungs-Funktion ...
    
    #include PACKET_DEFINITION
    
    #undef BEGIN_PACKET
    #undef PACKET_MEMBER
    #undef END_PACKET
    
    #undef PACKET_DEFINITION
    
    // Packets.h:
    
    #pragma once
    
    #define PACKET_DEFINITION "Packets1Def.h"
    #include "IncludePacketDefinition.h"
    
    #define PACKET_DEFINITION "Packets2Def.h"
    #include "IncludePacketDefinition.h"
    
    // Foo.cpp:
    
    #include "Packets.h"
    

    So in der Richtung.

    Und falls in den Packet-Definition-Files noch andere Dinge stehen sollen die nicht wiederholt werden können, dann lässt sich das ja relativ einfach über ein weiteres #define lösen:

    // Packets1Def.h:
    
    #ifdef NORMAL_STUFF // nur im 1. Durchgang definiert
    struct SomeNormalStruct {...};
    #endif
    
    BEGIN_PACKET(SomePacket)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
        PACKET_MEMBER(...)
    END_PACKET()
    
    //...
    


  • Nun, es gibt Vor- und Nachteile fuer alle Varianten. Einige Kritikpunkte, die mich als Entwickler davon abhalten wuerde es zu benutzen als auch es zu entwickeln. Reflektions bzw. deren Nachbau ist in deiner Variante sehr intrusiv. Durch das Makro ist nicht nur Serialisierung sondern auch die ganze Klassendefinition betroffen. Auch scheint das Framework sehr umfangreich zu sein, eine Entwicklerdoku fuer interne Zwecke als auch die Qualitaetssicherung/Tests scheint sehr aufwendig zu werden.

    Aus deinem Beispiel sehe ich kaum Vorteile bei der Benutzung, ich habe Format, Senke (was bei mir der Byteblob ist) und den Wert. Eine kurze Gegenueberstellung:

    szn::be32().serialize(sink2, s);
    fprintf(fd, "%d", s)
    

    Beide Zeilen enthalten die genannten Elemente. Nun kann wenig weggelassen werden. Ich wuerde am Format sparen und die Umwandlung der Endianess oder Strings mit UTF-Codierung einer Policy ueberlassen wenn noetig.

    Außerdem wird Platz verschwendet, weil die Datentypen mit gespeichert werden.

    Aus gegebenen Anlass habe ich viel drueber nachgedacht. Natuerlich wird Platz verschwendet. Die "Reflektion" steckt in der Serialisierung und nicht in der Klasse, dafuer auf Primitive beschraenkt. Weiterhin ist die Serialisierung wertorientiert, in deinem Beispiel werden immer 4 Byte fuer die Codierung benutzt, auch wenn es sich um kleine Zahlen handelt. Kommt halt auf Narichtengroesse und Einsatzzweck an. Ich habe mich fuer groessere Nachrichten entschieden. Komprimierung mit lz4 hat sich nicht ausgezahlt.

    Damit hat man wieder die Duplizierung beim Lesen und Schreiben.

    Ich weiss nicht was du damit meinst, aber ich habe deine Implementation nur ueberflogen.



  • @hustbaer: Den Ansatz finde ich super. Man muss zwar bei den Definitionen ein wenig mehr tippen, dafuer ist alles um einiges flexibler. 😋 👍



  • knivil schrieb:

    Aus deinem Beispiel sehe ich kaum Vorteile bei der Benutzung, ich habe Format, Senke (was bei mir der Byteblob ist) und den Wert. Eine kurze Gegenueberstellung:

    szn::be32().serialize(sink2, s);
    fprintf(fd, "%d", s)
    

    Beide Zeilen enthalten die genannten Elemente. Nun kann wenig weggelassen werden. Ich wuerde am Format sparen und die Umwandlung der Endianess oder Strings mit UTF-Codierung einer Policy ueberlassen wenn noetig.

    Hast du dazu mal ein Beispiel? Um das Format geht es doch gerade. Das kann man nicht weglassen.

    knivil schrieb:

    Weiterhin ist die Serialisierung wertorientiert, in deinem Beispiel werden immer 4 Byte fuer die Codierung benutzt, auch wenn es sich um kleine Zahlen handelt.

    Dass ein 4-Byte-Integer 4 Byte lang ist, ist by design so. Selbstverständlich kann man ganz leicht komprimierende Formate hinzufügen. Meine Bibliothek ist für benutzerdefinierte Formate offen.

    knivil schrieb:

    Damit hat man wieder die Duplizierung beim Lesen und Schreiben.

    Ich weiss nicht was du damit meinst, aber ich habe deine Implementation nur ueberflogen.

    Es ist redundant, wenn sowohl beim Lesen als auch beim Schreiben Reihenfolge und Typen vom Benutzer einhalten werden müssen:

    int main(void) {
        // This is target object.
        std::vector<std::string> target;
        target.push_back("Hello,");
        target.push_back("World!");
    
        // Serialize it.
        msgpack::sbuffer sbuf;  // simple buffer
        msgpack::pack(&sbuf, target);
    
        // Deserialize the serialized data.
        msgpack::unpacked msg;    // includes memory pool and deserialized object
        msgpack::unpack(&msg, sbuf.data(), sbuf.size());
        msgpack::object obj = msg.get();
    
        // Print the deserialized object to stdout.
        std::cout << obj << std::endl;    // ["Hello," "World!"]
    
        // Convert the deserialized object to staticaly typed object.
        std::vector<std::string> result;
        obj.convert(&result);
    
        // If the type is mismatched, it throws msgpack::type_error.
        obj.as<int>();  // type is mismatched, msgpack::type_error is thrown
    }
    

    Ein Typfehler kann zur Laufzeit noch festgestellt werden. Ein einfaches Verwechseln zweier Typ-gleicher Elemente kann MessagePack nicht erkennen.
    Mit meiner Bibliothek muss man die Struktur nur an einer Stelle definieren. Außerdem kann man sofort mit C++- struct s arbeiten und kommt nicht mit Zwischenformen oder -schritten in Berührung. Fehler werden oft schon vom Compiler erkannt. Die schlanke Notation ist auch leicht lesbar.
    Wer will, kann sich das noch weiter verkürzen.

    MESSAGE (example,
    	(a, be16) (int),
    	(b, be32),
    	(c, bytes<be8>),
    	(d, bytes<be8>) (std::string),
    	(v, vector<be8, be16>)
    )
    

    EDIT: MessagePack benutzt sich in D anscheinend wie ich mir das vorstelle. Die Sprache hat wohl brauchbare Reflection.



  • Hast du dazu mal ein Beispiel? Um das Format geht es doch gerade. Das kann man nicht weglassen.

    Okay, zu be32: Format ist Big endian 32 bit. Der Part fuer 32 Bit wird schon durch bspw. uint32_t uebernommen, d.h. eine passende Ueberladung/Template reicht aus. Falls 32 Bit Integer in 64 Bit serialisiert werden soll, kann entprechend vom Nutzer gecastet werden, um die richtige Ueberladung auszuwaehlen. Big endian wird wahrscheinlich innerhalb eines Pakets sich wohl nicht aendern und kann anderweitig gesetzt werden. Deswegen schlage ich eine Klasse Packer vor, die wie folgt benutzt werden kann:

    Packer p(sink, policy1, policy2, ...); // gern auch als template parameter
    uint32_t value1;
    float value2;
    
    p.pack(value1); // ruft Packroutine fuer uint32_t mit Byte order policy
    p.pack(value2); // ruft das Ueberladung fuer float auf
    

    Im Vergleich dazu

    szn::be32().serialize(sink2, s);
    

    hat s einen Typ, der nochmals durch be32 wiederholt wird. D.h. 32 kann weggelassen werden. Nun werde ich die endianess nur selten im Paket selbst als auch zwischen Paketen aendern. Es ist Muehsam es explizit hinzuschreiben. Deswegen wuerde ich "ausklammern" (k.A. finde kein besseres Wort fuer eng. to factor out).

    Wie das mit deiner Bibliothek in Einklang zu bringen ist, weiss ich nicht. Es sind nur einige Anregungen.

    Ein einfaches Verwechseln zweier Typ-gleicher Elemente kann MessagePack nicht erkennen.

    Ich benutze MessagePack nicht als Bibliothek, sondern nur als Formatspezifikation. Weiterhin habe ich keine Erfahrung in D.



  • Die Serialisierungsfunktion kann dann _1, _2 usw. verwenden.

    Ja, da hat camper einen Volltreffer erzielt...** unions **. Bastard. Mir fällt nie was tolles ein, stattdessen mache ich die Drecksarbeit 😞

    Funktioniert nicht mit Arrays.

    Funktioniert auch nicht ohne weiteres mit UDTs. 😉

    Hie ist die Lösung:

    template <typename T> struct fun_arg_;
    template <typename T, typename A> struct fun_arg_<T(A)> {
        using type = A;
    };
    template <typename T>
    using fun_arg = typename fun_arg_<T>::type;
    
    #include <type_traits>
    
    template<typename T>
    using noref = typename std::remove_reference<T>::type;
    
    template< typename T,
              typename = typename std::enable_if<std::is_class<noref<T>>::value>::type >
    void destroy( T&& t )
    {
    	t.~noref<T>();
    }
    
    template< typename T,
              typename = typename std::enable_if<not std::is_class<typename std::remove_reference<T>::type>::value>::type,
              typename=void >
    void destroy( T&& ) {}
    
    /// __________________________________________________________________________
    
    #include <cstdint>
    
    template< typename DirectionTag,
              uint8_t Id>
    struct Packet
    {
        static constexpr auto ID = Id;
        using Tag = DirectionTag;
    };
    
    struct ClientTag;
    struct ServerTag;
    struct TwoWayTag;
    
    #include <boost/preprocessor/tuple/size.hpp>
    #include <boost/preprocessor/tuple/elem.hpp>
    #include <boost/preprocessor/repetition/repeat.hpp>
    #include <boost/preprocessor/repetition/enum.hpp>
    #include <boost/preprocessor/variadic/elem.hpp>
    
    #define GET_NTH_TYPE( n, tuple ) \
    	fun_arg<void( BOOST_PP_TUPLE_ELEM( BOOST_PP_TUPLE_SIZE(tuple), n, tuple ) )>
    
    #define DECLARE_ELEMS( z, n, t ) \
    	GET_NTH_TYPE(n, t) _##n
    
    #define DEFINE_UNION( z, number, declarations ) \
        union \
        { \
            BOOST_PP_TUPLE_ELEM( BOOST_PP_TUPLE_SIZE(declarations), number, declarations ) ; \
            DECLARE_ELEMS(, number, declarations) ; \
            static_assert( not std::is_array< GET_NTH_TYPE(number, declarations) >::value, "Array type not supported!" );  \
        };
    
    #define DUMP_FUNCTION( z, i, t ) std::cout << _##i << '\n';
    
    #define CTOR_INITIALIZER_LIST( z, n, t ) \
        _##n( _##n )
    
    #define DESTROY_ALL( z, i, t ) \
    	destroy( _##i );
    
    #define DEFINE_PACKET( name, dir, id, tuple ) \
    struct name##Packet : Packet<dir, id> \
    { \
    	BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DEFINE_UNION, tuple ) \
    \
    	name##Packet ( BOOST_PP_ENUM(BOOST_PP_TUPLE_SIZE(tuple), DECLARE_ELEMS, tuple) ) : \
    		BOOST_PP_ENUM( BOOST_PP_TUPLE_SIZE(tuple), CTOR_INITIALIZER_LIST, ) \
    	{} \
    \
    	~ name##Packet() \
    	{ \
    		BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DESTROY_ALL, ) \
    	} \
    \
    	void dump() \
    	{ \
    		BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DUMP_FUNCTION, ) \
    	} \
    };
    
    #include <string>
    #include <iostream>
    
    struct A
    {
    	A( int )
    	{
    		std::cout << "A: constructor\n";
    	}
    
    	A( A const& )
    	{
    		std::cout << "A: copy constructor\n";
    	}
    
    	~A()
    	{
    		std::cout << "A: destructor\n";
    	}
    
    	friend std::ostream& operator<<( std::ostream& os, A const& )
    	{
    		return os;
    	}
    };
    
    DEFINE_PACKET( Foo, TwoWayTag, 0x20,
    			   ( int x,
    				 std::string s,
    				 double d,
    				 A a ) )
    
    int main()
    {
    	FooPacket p{4, "Hi", 54.87, 4};
    	p.dump();
    
    	static_assert( std::is_same<decltype(p._1), std::string>::value, "" );
    }
    

    Noch sehr unschön, funktioniert an sich aber und hat AFAICS wohldefiniertes Verhalten.

    ~Edit: Nein, hat schwerwiegende Bugs. Wird gefixt.~
    Edit²: Bug gefixt.

    Arrays funktionieren immer noch nicht... ein weiterer Faktor, der einem die Wahl von std::array erleichtert 🙂

    Eine dummy-Serialisierungs-Funktion ist vorimplementiert. Wobei ich nicht genau weiß, was "Serialisierung" hier eigentlich heißt, einfach alles in einen Stream schreiben?



  • Erklaer mal lieber, wie das ganze ueberhaupt funktioniert.


  • Mod

    Sone schrieb:

    Arrays funktionieren immer noch nicht...

    Dafür gibt es wahrscheinlich auch keine allgemeingültige Lösung. Ursprünglich hatte ich das [1] angefügt, am den Decay zu vermeiden, dann ist mir aufgefallen, dass das ja am falschen Ende angehängt würde. Probleme mit UDTs sehe ich nicht, sofern diese nicht inplace-definiert werden sollen. fun_arg funktioniert nicht mit abstrakten Klassen, aber die können bei dieser Anwendung sowieso nicht auftreten.



  • Kellerautomat schrieb:

    Erklaer mal lieber, wie das ganze ueberhaupt funktioniert.

    Das ist doch total einfach! Was gibt es da zu erklären?

    Was soll's:

    Das Grundprinzip hinter diese Variante ist die, dass man innerhalb der Paketklasse für jeden Member eine anonyme union mit zwei Membern definiert:

    union
    {
        int i; // Der eigentliche, durch das Makro vorgegebene Member.
    
        fun_arg< void(int i) > _1; // Der zweite Member, dessen Namen 'wir' wissen
    };
    

    Da beide Typen exakt gleich sind, können wir problemlos auf _1 oder i zugreifen. Das funktioniert für alle Typen, auch non-PODs, usw.
    Das geniale hier ist zudem, dass wir den Namen mit schreiben können, da in der Parameterliste eines Funktionstyp Namen zugelassen sind. Der Nachteil ist, dass Arrays nicht erlaubt sind (siehe auch das gerade hinzugefügte static_assert ).

    Die dump-Funktion nutzt nun _1, _2, und so weiter, während du einfach entweder die von dir festgelegten Identifier nimmst, oder auch _x.

    Das kann sogar soweit getrieben werden, dass du sowas schreiben kannst:

    DEFINE_PACKET( Foo, TwoWayTag, 0x20,
    			   ( int x,
    				 std::string s,
    				 double d ) )
    ( std::ostream& os )
    {
    	os << x << ' ' << s.size() << ' ' << s << ' ' << d; 
    }
    

    Und jetzt sag mir, das ist nicht wunderschön.



  • Probleme mit UDTs sehe ich nicht, sofern diese nicht inplace-definiert werden sollen

    Dann guck mal, was ich in meinen Destruktor schreiben musste. (UDTs gehen nicht ohne weiteres)
    Und die anderen speziellen Memberfunktionen funktionieren auch nicht ohne weiteres; siehe Standard:

    §9.5/3 schrieb:

    [ Example: Consider the following union:

    union U {
        int i;
        float f;
        std::string s;
    };
    

    Since std::string (21.3) declares non-trivial versions of all of the special member functions, U will have an implicitly deleted default constructor, copy/move constructor, copy/move assignment operator, and destructor. To use U, some or all of these member functions must be user-provided.—end example ]


  • Mod

    Sone schrieb:

    (UDTs gehen nicht ohne weiteres)

    Das hatte ich überlesen. UDT is ja auch ein bisschen unpräzise.


  • Mod

    Ich sehe nicht, wie

    static_assert( not std::is_array< GET_NTH_TYPE(number, declarations) >::value, "Array type not supported!" );
    

    jemals erfüllt sein könnte.

    Sofern für ein array ein typedef existiert und verwendet wird, kann man das Ganze zum funktionieren bringen:

    typedef int foo[42];
    // als Makroargument: foo x;
    
    // ==>
    using type1 = fun_arg<void(foo x)>;
    union dummy { foo x, _0; };
    // wenn
    std::is_same<type1, std::decay<decltype(dummy::_0)>::type>::value
    // so enthält die Deklaration von x keine weiteren Deklaratoren und decltype(dummy::_0) ist der gesuchte Typ
    // andernfalls verwenden wir type1
    // können aber nicht zwischen
    // int x[42] und int* x unterscheiden
    

    sofern man ein bisschen unportabel wird, könnte man noch Arrays zulassen, deren Größe nicht mit der Größe eines Zeigers übereinstimmt, dann ist die gesuchte Dimension sizeof(dummy) / sizeof(dummy::_0)

    statt

    DEFINE_PACKET( Foo, TwoWayTag, 0x20,
                   ( int x,
                     std::string s[42],
                     double d ) )
    

    schreibt man dann eben

    template <typename T> using simple_typeid = T;
    
    DEFINE_PACKET( Foo, TwoWayTag, 0x20,
                   ( int x,
                     simple_typeid<std::string[42]> s,
                     double d ) )
    

    wobei das nicht mehr ganz so schön ist.



  • Ich sehe nicht, wie [...] jemals erfüllt sein könnte.

    Ich schon. In dem der entsprechende Typ kein Array ist. 😕 Schon wieder irgend etwas überlesen?

    UDT is ja auch ein bisschen unpräzise.

    Was soll ich schon schreiben, nicht-triviale Klassen? :p

    wobei das nicht mehr ganz so schön ist.

    Doch, durchaus verkraftbar. Eine simple Lösung. Leider natürlich nicht so perfekt wie die ideale, aber anwendbar und relativ kurz.


Anmelden zum Antworten