null in std::string -> UB oder OK?



  • Hallo zusammen,

    folgende zwei Assertions halten hier bei mir (VC10 32bit):

    #include <cassert>
    #include <string>
    int main()
    {
        using namespace std;
        string s("0123");
        s.back() = '\0';
        assert(s.size() == 4);
        assert(s.back() == '\0');
    }
    

    Ist das vom Standard so vorgeschrieben oder sollte ich mich da lieber nicht drauf verlassen?

    Gruß
    Dobi



  • C++ std::strings sind nicht nullterminiert. Darauf kannst du dich AFAIK verlassen.



  • Du schreibst in das letzte Zeichen des String das Zeichen '\0'. Was soll daran nicht definiert sein?
    Die Frage ist, warum tust du das? Du wirst dir mit diesem Konstrukt in den Fuß schießen!



  • manni66 schrieb:

    Du schreibst in das letzte Zeichen des String das Zeichen '\0'. Was soll daran nicht definiert sein?

    Die Tatsache, dass der Debugger erstmal das letzte Zeichen nicht mit anzeigt, und dass hierbei:

    string s = string("0") + string("1\0");
    

    s.size() == 2 (und nicht 3) ist, hat mich skeptisch gemacht. Aber OK, "1\0" wird hier halt erstmal nullterminiert behandelt.
    Kopieren hingegen geht übrigens klar:

    #include <cassert>
    #include <string>
    int main()
    {
    	using namespace std;
    	string s("0123");
    	s.back() = '\0';
    	auto b = s;
    	assert(s.size() == 4);
    	assert(b.size() == 4);
    	assert(s.back() == '\0');
    	assert(b.back() == '\0');
    }
    

    manni66 schrieb:

    Die Frage ist, warum tust du das? Du wirst dir mit diesem Konstrukt in den Fuß schießen!

    In einem kleinen TCP/IP-Protokoll, dessen Tokens in meiner Software als Strings rumgereicht werden, kommt nun die (unnötige) Anforderung hinzu, dass an einer Stelle ein redundantes '\0' hinten dran sein muss.
    Da sich diese Krankheit aber ziemlich isoliert in einer kleinen Klasse abspielen wird, und ich die Gefahr, dass sich die Seuche durch die ganze Software ausbreiten wird, momentan als sehr gering einschätze, wollte ich jetzt nicht deswegen alles umschreiben, sondern es erstmal so lösen. Ich denke also, meine Füße (und die von anderen) sollten erstmal weiterhin sicher sein. 😉

    Danke für eure Hilfe. 👍



  • Dobi schrieb:

    dass hierbei:

    string s = string("0") + string("1\0");
    

    s.size() == 2 (und nicht 3) ist, hat mich skeptisch gemacht.

    In dem Fall sieht der String ja schon gar nichts mehr vom \0, oder?



  • Jupp, das string-Literal geht in den ctor mit (const char*) und wird dementsprechend der nach der ersten Nullterminierung nicht mehr weiter verarbeitet.


  • Mod

    nman schrieb:

    Dobi schrieb:

    dass hierbei:

    string s = string("0") + string("1\0");
    

    s.size() == 2 (und nicht 3) ist, hat mich skeptisch gemacht.

    In dem Fall sieht der String ja schon gar nichts mehr vom \0, oder?

    richtig. Dieser Stringkonstruktor erwartet ja schließlich nicht ausdrücklich ein Stringliteral sondern einen Zeiger, der auf einen nullterminierten String zeigt. Und sobald diese Null gefunden wird, ist der String eben zu Ende. Man müsste hier entsprechend noch die Größe angeben.

    string s = string("0") + string("1\0",2);
    


  • dass an einer Stelle ein redundantes '\0' hinten dran sein muss.

    dann mach doch dort:

    const char* dest = s.c_str();
    

  • Mod

    Ist das vom Standard so vorgeschrieben oder sollte ich mich da lieber nicht drauf verlassen?

    Ja, ist vorgeschrieben. Das Nullzeichen ist ein Zeichen wie jedes andere auch. Der einzige Kontext, wo es sich bei std::string nicht verhaelt wie jedes andere, ist der von nman gezeigte (C-Strings die ohne explizite Laengenangabe an solche Funktionen uebergeben werden).
    Das hier duerfte stattdessen ein anderes Ergebnis bringen:

    auto s = "a"s + string("b\n", 2);
    

    Edit: Verdammt, camper 😡

    dann mach doch dort

    Es ist nicht definiert, dass s.c_str() == &s[0] .


  • Mod

    Sogar das hier duerfte AFAIR ein anderes Ergebnis bringen:

    auto s = "a"s + "b\n"s;
    

    (Bin gerade in der Schule, kann das nicht testen, camper du hast doch Clang 3.4?)

    Edit: Moment:

    If L is a user-defined-string-literal, let str be the literal without its ud-suffix and let len be the number of code units in str (i.e., its length excluding the terminating null character).

    Dann klappt das also, nicht wahr?



  • Es ist nicht definiert, dass s.c_str() == &s[0].

    hier steht was anderes:

    http://www.cplusplus.com/reference/string/string/c_str/


  • Mod

    weicher schrieb:

    hier steht was anderes:

    Noe.

    Returns a pointer to an array that contains a null-terminated sequence of characters (i.e., a C-string) representing the current value of the string object.

    This array includes the same sequence of characters that make up the value of the string object plus an additional terminating null-character ('\0') at the end.

    Zudem darf der Inhalte dieses zurueckgegebenen C-Strings nicht veraendert warden.



  • Dobi sagt:

    In einem kleinen TCP/IP-Protokoll, dessen Tokens in meiner Software als Strings rumgereicht werden, kommt nun die (unnötige) Anforderung hinzu, dass an einer Stelle ein redundantes '\0' hinten dran sein muss.

    Da bei TCP/IP fast immer irgend eine API aufgerufen wird ist IMHO das Wort "unnötig" zurecht in Klammern.

    std::string.c_str() doku sagt:

    This array includes the same sequence of characters that make up the value of the string object plus an additional terminating null-character ('\0') at the end.

    Also macht std::string.c_str() genau das was Dobi will.



  • weicher schrieb:

    Da bei TCP/IP fast immer irgend eine API aufgerufen wird ist IMHO das Wort "unnötig" zurecht in Klammern.

    Naja, wenn ein Token so aussehen soll:

    Startzeichenfolge
    char[3] -> Anzahl Zeichen
    char[n] -> Zeichen
    \0
    Endzeichenfolge
    

    finde ich die null schon ziemlich unnötig. Ich würde normal entweder die Anzahl Zeichen vorwegschicken oder nullterminieren, aber nicht beides.

    weicher schrieb:

    Also macht std::string.c_str() genau das was Dobi will.

    Nö, selbst wenn das gehen würde, würde sich die Seuche dann ja auch in den Rest des Programms ausbreiten, weil ich mit char* hantieren müsste. Mit der andereren Lösung bleiben schön alles ein String, nur dass manchmal halt '\0' drin vorkommt, was mich im Rest vom Code aber nicht interessieren muss. Ich finde das so wesentlich schicker.



  • Nö, selbst wenn das gehen würde, würde sich die Seuche dann ja auch in den Rest des Programms ausbreiten, weil ich mit char* hantieren müsste.

    Nein, so habe ich das nicht gemeint.

    Du kannst im ganzen Programm ganz normal mit dem std::string arbeiten ohne dieses gefrickel mit dem anhängen einer '\0'.

    Dann must Du nur genau dort wo ein NULL terminierter string verlangt wird den std::string.c_str() verwenden. Das ist übrigends keine Seuche 😉 die meisten API's verwenden const char* oder char* und genau zu diesem Zweck hat std::string die Funktion c_str()



  • Mal ganz banal:

    #include <string>
    #include <iostream>
    
    std::string frame(const std::string& s)
    {
    	return std::string("start") + s + "ende";
    }
    
    std::string generateNormal()
    {
    	return "huhu";
    }
    
    std::string generateFrickelMist()
    {
    	std::string s("003bla_");
    	s.back() = '\0';
    	return s;
    }
    
    std::string generate()
    {
    	bool cond( false );
    	auto s = cond ? generateNormal() : generateFrickelMist();
        return frame(s);
    }
    
    void send(const std::string& s)
    {
        // Druecks ins Socket.
    	std::cout << s << "\n";
    }
    
    int main()
    {
        for(;;)
        {
            auto s = generate();
            send( s );
        }
    }
    

    Wie wär das jetzt mit std::string.c_str() schöner?



  • Nee, wird nicht schöner 😉

    Ich bin davon ausgegangen, dass Du eine struct für einen API Aufruf ausfüllen musst.



  • Arcoth schrieb:

    Es ist nicht definiert, dass s.c_str() == &s[0] .

    Doch.

    (Ja, in C++03 ist es nicht explizit definiert, aber de facto eben doch)



  • Die C++-Programmiersprache §20.3.7 schrieb:

    [...]im von c_str() oder data() reservierten Puffer[...]


  • Mod

    c++11 schrieb:

    Doch.

    Ja, das stimmt, seit C++11:

    const charT* c_str() const noexcept;
    const charT* data() const noexcept;
    

    Returns: A pointer p such that p + i == &operator[](i) for each i in [0,size()] .

    In C++03 war das zwar in den Implementierungen praktisch immer der Fall, war aber nicht garantiert.


  • Mod

    §20.3.7

    So einen Paragraphen gibt es nicht 😕 21.3.7 genauso wenig.


Anmelden zum Antworten