Unicode/Ansi: Eure Vorgehensweisen



  • Hallo,

    als ich letztes Jahr anfing mich wieder (nach dem Studium) mit C++-Programmierung zu beschäftigen, musste ich schnell merken, dass das, was ich im Studium über C++ beigebracht bekommen habe, nur ein kleiner Einblick in die C++-Welt war und meistens mehr C bzw. "Klassen-C" war oder MS-spezifisches.

    Gut, fangen wir mal richtig an C++ zu lernen, dachte ich mir. Meine Anlaufstellen waren zunächst diverse C++-Tutorials, Websites und ebooks zum Thema, dann "Effektiv C++" von Scott Meyers und dieses Forum hier.

    Nun zum eigentlichen Thema: Damals bin ich ziemlich schnell über die _T-Macros von MSVC auf das Thema Zeichencodierung gestoßen. Nach einiger Recherche dann, traf ich die Entscheidung vollständig Unicode zu verwenden. Immer. Schließlich ist Unicode moderner, besitzt einen höheren Zeichenumfang und Windows arbeitet eh intern mit Unicode und besitzt sogar reine Unicode-WinApi-Funktionen (eine habe ich zumindest angetroffen). Warum also noch Ansi/MBCS...

    Leider hat sich das Thema mit dieser Entscheidung nicht erledigt und bereitet mir heute immer wieder Kopfzerbrechen. Gerade die C++-Standardbibliothek, die eigentlich für alles ein Unicode-Pendant besitzt macht einem das Leben schwer. Zu mindest mir:

    ➡ std::wfstream: Da hat man alles in seinem Programm in Form von Unicode-Strings, wfstream erwartet den Dateinamen aber als char-C-String. 😡

    ➡ std::exception: Alle Texte in Unicode. Logging in Unicode. std::exception::what() leider nicht in Unicode. std::runtime_error und std::domain_error nehmen auch nur std::string/char* als Übergabeparameter an. 😡

    ➡ Ausgabe-Streams: Jetzt kann man aber die vollen Vorzüge von Unicode genießen: Ausgabe von Strings in Unicode! Unendliche Weiten der Zeichencodierung endlich dem User sichtbar machen. Welch ein Augenschmaus! Denkste... std::cout und std::fstream unterstützen mehr Zeichen wie std::wcout und std::wfstream. 😡

    Eigentlich bin ich jetzt soweit, dass ich nur noch vollständig auf std::string, std::fstream und Co. setzten möchte. Unicode ist ja schön und gut, aber wenn man C++ mit der Standardbibliothek programmieren möchte, kann man das eh nicht konsequent durchziehen, bzw. hat genau das Gegenteil erreicht (reduzierung des Zeichenumfanges).

    Wie sieht da bei Euch aus? Findet man bei Euch std::w...? Habt ihr auch solche Probleme oder mache ich etwas falsch? Wenn ich hier im Forum z.B. nach std::string<->std::wstring-Konvertierungen suche, dann sieht man Antworten wie "das braucht man eh nicht!". 😕



  • Ich benutze durchgängig Widecharacter-Varianten. Deine Schwierigkeiten kann ich deshalb nicht nachvollziehen. Ich kann dir folgendes erstmal ans Herz legen:
    http://www.kharchi.eu/wiki/doku.php?id=cpp:std:string#internationalisierung

    Nun noch zu deinen speziellen Anliegen:

    std::wfstream mit Filename:

    der Dateiname hat nichts mit dem zu tun, was du in den Stream schreibst oder raus liest! Der Dateiname ist halt noch von 1998 und da waren halt noch ASCII-Dateinamen gängig. Oder konnte man damals in CD-ROMs, Fat16 usw. chinesische Filenamen benutzen? Nein! Aber C++ ist nunmal Multiplattform und muß sich in solchen Fällen an Kompromisse halten.

    Das NTFS und Unix-Filesysteme heute vielleicht Unicode-Dateinamen unterstützten, ist kein Beinbruch. Lösung: benutze Boost.Filesystem für den Dateinamen! Und wenn wir Glück haben, sehen wir Boost.Filesystem im C++ TR2. Denn in Wahrheit fehlt uns in C++ eine Filesystem-Library, und darüber sollte man sich eher aufregen. 😉

    std::exception und what():

    Das what() liefert einen technischen Errortext. Und technische Texte sind meistens für den Anwender unbrauchbar und reichen deshalb in englisch. Ein Anwender kann mit "Index out of range" nicht wirklich was anfangen. 😉 Exceptions sind zur Programmsteuerung da! Wenn du chinesische Errortexte dem Anwender ausgeben willst, solltest du diese anhand des Exceptiontyps ausgeben. what() schreibe ich z.B. in Logdateien. Nichts für den Endanwender!

    Ausgabe-Streams:

    wcin und wcout machen das, was man ihnen sagt. Schau dir mal imbue() an! Meistens ist das locale-Objekt so implementiert, das es einfach alles in Bytes umwandelt. Du mußt dann halt ein anderes Locale-Objekt benutzen, um eine korrekte Ausgabe/EIngabe zu haben. Das ist aber deine Aufgabe, das richtige Locale zu benutzen. Außerdem kann z.B. die Windows-Konsole von Haus aus eh nur ASCII ausgeben.

    Es gibt sicherlich ein paar Ungereimtheiten oder man muß noch selber was "einstellen" (siehe Locales). Aber es gibt meistens einfache Lösungen in Dritt-Bibliotheken, für die von dir genannten Dinge (z.B. Boost.Filesystem).

    Noch als Tip: ich benutze für technische Strings std::string (weil meistens englisch genügt). Alles was der User verarbeitet mit std::wstring.



  • Hi,

    also ich habe mir eine eigene string- und filestream-Klasse geschrieben die intern alles in UTF-8 konvertiert, egal ob ANSI, UTF-16, UTF-32, usw. reinkommt. Meine Erfahrung ist leider, dass die STL nicht gerade für Zeichensätze outside von ANSI murks ist. Multibyte-Zeichensätze sind hier das gute Beispiel für. Dann sind auch viele STL-Implementierungen murksig. Stichwort length/size von std::string, bei MBCS. 😞

    Musste diese Vorgehensweise aus beruflichen Gründen vollziehen (hatten auf dem Zielsystem besondere Vorgaben) und habe das schreiben einer eigenen string-Klasse und filestream-Klasse nie bereut und keinen Stress mehr mit Zeichensätzen. 🙂

    Wer das nicht machen möchte, empfehle ich wie Artchi durchgängig widecharacter zu benutzen, doch auch hier gibt es oft genug Stress.

    Gruß,
    unsigned long



  • Hallo Artchi und danke für Deine Antwort!

    Artchi schrieb:

    der Dateiname hat nichts mit dem zu tun, was du in den Stream schreibst oder raus liest! Das der Dateiname ist halt noch von 1998 und da waren halt noch ASCII-Dateinamen gängig.
    Das NTFS und Unix-Filesysteme heute vielleicht flexiblere Dateinamen unterstützten, ist kein Beinbruch. Lösung: benutze Boost.Filesystem für den Dateinamen! Und wenn wir Glück haben, sehen wir Boost.Filesystem im C++ TR2.

    Aber kein Grund sich aufzuregen, das angeblich Widecharacter nicht funktionieren.

    Das ist mir bewusst und der Grund ist klar. Mein Problem war nun, dass ich beim Design meiner "Config-Reader-Klassen" nur Unicode-Strings vorgesehen habe. Auch Dateinamen, die ich benötige habe ich in Unicode eingelesen. Als ich diese nun mit std::wfstream öffnen wollte, hatte ich ein Problem. Als Anfänger weiß man das nicht. Es soll auch nicht bedeuten, dass "Widecharacter nicht funktionieren", sondern nur, dass man eben nicht vollständig auf Widecharacter setzen kann. Problematisch wird es vor allen dann, wenn man den Dateinamen auch ausgeben muss und die Ausgabe eben wieder auf Unicode festgelegt ist. Entweder man liest den Dateinamen dann als std::string & std::wstring ein oder man konvertiert z.B. bei der Ausgabe. Man erhält halt einen "Mischbetrieb", den ich in meiner jungfräulichen Naivität verhindern wollte.

    Artchi schrieb:

    std::exception und what():

    Das what() liefert einen technischen Errortext. Und technische Texte sind meistens für den Anwender unbrauchbar. Ein Anwender kann mit "Index out of bounds" nicht wirklich was anfangen. 😉 Exceptions sind zur Programmsteuerung da! Wenn du Chinesische Errortexte haben willst, solltest du diese anhand des Exceptiontyps ausgeben. what() benutze ich für Logdateien.

    Genau, und mein "Logging-Design" setzte eben auch nur auf reines Unicode. Das war mein Problem. Du kannst also doch nicht durchgängig auf Widecharacter-Strings setzten. 😉

    Artchi schrieb:

    Ausgabe-Streams:

    wcin und wcout machen das, was man ihnen sagt. Schau dir mal inbue() an! Meistens ist das locale-Objekt so implementiert, das es einfach alles in Bytes umwandelt. Du mußt dann halt ein anderes Locale-Objekt benutzen, um eine korrekte Ausgabe/EIngabe zu haben. Das ist aber deine Aufgabe, das richtige Locale zu benutzen. Außerdem kann z.B. die Windows-Konsole von Haus aus eh nur ASCII ausgeben.

    Ich benutze Locale-Objekte und trotzdem kann ich mit std::cout z.B. das €-Zeichen ausgeben (aus Windows-Konsole, während es std::wcout nicht kann. Das Gleiche gilt wieder für das Schreiben in eine Textdatei mittels std::(w)fstream.

    Vielleicht liegt der Fehler bei mir, dann wäre ich für ein Code-Beispiel dankbar, welches z.B. das €-Zeichen mittels std::wcout ausgibt.



  • Roger Wilco schrieb:

    Artchi schrieb:

    Außerdem kann z.B. die Windows-Konsole von Haus aus eh nur ASCII ausgeben.

    Ich benutze Locale-Objekte und trotzdem kann ich mit std::cout z.B. das €-Zeichen ausgeben (aus Windows-Konsole, während es std::wcout nicht kann. Das Gleiche gilt wieder für das Schreiben in eine Textdatei mittels std::(w)fstream.

    Auf deutsch: Es geht nicht mit der Windows-Konsole... Sie kann nun mal "nur" ASCII - da ist nichts mit L'€' oder so - '€' hingegen geht aber (weil es nun mal ASCII ist)

    bb



  • unskilled schrieb:

    Auf deutsch: Es geht nicht mit der Windows-Konsole... Sie kann nun mal "nur" ASCII - da ist nichts mit L'€' oder so - '€' hingegen geht aber (weil es nun mal ASCII ist)
    bb

    Ok, musste es eben auch feststellen. Also eher ein "Windows_Problem". Bedeutet also, dass man also bei Windows-Konsolen-Programmen (weiß nicht wie es unter Linux aussieht) std::cout verwenden sollte.

    Ich hätte einfach nicht gedacht, dass ich mit der Entscheidung durchgängig Unicode/wchar-Varianten zu benutzen, solche Probleme bekommen würde.



  • unskilled schrieb:

    Sie kann nun mal "nur" ASCII

    So schlimm ist es auch wieder nicht 😉 Soweit ich weiß, ist die Konsole nur durch die eingestellte Codepage eingeschränkt (fällt mit Verwendung von z.B. UTF-8 weg) und durch die Schriftart (es muss ein Glyph für den Character existieren).



  • Roger Wilco schrieb:

    Ok, musste es eben auch feststellen. Also eher ein "Windows_Problem". Bedeutet also, dass man also bei Windows-Konsolen-Programmen (weiß nicht wie es unter Linux aussieht) std::cout verwenden sollte.

    Ich hätte einfach nicht gedacht, dass ich mit der Entscheidung durchgängig Unicode/wchar-Varianten zu benutzen, solche Probleme bekommen würde.

    Warum kannst du kein wcout benutzen??? Huff... hast du dir überhaupt meinen Link oben angeschaut???

    Natürlich kann die Windows-Konsole auch Euro-Zeichen ausgeben, wenn es sein muß auch Kyrillisch. Es muß nur die richtige Codepage und/oder Font in der Konsole eingestellt sein. Das ist kein Problem von wcout, diese Schwierigkeit bekommst du mit jeder Programmiersprache.

    Und Widecharacter zu benutzen ist kein Fehler. Wenn die Windows-Konsole kein UTF-8 versteht, mußt du halt ein Locale mittels imbue() übergeben, die die Windows-Konsole versteht.

    Standard-mäßig ist in der dt. Windows-Konsole die CP 1252 eingestellt: das EURO-Symbol hat dort den Dezimalcode 128. Eine Locale im Stream müsste also entsprechend die Konvertirung vornehmen, damit du dich nicht drum kümmern bräuchtest. Oder es wird die Codepage in der Konsole umgestellt.

    Und ohne wcout wäre die Situation keinen Deut besser. Das Codepage-Thema würde immer noch da sein.

    Die Linux-Konsolen benutzen meines Wissens die Codepage UTF-8. D.h. du bräuchtest auch dort eine Locale im Stream, die in UTF-8 wandelt.
    Für einen Anfänger alles vielleicht verwirrend, weil tatsächlich umständlich. Les dir einfach mal meinen Link durch, dann wirds vielleicht einfacher.





  • Gib mal in deiner Windows-Konsole chcp ein, damit du erfährst, welche CP eingestellt ist.

    Mit chcp 857 kannst du z.B. auf die türkische CP umschalten.

    Mehr: http://technet.microsoft.com/en-us/library/bb490874.aspx

    Im Fenstermenü von der Konsole unter Eigenschaften > Schriftart > Lucida COnsole kannst du nen vernünftigen Font einstellen.



  • @Roger Wilco! Damit dir einiges verständlicher wird, wie die Konsole bzw. Codepages arbeiten:

    #include <iostream>
    
    void printAll()
    {
    	for(int i=0; i<256; i++)
    		std::wcout << (wchar_t)i;
    	std::wcout << std::endl;
    }
    int main()
    {
    	system("chcp 850");
    	printAll();
    	system("chcp 1252"); // Mit EUR-Symbol
    	printAll();
    }
    

    Dann aber den richtigen Font einschalten!



  • Danke Artchi für Deine Hilfe, ich hab's jetzt! 😉

    Vor einiger Zeit bin ich bei der Suche nach Thema auf ein Artikel gestoßen, in dem es ein Workaround zur Widechar-Ausgabe auf die Windows-Konsole (via std::cout) gab. Dort wurde behauptet std::cout unterstütze 256 Zeichen und std::wcout nur 128.

    Der Code dort funktionierte und das mit std::wcout habe ich naiver-weise einfach geglaubt.

    Hier nochmal ein Code-Beispiel, falls hier jemand landet, der gerne Unicode-Strings (std::wstring) auf die Konsole ausgeben möchte:

    #include <windows.h> // Get/SetConsoleOutputCP()
    #include <locale>    // std::locale
    #include <string>    // std::(w)string
    #include <iostream>  // std::(w)cout
    
    int main(){
    
        unsigned int oldCodepage = GetConsoleOutputCP(); // aktuelle Konsolen-Codepage sichern
        if(!SetConsoleOutputCP(1252)){ // Codepage 1252 (Westeuropa-Zeichensatz)
          std::cout << "Failed to setup Console-Codepage!" << std::endl;
            return 1;
        }
    
        std::locale gerLocal("german"); // alternativ std::locale("") (lädt Benutzer-Einstellung)
        std::locale::global(gerLocal);
        // alternativ einzelnd setzen:
        // std::cout.imbue(gerLocal); 
        // std::wcout.imbue(gerLocal);
    
        std::cout << "std::cout: \x80" << std::endl;
        std::wcout << L"std::wcout: \x20AC" << std::endl;
    
        std::cout << "std::cout: €" << std::endl; // geht zumindest bei mir (VC6.0)
        std::wcout << L"std::wcout: €" << std::endl;
    
        SetConsoleOutputCP(oldCodepage); // ursprüngliche Codepage wieder einstellen
        return 0;
    }
    

    Konsolenausgabe:

    std::cout: €
    std::wcout: €
    std::cout: €
    std::wcout: €

    Wichtig ist nur, wie Artchi schon sagte, dass eine Unicode-Schriftart in der Konsole aktiv ist (z.B. Lucida Console):

    WINAPI schrieb:

    If the current font is a fixed-pitch Unicode font, SetConsoleOutputCP changes the mapping of the character values into the glyph set of the font, rather than loading a separate font each time it is called. This affects how extended characters (ASCII value greater than 127) are displayed in a console window. However, if the current font is a raster font, SetConsoleOutputCP does not affect how extended characters are displayed.

    Und hier noch ein empfehlenswerter Link zum Thema The Standard C++ Locale



  • Ja, das mit dem Locale und imbue() ist entscheidend (hatte ich ja mehrmals geschrieben). Leider ist es echt schwierig auf solche Infos im Web zu stossen. Die Locales und Std-Streams werden leider sehr stiefmütterlich behandelt, weil die meisten C++-User diese sehr selten benutzen.

    Das man da aber einfach "german" als Locale verwenden kann, war mir so nicht bewusst.

    Aber eine Sache ist sehr gefährlich:

    // O.K.:
        std::cout << "std::cout: \x80" << std::endl;
        std::wcout << L"std::wcout: \x20AC" << std::endl;
    
        // Gefährlich:
        std::cout << "std::cout: €" << std::endl; // geht zumindest bei mir (VC6.0)
        std::wcout << L"std::wcout: €" << std::endl;
    

    Wenn das EUR-Symbol direkt eingegeben wird, ist es Quelltext-Editor- und System-abhängig. Weil du dann das Symbol so eingibst, wie es dein Editor oder OS verstehen. Die \x-Variante ist vorzusziehen, da so eindeutig wird, welches Sonderzeichen gemeint ist.

    Danke für den Beispielcode! 👍



  • Artchi schrieb:

    Wenn das EUR-Symbol direkt eingegeben wird, ist es Quelltext-Editor- und System-abhängig. Weil du dann das Symbol so eingibst, wie es dein Editor oder OS verstehen. Die \x-Variante ist vorzusziehen, da so eindeutig wird, welches Sonderzeichen gemeint ist.

    Wann genau wird das gefährlich? Wie muss ich mir das vorstellen: OS+IDE nutzen z.B. irgendeine Codepage bei der das EUR-Zeichen den Hex-Code xyz hat und der wird dann in den String gespeichert? Ich dachte, dass wird vom Editor entsprechend umgesetzt. Immerhin unterscheidet er ja auch "€" und L"€", was unterschiedliche Hex-Codes ergibt.

    Welche Zeichen sollte man dann lieber per Hex-Code eingeben? Dann sind ja theoretisch alle Zeichen nur per Hex-Code eindeutig.



  • Wenn du dir die verschiedenen Codepages anschaust (inkl. Unicode), wirst du feststellen, das alle Zeichen bis 128 gleich sind. Und das sind konkret gesagt die ersten 128 Zeichen vom US-ASCII. Die kannst du also über das Keyboard direkt eingeben. Alles was dadrüber ist, ist in jeder Codepage potenziell anders. Das EURO-Zeichen als 129. Zeichen zählt dazu.

    Meine beiden Schleifen in dem oberen Beispiel machen das auch deutlich.


Anmelden zum Antworten