char, short, int, long to hex so richtig?



  • Hallo,

    ich habe mir jetzt mal ein template gebastelt welches einen integer type in hex zurück gibt. Ich wollte nun fragen, ob das so in Ordnung ist, oder ob es Verbesserungsvorschläge gibt.

    template<typename T1>
    const std::string int2hex(const T1 value, bool separate = true, char delimiter = ' ')
    {
    	char size = sizeof(value);
    	std::ostringstream ss;
    
    	for (char i = size * 2 - 1; i >=0; i--)
    	{
    		ss << std::hex << std::uppercase << (value >> 4 * i & 0xF);
    
    		if (separate && i > 0 && i % 2 == 0)
    		{
    			ss << delimiter;
    		}
    	}
    
    	return ss.str();
    }
    
    int main()
    {
    	int a = 4857845;
    	std::cout << int2hex(a, true, '-') << std::endl;	// 00-4A-1F-F5
    
    	short b = 38576;
    	std::cout << int2hex(b, true, ' ') << std::endl;	// 96 B0
    
    	char c = 128;
    	std::cout << int2hex(c, true, ' ') << std::endl;	// 80
    
    	long d = 3394894;
    	std::cout << int2hex(d) << std::endl; 				// 00 33 CD 4E
    
    	int f = 8903485;
    	std::cout << int2hex(f, false) << std::endl; 		// 0087DB3D
    	return 0;
    }
    

    EDIT: Beim einbinden in eine Klasse gibt es leichte Probleme, kann mir jemand sagen warum?

    // header
    class CClass
    {
    public:
    template<typename T1> const tstring int2hex(const T1 value, bool separate = true, char delimiter = ' ');
    }
    
    // cpp
    template<typename T1>
    const tstring CClass::int2hex(T1 value, bool separate = true, char delimiter = ' ')
    {
    }
    
    // CClass.cpp(1339): error C2572: 'CClass::int2hex': Neudefinition des Standardparameters: Parameter 3 CClass.h(217): Siehe Deklaration von 'CClass::int2hex'
    // CClass.cpp(1339): error C2572: 'CClass::int2hex': Neudefinition des Standardparameters: Parameter 2 CClass.h(217): Siehe Deklaration von 'CClass::int2hex'
    

    mfg Spoocy



  • Die Standardparameter dürfen nur bei der ersten Deklaration angegeben werden.



  • Also jetzt verwirrt mich MS VS, ich hab es in der cpp Datei geändert. Und nun ist es so das der Code mal zu übersetzen geht und mal nicht. Obwohl ich nichts an dem Code geändert habe.

    error LNK2001: Nicht aufgelöstes externes Symbol ""public: class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const __thiscall CClass::int2hex<unsigned long>(unsigned long,bool,char)"

    EDIT: Soweit konnte ich es jetzt schon eingrenzen: In der CClass ist die Function deklariert und definiert. Wenn ich irgendwo in der CClass.cpp den Aufruf

    CClassPointer->int2Hex(338478);

    dann kann ich in jeder anderen cpp Datei auch int2Hex(unsigned long) machen. Wenn ich aber in der CClass.cpp nun nicht den Aufruf mache, wird keine Funktion mit den default Parametern erstellt. Und das obwohl ich die CClass.h in z.b. AndereClass.cpp einbinde.



  • Templates werden nicht in einer cpp-Datei implementiert, sondern nur im Header. Die komplette Definition des Templates muss an der Verwendungsstelle sichtbar sein.



  • omg, ok das war es. Gibt es sonst irgend welche Verbesserungen an dem Code?



  • Hallo, du brauchst std::hex und std::uppercase nicht jedesmal in der Schleife
    setzen, auch wenn der Compiler das evtl. wegoptimiert...

    template<typename T1>
    const std::string int2hex(const T1 value, bool separate = true, char delimiter = ' ')
    {
    	char size = sizeof(value);
    	std::ostringstream ss;
        // Setze hex und uppercase einmalig hier, das reicht
        ss << std::hex << std::uppercase; // oder std::setioflags()
    
    	for (char i = size * 2 - 1; i >=0; i--)
    	{
    		ss << (value >> 4 * i & 0xF);
    
    		if (separate && i > 0 && i % 2 == 0)
    		{
    			ss << delimiter;
    		}
    	}
    
    	return ss.str();
    }
    

    Dann würde ich mir den 'seperate' sparen und das 'delimiter'
    in der if-Abfrage prüfen....

    template<typename T1>
    const std::string int2hex(const T1 value, char delimiter = ' ') {
    
    ...     // char('\0') == int(0) == bool(false)
            if (delimiter && i > 0 && i % 2 == 0)
            {
                ss << delimiter;
            }
    ...
    }
    
    int main() {
    
      // nicht getestet....
      int f = 8903485;
      std::cout << int2hex(f, '\0') << '\n';   // 0087DB3D 
      std::cout << int2hex(f, 0) << '\n';      // int(0) == char('\0') 0087DB3D 
      std::cout << int2hex(f, false) << '\n';  // bool(false) == char('\0') 0087DB3D
      std::cout << int2hex(f) << '\n';         // 00 87 DB 3D
      std::cout << int2hex(f, '-') << '\n';    // 00-87-DB-3D
    
    }
    

    Wenn die Hex-Zahlen meistens ohne Trennsymbol ausgegeben werden sollen
    würde ich es umgekehrt machen:

    template<typename T1>
    const std::string int2hex(const T1 value, char delimiter = '\0')
    {
    ...
    }
    
    int main() {
    
      // nicht getestet....
      int f = 8903485;
      std::cout << int2hex(f)      << '\n';    // 0087DB3D 
      std::cout << int2hex(f, ' ') << '\n';    // 00 87 DB 3D
      std::cout << int2hex(f, '-') << '\n';    // 00-87-DB-3D
    
    }
    

    Einen schönen Tag noch...
    dirkski



  • Man könnte noch:

    if (delimiter && i > 0 && i % 2 == 0)

    if (delimiter && i && i % 2 == 0)

    machen oder? Denn wenn i gleich 0 ist, bricht die abfrage ja schon ab. Aber ich meine gelesen zu haben, das dieses verhalten (was ich so aus perl und php kennt) in c++ nicht zwangsweise so sein muss.

    Wobei ich nicht weis ob es überhaupt einen unterschied macht 😕



  • MrSpoocy schrieb:

    Man könnte noch:

    if (delimiter && i > 0 && i % 2 == 0)

    if (delimiter && i && i % 2 == 0)

    machen oder? Denn wenn i gleich 0 ist, bricht die abfrage ja schon ab. Aber ich meine gelesen zu haben, das dieses verhalten (was ich so aus perl und php kennt) in c++ nicht zwangsweise so sein muss.

    Wobei ich nicht weis ob es überhaupt einen unterschied macht 😕

    Ja, müßte gehen da i ja niemals kleiner als 0 werden kann.
    0 == false, alles andere == true. Hab mir das aber nicht so genau angeguckt,
    tut ja was es soll 😃

    EDIT: Vielleicht weiß jemand von den >= C++11-Spezialisten ob sich für
    die Rückgabe ein std::move() lohnt. Ich blicke da noch nicht so durch.

    #include <utility>
    
    template<typename T1> 
    const std::string int2hex(const T1 value, char delimiter = '\0') 
    { 
    ...
      return (std::move(ss.str()));
    }
    

    hth
    dirkski



  • template<typename T1>
    const std::string //const weg, weil das Move verhindert
    int2hex(const T1 value, bool separate = true, char delimiter = ' ')
    {
    	char size = sizeof(value); //der Typ ist size_t
    	std::ostringstream ss;
    
    	for (char i = size * 2 - 1; i >=0; i--) //char kann unsigned sein. Klammern wären schon
    	{
    		ss << std::hex << std::uppercase << (value >> 4 * i & 0xF); //Klammern fehlen
    
    		if (separate && i > 0 && i % 2 == 0) //Klammern fehlen mir persönlich auch hier
    		{
    			ss << delimiter;
    		}
    	}
    
    	return ss.str();
    }
    }
    


  • TyRoXx schrieb:

    ...
    	for (char i = size * 2 - 1; i >=0; i--) //char kann unsigned sein
    ...
    

    Jep, das ist übel, ist mir gar nicht aufgefallen 🙂
    Wenn char unsigned ist würde nach dem (eigentlich) letzten
    Schleifendurchlauf (i == 0) i nochmal um 1 decrementiert werden. Ergebnis
    wäre ein Überlauf und i hätte wieder den Wert 255. Das ist dann eine Endlosschleife 😮

    Als Schleifenvariablen besser nie char verwenden sondern explizit signed.
    Oder einfach int.

    Einen schönen Tag noch...
    dirkski



  • Noch ein Vorschlag: Wenn eine Template-Funktion nur mit einer bestimmten Klasse von Typen umgehen kann, schränke ich diese gerne explizit ein.
    Das kann man mit einem std::enable_if machen, oder in diesem Fall auch gut mit einem static_assert (simpler und liefert eine lesbarere Fehlermeldung):

    const std::string int2hex(const T1 value, bool separate = true, char delimiter = ' ')
    {
        static_assert(std::is_integral<T1>::value, "int2hex supports integral types only");
        ...
    }
    

    manni66 schrieb:

    Templates werden nicht in einer cpp-Datei implementiert, sondern nur im Header. Die komplette Definition des Templates muss an der Verwendungsstelle sichtbar sein.

    Den ersten Satz würde ich streichen, der zweite bringt es eher auf den Punkt.
    ("Private" Templates, die man nur in einer .cpp-Datei benötigt sind nicht unbedingt "falsch").

    dirkski schrieb:

    EDIT: Vielleicht weiß jemand von den >= C++11-Spezialisten ob sich für
    die Rückgabe ein std::move() lohnt. Ich blicke da noch nicht so durch.

    #include <utility>
    
    template<typename T1> 
    const std::string int2hex(const T1 value, char delimiter = '\0') 
    { 
    ...
      return (std::move(ss.str()));
    }
    

    Da der Rückgabewert von ss.str() ohnehin ein rvalue ist, sollte hier so oder so ein Move stattfinden. Ich halte das std::move daher für überflüssig.

    Möglicherweise könnte jedoch folgender Code effizienter sein:

    template<typename T1> 
    std::string int2hex(const T1 value, char delimiter = '\0') 
    { 
        ...
        auto result = ss.str();
        return result;
    }
    

    Grund: Handelt es sich bei dem Ausdruck der im Return-Statement zurückgegeben wird um den Namen eines automatischen Objekts (lokale Stack-Variable),
    dann erlaubt es der Standard dem Compiler das Objekt direkt im Rückgabewert zu konstruieren (Return Value Optimization).
    Ebenso darf der Compiler das implizite Move wegoptimieren (z.B. result = ss.str() und hex = int2hex(value) ) und das Objekt auch hier direkt in der Zielvariablen konstruieren.

    Ich habe das nicht getestet und man müsste mal tatsächlich prüfen, was für einen Code der Compiler erzeugt, aber meines erachtens erlaubt der Standard dem Compiler bei obigem Code
    z.B. std::string hex = int2hex(value); so zu optimieren, dass ein interner String, der innerhalb von ostringstream::str() eine lokale Variable ist, direkt in hex konstruiert wird.
    Natürlich ist das abhängig davon, wie die Funktion ostringstream::str() implementiert ist und ob deren Code überhaupt optimiert werden kann (also sichtbar für den Compiler und nicht von extern eingebunden).
    Meiner Meinung nach sollte das allerdings nicht schlechter als ein return ss.str() bzw. return std::move(...) sein (ich erwarte dass bei den zwei result -Zeilen da oben zumindest ein Move wegoptimiert wird).

    Gruss,
    Finnegan


  • Mod

    Finnegan schrieb:

    Das kann man mit einem std::enable_if machen, oder in diesem Fall auch gut mit einem static_assert (simpler und liefert eine lesbarere Fehlermeldung):

    enable_if ist dafür zuständig, ein Funktionstemplate nur in bestimmten Situationen verfügbar zu machen. static_assert wird (i.d.R.) verwendet, um die Validität und Sinnhaftigkeit eines bereits ausgewählten (Funktions)templates bei der Instantiierung zu prüfen. Hier ist static_assert besser, da der Name des Funktionstemplates selbst schon Ganzzahlen impliziert, aber die beiden sind meistens nicht sinnvoll austauschbar, weil sie eben unterschiedliche Dinge tun (sollten).


  • Mod

    Grund: Handelt es sich bei dem Ausdruck der im Return-Statement zurückgegeben wird um den Namen eines automatischen Objekts (lokale Stack-Variable),
    dann erlaubt es der Standard dem Compiler das Objekt direkt im Rückgabewert zu konstruieren (Return Value Optimization).

    Nein, deins ist genauso effizient wie die vorige Variante. Die Temporary, die der Rückgabewert von str ist, wird direkt im Endobjekt erzeugt (s.u.). Falls die Implementierung innerhalb str es erlaubt (sehr wahrscheinlich) werden gar alle Moves elidiert.

    (also sichtbar für den Compiler und nicht von extern eingebunden).

    Das ist zwangsläufig, da str ein Temploid ist. (Edit: Außer die Implementierung ist unglaublich drollig und packt eine explizite Instantiierung rein und die Implementierung woanders, also nur für ostringstream<char> .)

    Wobei const im Rückgabewert tatsächlich etwas völlig sinnfreies ist. Entweder es verhindert potenziell Move-Semantik für Klassentypen, oder hat gar keinen Effekt, da const von dem Typ eines andersartigen prvalue Rückgabewerts einfach abgeschnippelt wird.

    copy elision wird jedoch trotzdem angewandt. Schließlich können wir noch kopieren. Zuerst wird der Rückgabewert direkt konstruiert, und dieser wird wiederum direkt im entsprechenden Endobjekt konstruiert - beide dieser Schritte werden durch den folgenden Paragraphen ermöglicht:

    12.8/(31.3) schrieb:

    when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

    Der einzige Nachteil besteht in dem Falle wo copy elision aus unerfindlichen Gründen (e.g. -fno-elide-constructors ) nicht angewandt wird. Zwar wird laut 12.8/32 ein implizites Move generiert, i.e. der Code ohne [c]move [/c]ist tatsächlich äquivalent zu [..]
    Mein Fehler, selbst dass ist dann nicht möglich. Es werden daher zwei Kopien(!) gemacht.



  • Arcoth schrieb:

    Finnegan schrieb:

    Das kann man mit einem std::enable_if machen, oder in diesem Fall auch gut mit einem static_assert (simpler und liefert eine lesbarere Fehlermeldung):

    enable_if ist dafür zuständig, ein Funktionstemplate nur in bestimmten Situationen verfügbar zu machen. static_assert wird (i.d.R.) verwendet, um die Validität und Sinnhaftigkeit eines bereits ausgewählten (Funktions)templates bei der Instantiierung zu prüfen. Hier ist static_assert besser, da der Name des Funktionstemplates selbst schon Ganzzahlen impliziert, aber die beiden sind meistens nicht sinnvoll austauschbar, weil sie eben unterschiedliche Dinge tun (sollten).

    Ja, eine Möglichkeit das einzuschränken ist eben die Funktion nur für Integer-Typen zu definieren, das ist die enable_if -Variante.
    Diese ist unterm Strich dasselbe wie eine Reihe von Nicht-Template-Funktionen nur für Integer-Typen schreiben.
    Das kann durchaus auch eine valide Lösung sein um zu verhindern, dass das Template mit einem Typen instanziert wird,
    für den es nicht geschrieben wurde ( static_assert find ich ja auch besser).

    Dass enable_if und static_assert immer frei austauschbar wären, habe ich auch nicht behauptet.

    Finnegan



  • Arcoth schrieb:

    Grund: Handelt es sich bei dem Ausdruck der im Return-Statement zurückgegeben wird um den Namen eines automatischen Objekts (lokale Stack-Variable),
    dann erlaubt es der Standard dem Compiler das Objekt direkt im Rückgabewert zu konstruieren (Return Value Optimization).

    Nein, deins ist genauso effizient wie die vorige Variante. Die Temporary, die der Rückgabewert von str ist, wird direkt im Endobjekt erzeugt (s.u.). Falls die Implementierung innerhalb str es erlaubt (sehr wahrscheinlich) werden gar alle Moves elidiert.

    Jetzt wo ichs mir nochmal ansehe, denke ich dass du damit recht hast, und ich bin auch froh darüber, da es keine Verrenkungen erfordert.
    Ich habe mich bei meinem Gedankengang zu sehr auf RVO fixiert: Der Ausdruck ss.str() ist eben nicht der Name einer lokalen Variable und kann daher nicht für RVO herangezogen werden.
    Gleichzeitig gilt aber noch der von dir zitierte Absatz zur Copy Elision aus dem Standard:
    So wie ich das verstehe wird dann bei einem simplen return ss.str() innerhalb von int2hex() zwar direkt keine RVO gemacht (sondern lediglich innerhalb von ostringstream::str() bzw.
    in einer eventuellen weiteren Hilfsfunktion), aufgrund von Copy Elision können dann allerdings die Rückgabewerte "durchgereicht" werden so dass der String dennoch direkt in hex konstruiert wird.
    Macht das Sinn?

    Gruss,
    Finnegan



  • Also das ganz wird für mich zu hoch, dafür kann ich c++ einfach nicht gut genug. Ich bin jetzt auch unschlüssig was am ende die "richtige" Lösung ist.

    Letzter Stand ist:

    template<typename T1>
    		tstring int2hex(const T1 value, char delimiter = ' ')
    		{
    			signed char size = sizeof(value);
    			tostringstream ss;
    			ss << std::hex << std::uppercase;
    
    			for (signed char i = size * 2 - 1; i >= 0; i--)
    			{
    				ss << ((value >> (4 * i)) & 0xF);
    
    				if (delimiter && i && (i % 2) == 0)
    				{
    					ss << delimiter;
    				}
    			}
    
    			return ss.str();
    		}
    


  • Ich finde die Schleife sehr verwirrend. Ohne ein paar Unit Tests würde ich nicht meine Hand dafür ins Feuer legen, dass die das richtige tut.



  • TyRoXx schrieb:

    Ich finde die Schleife sehr verwirrend. Ohne ein paar Unit Tests würde ich nicht meine Hand dafür ins Feuer legen, dass die das richtige tut.

    Naja, minimum und maximum bei unsigned- sowie signed-typen funzt das...

    Und ein paar Zahlen dazwischen auch. Test bestanden 😉

    Einen schönen Abend noch...
    dirkski

    Edit: Aber was ist ein tstring?



  • tstring und tostringstream sind einfach typedef für std::(w)string etc.



  • MrSpoocy schrieb:

    Ich bin jetzt auch unschlüssig was am ende die "richtige" Lösung ist.

    Die "richtige" Lösung ist natürlich nur ein kurzlebiger Proxy, der vom op<<(ostream verstanden wird.
    Und lass die Kindereien in Sachen ">>4" und "i&0xF", wir sind nicht mehr im Gipskrieg.


Log in to reply