September 2016

Band 31, Nummer 9

C++: Unicode-Codierungskonvertierung mit STL-Zeichenfolgen und Win32-APIs

Von Giovanni Dicanio

Unicode ist der De-Facto-Standard für das Darstellen von internationalem Text in moderner Software. Auf der offiziellen Website des Unicode-Konsortiums (bit.ly/1Rtdulx) heißt es „Unicode gibt jedem Zeichen seine eigene Nummer, systemunabhängig, programmunabhängig, sprachunabhängig“. Jede dieser eindeutigen Nummern wird als Codepunkt bezeichnet und normalerweise mithilfe des Präfixes „U+“ dargestellt, gefolgt von der eindeutigen Nummer in hexadezimaler Schreibweise. Der Codepunkt, der dem Zeichen „C“ zugeordnet ist, ist beispielsweise U+0043. Beachten Sie, dass Unicode ein Branchenstandard ist, der die meisten Schriftsysteme der Welt abdeckt, einschließlich Ideografien. So ist beispielsweise das japanische Kanji-Ideogramm 学, das u.a. „Lernen“ und „Wissen“ bedeutet, dem Codepunkt U+5B66 zugeordnet. Aktuell definiert der Unicode-Standard mehr als 1.114.000 Codepunkte.

Von abstrakten Codepunkten zu tatsächlichen Bits: UTF-8- und UTF-16-Codierungen

Ein Codepunkt stellt allerdings ein abstraktes Konzept dar. Für einen Programmierer stellt sich eine andere Frage: Wie sind diese Unicode-Codepunkte konkret in Form von Computerbits dargestellt? Die Antwort auf diese Frage führt direkt zum Konzept der Unicode-Codierung. Im Prinzip ist eine Unicode-Codierung ein bestimmtes, wohldefiniertes Verfahren zum Darstellen der Werte von Unicode-Codepunkten in Bits. Der Unicode-Standard definiert eine Reihe von Codierungen, von denen die wichtigsten UTF-8 und UTF-16 sind, die beide längenvariable Codierungen darstellen, die alle möglichen Unicode-„Zeichen“ – oder besser: Codepunkte – codieren können. Daher sind Konvertierungen zwischen diesen zwei Codierungen verlustfrei: Kein Unicode-Zeichen geht in dem Prozess verloren.

UTF-8 verwendet, wie der Name nahelegt, Codeeinheiten zu 8 Bit. Es wurde im Hinblick auf zwei wichtige Merkmale entwickelt. Erstens ist es abwärtskompatibel mit ASCII; dies bedeutet, dass jeder gültige ASCII-Zeichencode bei der Codierung in UTF-8 den gleichen Bytewert aufweist. Anders gesagt ist gültiger ASCII-Text automatisch gültiger UTF-8-codierter Text.

Zweitens, da in UTF-8 codierter Unicode-Text lediglich eine Abfolge von 8-Bit-Byteeinheiten ist, gibt es keine Probleme mit der Endiancharakteristik. Die UTF-8-Codierung ist (im Gegensatz zu UTF-16) konzeptbedingt Endian-neutral. Das ist ein wichtiges Feature beim Austauschen von Text zwischen verschiedenen Computersystemen, die unterschiedliche Architekturen mit abweichender Endiancharakteristik aufweisen können.

Wenn wir die zwei zuvor erwähnten Unicode-Zeichen betrachten, zeigt sich, dass der Großbuchstabe C (Codepunkt U+0043) in UTF-8 als einzelnes Byte 0x43 (43 hexadezimal) codiert ist, was exakt der dem Zeichen C zugeordnete ASCII-Code ist (aufgrund der Abwärtskompatibilität von UTF-8 mit ASCII). Im Gegensatz dazu ist das japanische Ideogramm 学 (Codepunkt U+5B66) in UTF-8 als die aus drei Byte bestehende Folge 0xE5 0xAD 0xA6 codiert.

UTF-8 ist die im Internet am häufigsten verwendete Unicode-Codierung. Nach kürzlich erhobenen Statistiken von W3Techs, die unter bit.ly/1UT5EBC verfügbar sind, wird UTF-8 von 87 Prozent aller analysieren Websites verwendet.

UTF-16 ist praktisch die Standardcodierung, die von Unicode-fähigen Windows-APIs verwendet wird. UTF-16 stellt auch in vielen anderen Softwaresystemen die „native“ Unicode-Codierung dar. Beispielsweise verwenden Qt, Java und die ICU-Bibliothek (International Components for Unicode), um nur einige zu nennen, UTF-16-Codierung zum Speichern von Unicode-Zeichenfolgen.

UTF-16 verwendet 16-Bit-Codeeinheiten. Genau wie UTF-8 kann UTF-16 alle möglichen Unicode-Codepunkte codieren. Während UTF-8 jedoch jeden gültigen Unicode-Codepunkt mithilfe von einer bis vier 8-Bit-Byteeinheiten codiert, ist UTF-16 in gewisser Weise einfacher. Tatsächlich sind Unicode-Codepunkte in UTF-16 nur in Form von einer oder zwei 16-Bit-Codeeinheiten codiert. Die Verwendung von Codeeinheiten, die größer als ein einzelnes Byte sind, impliziert jedoch durch die Endiancharakteristik bedingte Probleme: Es gibt tatsächlich sowohl ein Big-Endian-UTF-16 als auch ein Little-Endian-UTF-16 (während nur eine Endian-neutrale UTF-8 Codierung existiert).

Unicode definiert ein Konzept von Ebenen als einer fortlaufenden Gruppe von 65.536 (216) Codepunkten. Die erste Ebene wird als Ebene 0 oder Mehrsprachige Basisebene (Basic Multilingual Plane, BMP) bezeichnet. Die Zeichen fast aller modernen Sprachen und viele Sonderzeichen befinden sich in der BMP, und alle diese BMP-Zeichen werden in UTF-16 mithilfe einer einzelnen 16-Bit-Codeeinheit dargestellt.

Ergänzende Zeichen befinden sich in anderen Ebenen als der BMP; dazu gehören piktografische Symbole wie Emoji und historische Schriften, etwa die ägyptischen Hieroglyphen. Diese ergänzenden Zeichen außerhalb der BMP sind in UTF-16 mithilfe von zwei 16-Bit-Codeeinheiten codiert, die auch als Surrogate-Paare bezeichnet werden.

Der Großbuchstabe C (U+0043) ist in UTF-16 als einzelne 16-Bit-Codeeinheit 0x0043 codiert. Das Ideogramm 学 (U+5B66) ist in UTF-16 als einzelne 16-Bit-Codeeinheit 0x5B66 codiert. Bei vielen Unicode-Zeichen besteht eine direkte Korrespondenz zwischen ihrer „abstrakten“ Codepunktdarstellung (wie z. B. U+5B66) und der dieser zugeordneten UTF-16-Codierung in Hex (Beispiel: das 16-Bit-Wort 0x5B66).

Sehen wir uns spaßeshalber einige piktografische Symbole an. Das Unicode-Zeichen „Schneemann“ (, U+2603) ist in UTF-8 als die aus drei Byte bestehende Folge 0xE2 0x98 0x83 codiert; die UTF-16-Codierung ist jedoch die einzelne 16-Bit-Einheit 0x2603. Das Unicode-Zeichen „Bierglas“ (U+1F37A), das sich außerhalb der BMP befindet, ist in UTF-8 durch die aus vier Bytes bestehende Sequenz 0xF0 0x9F 0x8D 0xBA codiert. Ihre UTF-16-Codierung verwendet stattdessen zwei 16-Bit-Codeeinheiten, 0xD83C 0xDF7A, ein Beispiel für ein UTF-16-Surrogate-Paar.

Konvertierung zwischen UTF-8 und UTF-16 mithilfe von Win32-APIs

Wie in den vorherigen Absätzen erörtert, wird Unicode-Text im Arbeitsspeicher eines Computers mithilfe verschiedener Bits dargestellt, basierend auf der jeweiligen Unicode-Codierung. Welche Codierung sollten Sie verwenden? Auf diese Frage gibt es keine einfache Antwort.

In letzter Zeit scheint sich ein Konsens herauszubilden, dass ein guter Ansatz darin besteht, UTF-8-codierten Unicode-Text in Instanzen der Klasse „std::string“ in plattformübergreifendem C++-Code zu speichern. Darüber hinaus besteht allgemeine Übereinstimmung darin, dass UTF-8 die Codierung der Wahl für das Austauschen von Text über Plattformgrenzen und über verschiedene Computer hinweg ist. Die Tatsache, dass UTF-8 ein hinsichtlich der Endiancharakteristik neutrales Format ist, spielt hierbei eine wichtige Rolle. In jedem Fall sind Konvertierungen zwischen UTF-8 und UTF-16 mindestens an der Win32-API-Grenze erforderlich, da die Unicode-fähigen Windows APIs UTF-16 als native Codierung verwenden.

Tauchen wir etwas in C++-Code ein, um diese Unicode UTF-8/UTF-16-Codierungskonvertierungen zu implementieren. Es gibt zwei wichtige Win32-APIs, die für diesen Zweck verwendet werden können: MultiByteToWideChar und sein symmetrischer Widerpart WideCharToMultiByte. Die erste kann verwendet werden, um aus UTF-8 („Multibyte“-Zeichenfolge in der Terminologie der API) in UTF-16 („WideChar“-Zeichenfolge) zu konvertieren; die zweite wird für den umgekehrten Weg verwendet. Da diese Win32-Funktionen ähnliche Schnittstellen und Verwendungsmuster aufweisen, lege ich in diesem Artikel den Schwerpunkt nur auf MultiByteToWideChar, ich habe aber in den Download zu diesem Artikel in C++ compilierbaren Code mit aufgenommen, der die andere API verwendet.

Verwenden von STL-Standard-Zeichenfolgenklassen zum Speichern von Unicode-Text Da dies hier ein C++-Artikel ist, besteht die berechtigte Erwartung, dass Unicode-Text in Zeichenfolgenklassen irgendeiner Art gespeichert wird. Die Frage stellt sich jetzt also so: Welche Arten von C++-Zeichenfolgenklassen können zum Speichern von Unicode-Text verwendet werden? Die Antwort hängt von der bestimmten Codierung ab, die für den Unicode-Text verwendet wird. Wenn UTF-8-Codierung verwendet wird, kann für die Darstellung jeder dieser Codeeinheiten in C++ einfach „char“ verwendet werden, da die Codierung auf 8-Bit-Codeeinheiten beruht. In diesem Fall ist die STL-Klasse „std::string“, die char-basiert ist, eine gute Option zum Speichern von UTF-8-codiertem Unicode-Text.

Wenn andererseits der Unicode-Text in UTF-16 codiert ist, wird jede Codeeinheit durch 16-Bit-Wörter dargestellt. In Visual C++ ist der wchar_t-Typ genau 16 Bit groß; daher funktioniert die STL-Klasse „std::wstring“, die wchar_t-basiert ist, problemlos zum Speichern von UTF-16-Unicode-Text.

Es sollte beachtet werden, dass der C++-Standard die Größe des wchar_t Typs nicht festlegt, und während der Visual C++-Compiler 16 Bit verwendet, hindert nichts andere C++-Compiler, andere Größen zu verwenden. Tatsächlich beträgt die Größe von wchar_t, so, wie der GNU GCC C++-Compiler den Typ unter Linux definiert, 32 Bit. Da der wchar_t-Typ auf verschiedenen Compilern und Plattformen verschiedene Größen aufweist, ist die Klasse „std::wstring“, die auf diesem Typ basiert, nicht portierbar. Anders ausgedrückt, „wstring“ kann zum Speichern von in UTF-16 codiertem Unicode-Text unter Windows mit dem Visual C++-Compiler (wo die Größe von „wchar_t“ 16 Bit beträgt) verwendet werden, aber nicht unter Linux mit dem GCC C++-Compiler, der einen wchar_t-Typ mit der abweichenden Größe von 32 Bit definiert.

Es existiert noch eine weitere Unicode-Codierung, die weniger gut bekannt ist und in der Praxis seltener verwendet wird als ihre Geschwister: UTF-32. Wie der Name eindeutig nahelegt, basiert sie auf 32-Bit-Codeeinheiten. Ein 32-bittiger wchar_t-Typ unter GCC/Linux ist also ein guter Kandidat für die UTF-32-Codierung auf der Linux-Plattform.

Die fehlende Eindeutigkeit bei der Größe von „wchar_t“ führt zu einem entsprechenden Mangel an Portierbarkeit von C++-Code, der darauf basiert (einschließlich der std::wstring-Klasse selbst). Andererseits ist „std::string“ als auf „char“ basierende Klasse portierbar. Unter praktischen Gesichtspunkten ist aber anzumerken, dass gegen die Verwendung von „wstring“ zum Speichern von UTF-16-codiertem Text in Windows-spezifischem C++-Code nichts einzuwenden ist. Tatsächlich interagieren diese Codeteile bereits mit Win32-APIs, die definitionsgemäß plattformspezifisch sind. Dieser Mischung noch „wstring“ hinzuzufügen, ändert an der Situation nichts.

Schließlich sollte angemerkt werden, dass aufgrund der Tatsache, dass UTF-8 und UTF-16 Codierungen mit variabler Länge sind, die Rückgabewerte der Methoden „string::length“ und „wstring::length“ im allgemeinen nicht mit der Anzahl der in den Zeichenfolgen gespeicherten Unicode-Zeichen (oder Codepunkte) übereinstimmt.

Die Schnittstelle der Konvertierungsfunktion Entwickeln wir eine Funktion zum Konvertieren von in UTF-8 codiertem Unicode-Text in gleichbedeutenden Text, der in UTF-16 codiert ist. Das kann z. B. dann nützlich sein, wenn Sie plattformübergreifenden C++-Code verwenden, der UTF-8-codierte Unicode-Zeichenfolgen mithilfe der STL-Klasse „std::string“ speichert und Sie diesen Text an Unicode-fähige Win32 APIs übergeben möchten, die normalerweise die UTF-16-Codierung verwenden. Da dieser Code Daten mit Win32-APIs austauscht, ist er ohnehin nicht portierbar, die Verwendung von „std::wstring“ ist also in diesem Fall für die Speicherung von UTF-16-Text geeignet. Ein möglicher Prototyp der Funktion ist:

std::wstring Utf8ToUtf16(const std::string& utf8);

Diese Konvertierungsfunktion nimmt als Eingabe eine mit Unicode UTF-8 codierte Zeichenfolge an, die in der STL-Standardklasse „std::string“ gespeichert wird. Da es sich um einen Eingabeparameter handelt, wird er als Konstantenverweis (const &) an die Funktion übergeben. Als Ergebnis der Konvertierung wird eine in UTF-16 codierte Zeichenfolge zurückgegeben, die in einer std::wstring-Instanz gespeichert ist. Allerdings können während der Konvertierung der Unicode-Codierung Fehler auftreten. Beispielsweise kann die UTF-8-Eingabezeichenfolge eine ungültige UTF-8-Sequenz enthalten (die das Ergebnis eines Programmfehlers in anderen Codeteilen oder böswilliger Aktivitäten sein kann). In derartigen Fällen ist unter Sicherheitsaspekten das beste Vorgehen, die Konvertierung für gescheitert zu erklären, statt potenziell gefährliche Bytesequenzen zu verwenden. Die Konvertierungsfunktion kann mit Fällen von ungültigen UTF-8-Eingabesequenzen so umgehen, dass sie eine C++-Ausnahme auslöst.

Definieren einer Ausnahmeklasse für Konvertierungsfehler Welche Art von C++-Klasse kann zum Auslösen einer Ausnahme für den Fall verwendet werden, dass bei einer Unicode-Codierungskonvertierung ein Fehler auftritt? Eine Option könnte die Verwendung einer bereits in der Standardbibliothek definierten Klasse sein, z. B. „std::runtime_error“. Ich ziehe es allerdings vor, für diesen Zweck eine neue, benutzerdefinierte C++-Ausnahmeklasse zu definieren, die aus „std::runtime_error“ abgeleitet wird. Wenn in Win32-APIs wie MultiByteToWideChar Fehler auftreten, können über den Aufruf von GetLastError weitere Informationen zur Fehlerursache abgerufen werden. Beispielsweise wäre ein typischer Fehlercode, der von GetLastError im Falle ungültiger UTF-8-Sequenzen in der Eingabezeichenfolge zurückgegeben wird, ERROR_NO_UNICODE_­TRANSLATION. Es ist sinnvoll, der benutzerdefinierten C++-Ausnahmeklasse diese Information hinzufügen; sie kann später zu Debugzwecken nützlich sein. Der Anfang der Definition dieser Ausnahmeklasse kann etwa wie folgt aussehen:

// utf8except.h
#pragma once
#include <stdint.h>   // for uint32_t
#include <stdexcept>  // for std::runtime_error
// Represents an error during UTF-8 encoding conversions
class Utf8ConversionException
  : public std::runtime_error
{
  // Error code from GetLastError()
  uint32_t _errorCode;

Beachten Sie, dass der von GetLastError zurückgegebene Wert vom Typ DWORD ist und eine Ganzzahl ohne Vorzeichen mit 32 Bit Länge darstellt. DWORD ist aber eine nicht portierbare, Win32-spezifische Datentypenvereinbarung mittels typedef. Selbst wenn diese C++-Ausnahmeklasse von Win32-spezifischen Anteilen von C++-Code ausgelöst wird, kann sie von plattformübergreifendem C++-Code abgefangen werden! Es ist also sinnvoll, portierbare typedefs anstelle von Win32-spezifischen zu verwenden; „uint32_t“ stellt ein Beispiel für solche Typen dar.

Als nächstes kann ein Konstruktor definiert werden, um Instanzen dieser benutzerdefinierten Ausnahmeklasse mit einer Fehlermeldung und einem Fehlercode zu initialisieren:

public:
  Utf8ConversionException(
    const char* message,
    uint32_t errorCode
  )
    : std::runtime_error(message)
    , _errorCode(errorCode)
  { }

Schließlich kann ein öffentlicher Getter definiert werden, um schreibgeschützten Zugriff auf den Fehlercode zu bieten:

uint32_t ErrorCode() const
  {
    return _errorCode;
  }}; // Exception class

Da diese Klasse von „std::runtime_error“ abgeleitet ist, kann die what-Methode zum Abrufen der im Konstruktor übergebenen Fehlermeldung verwendet werden. Beachten Sie, dass in der Definition dieser Klasse nur portierbare Standardelemente verwendet wurden, daher kann diese Klasse in plattformübergreifenden Teilen von C++-Code einwandfrei genutzt werden, selbst wenn diese weit vom Windows-spezifischen Auslösepunkt entfernt sind.

Konvertierung von UTF-8 in UTF-16: MultiByteToWideChar in der Praxis

Da wir jetzt den Prototyp der Konvertierungsfunktion definiert und eine benutzerdefinierte C++-Ausnahmeklasse implementiert haben, um Fehler bei der UTF-8-Konvertierung ordnungsgemäß darzustellen, geht es jetzt darum, den Hauptteil der Konvertierungsfunktion zu entwickeln. Wie bereits zuvor erläutert, kann die Konvertierungsarbeit von UTF-8 in UTF-16 mithilfe der MultiByte­ToWideChar-Win32-API ausgeführt werden. Die Ausdrücke „multi-byte“ und „wide-char“ haben historische Wurzeln. Ursprünglich waren diese API und ihr symmetrischer Zwilling WideCharToMultiByte für die Konvertierung von Text, der in bestimmten Codepages gespeichert ist, und Unicode-Text vorgesehen, der die UTF-16-Codierung in Unicode-fähigen Win32-APIs verwendet. „Wide char“ bezieht sich auf „wchar_t“, ist also einer auf „wchar_t“ basierenden Zeichenfolge zugeordnet, bei der es sich um eine UTF-16-codierte Zeichenfolge handelt. Im Gegensatz dazu ist eine Multibyte-Zeichenfolge eine in einer Codepage ausgedrückte Bytesequenz. Das Legacykonzept der Codepage wurde anschließend auf die UTF-8-Codierung ausgeweitet.

Ein typisches Verwendungsmuster dieser API besteht darin, zunächst Multi­ByteToWideChar aufzurufen, um die Größe der Ergebniszeichenfolge abzurufen. Anschließend wird ein Zeichenfolgenpuffer zugeordnet, der diesem Größenwert entspricht. Dies erfolgt normalerweise mithilfe der Methode „std::wstring::resize“, falls es sich bei dem Ziel um eine UTF-16-Zeichenfolge handelt. (Weitere Details können Sie in meinem Artikel aus Juli 2015, „C++ - Verwendung von STL Strings an den Begrenzungen der Win32-API” (Using STL Strings at Win32 API Boundaries), unter msdn.com/magazine/mt238407 finden.) Schließlich wird die MultiByteToWideChar-Funktion ein zweites Mal aufgerufen, um die eigentliche Codierungskonvertierung mithilfe des zuvor zugewiesenen Zielzeichenfolgen-Puffers auszuführen. Beachten Sie, dass das gleiche Verwendungsmuster für die symmetrische WideCharToMultiByte-API gilt.

Implementieren wir also dieses Muster in C++-Code, im Hauptteil der benutzerdefinierten Konvertierungsfunktion „Utf8ToUtf16“. Wir beginnen mit der Behandlung des Sonderfalls einer leeren Eingabezeichenfolge, für die nur eine leere wstring-Ausgabezeichenfolge zurückgegeben wird:

#include <Windows.h> // For Win32 APIs
#include <string>    // For std::string and std::wstring
std::wstring Utf8ToUtf16(const std::string& utf8)
{
  std::wstring utf16; // Result
  if (utf8.empty())
  {
    return utf16;
  }

Konvertierungsflags MultiByteToWideChar kann zum ersten Mal aufgerufen werden, um die Größe der UTF-16-Zielzeichenfolge abzurufen. Diese Win32-Funktion weist eine relativ komplexe Schnittstelle auf, und ihr Verhalten wird durch einige Flags definiert. Da diese API innerhalb des Hauptteils der Utf8ToUtf16-Konvertierungsfunktion zweimal aufgerufen wird, ist es aus Gründen der Lesbarkeit und Wartungsfreundlichkeit des Codes sinnvoll, eine benannte Konstante zu definieren, die in beiden Aufrufen verwendet werden kann:

// Safely fails if an invalid UTF-8 character
// is encountered in the input string
constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;

Unter Sicherheitsaspekten ist es ferner eine bewährte Verfahrensweise, einen Fehler für den Konvertierungsprozess auszugeben, wenn in der Eingabezeichenfolge eine ungültige UTF-8-Sequenz erkannt wird. Die Verwendung des Flags MB_ERR_INVALID_CHARS wird auch in Michael Howard und David LeBlancs Buch „Writing Secure Code, Second Edition“ (Microsoft Press, 2003) empfohlen.

Wenn Ihr Projekt eine ältere Version des Visual C++-Compilers verwendet, die das Schlüsselwort „constexpr“ nicht unterstützt, können Sie in diesem Kontext ersatzweise „static const“ verwenden.

Länge von Zeichenfolgen und sichere Konvertierung aus „size_t“ in „int“ MultiByteToWideChar erwartet, dass der Parameter für die Länge der Eingabezeichenfolge als Typ „int“ ausgedrückt wird, während die Methode „length“ der STL-Zeichenfolgenklassen einen Wert eines zu „size_t“ äquivalenten Typs zurückgibt. In 64-Bit-Builds gibt der Visual C++-Compiler eine Warnung aus, die auf einen potenziellen Datenverlust bei der Konvertierung von „size_t“ (dessen Größe 8 Byte beträgt) in „int“ (dessen Größe 4 Byte beträgt) hinweist. Aber selbst in 32-Bit-Builds, bei denen sowohl „size_t“ als auch „int“ vom Visual C++-Compiler als 32-Bit-Ganzzahlen definiert sind, besteht hinsichtlich des Vorzeichens keine Übereinstimmung: „size_t“ trägt kein Vorzeichen, während „int“ ein Vorzeichen aufweist. Das ist bei Zeichenfolgen üblicher Länge kein Problem, aber bei wirklich gigantischen Zeichenfolgen mit einer Länge von mehr als (231-1) – also einer Größe von mehr als 2 Milliarden Bytes – kann bei der Konvertierung einer Ganzzahl ohne Vorzeichen (size_t) in eine vorzeichenbehaftete Ganzzahl (int) eine negative Zahl erzeugt werden, und negative Längen ergeben keinen Sinn.

Statt also einfach „utf8.length“ aufzurufen, um die Länge der UTF-8-Quell-Eingabezeichenfolge abzurufen und sie an die MultiByteTo­WideChar-API zu übergeben, ist es besser, den tatsächlichen Längenwert von „size_t“ zu überprüfen, um sicherzustellen, dass er sicher und bedeutungsvoll in einen „int“ konvertiert werden kann, und ihn erst dann an die MultiByteToWideChar-API zu übergeben.

Der folgende Code kann verwendet werden, um zu überprüfen, ob die Länge von „size_t“ den Maximalwert für eine Variable vom Typ „int“ überschreitet und eine Ausnahme auszulösen, wenn das der Fall ist:

if (utf8.length() > static_cast<size_t>(std::numeric_limits<int>::max()))
{
  throw std::overflow_error(
    "Input string too long: size_t-length doesn't fit into int.");
}

Beachten Sie die Verwendung der Klassenvorlage „std::numeric_limits“ (aus dem <limits> C++-Standardheader) zum Abfragen des größten möglichen Werts für den Typ „int“. Allerdings lässt sich dieser Code möglicherweise nicht compilieren. Wie das? Das Problem liegt in der Definition der min- und max-Makros in den Headern des Windows-Plattform-SDKs. Insbesondere die Windows-spezifische Definition des max-Präprozessormakros steht mit dem Aufruf der std::numeric_limits<int>::max-Memberfunktion im Konflikt. Es gibt eine Reihe von Möglichkeiten, das zu verhindern.

Eine mögliche Lösung besteht im #define von NOMINMAX vor dem Einbinden von <Windows.h> per #include. Dies verhindert die Definition der Windows-spezifischen Präprozessormakros „min“ und max“. Allerdings kann das Verhindern der Definition dieser Makros zu Problemen mit anderen Windows-Headern führen, wie etwa <gdiplus.h>, die die Definitionen dieser Windows-spezifischen Makros benötigen.

Eine weitere Option besteht in der Verwendung eines zusätzlichen Paars Klammern um den Aufruf der Memberfunktion „std::numeric_limits::max“, um die zuvor erwähnte Makroerweiterung zu verhindern:

if (utf8.length() > static_cast<size_t>((std::numeric_limits<int>::max)()))
{
  throw std::overflow_error(
    "Input string too long: size_t-length doesn't fit into int.");
}

Darüber hinaus kann als Alternative die Konstante INT_MAX anstelle der C++-Klassenvorlage „std::numeric_limits“ verwendet werden.

Gleich, welcher Ansatz verwendet wird, sobald die Größenüberprüfung erledigt ist und der Längenwert als für eine Variable vom Typ „int“ passend erkannt wurde, kann die Umwandlung von „size_t“ in „int“ mithilfe von „static_cast“ sicher ausgeführt werden:

// Safely convert from size_t (STL string's length)
// to int (for Win32 APIs)
const int utf8Length = static_cast<int>(utf8.length());

Beachten Sie, dass die Länge der UTF-8-Zeichenfolge in 8-Bit-Zeicheneinheiten gemessen wird, also in Byte.

Erster API-Aufruf: Abrufen der Länge der Zielzeichenfolge Jetzt kann MultiByteToWideChar zum ersten Mal aufgerufen werden, um die Länge der UTF-16-Zielzeichenfolge abzurufen:

const int utf16Length = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of the source UTF-8 string, in chars
  nullptr,       // Unused - no conversion done in this step
  0              // Request size of destination buffer, in wchar_ts
);

Beachten Sie, dass beim Aufruf der Funktion null als letztes Argument übergeben wird. Dies weist die MultiByteToWideChar-API an, nur die angeforderte Größe der Zielzeichenfolge zurückzugeben; in diesem Schritt erfolgt keine Konvertierung. Beachten Sie ferner, dass die Größe der Zielzeichenfolge in „wchar_ts“ (nicht in 8-Bit-chars) ausgedrückt ist, was sinnvoll ist, da die Zielzeichenfolge eine UTF-16-codierte Unicode-Zeichenfolge ist, die aus 16-Bit-Sequenzen von „wchar_ts“ besteht.

Um schreibgeschützten Zugriff auf den Inhalt des UTF-8 „std::string“ zu erhalten, wird die Methode „std::string::data“ aufgerufen. Da die Länge der UTF-8-Zeichenfolge explizit als Eingabeparameter übergeben wird, funktioniert dieser Code auch für Instanzen von „std::string“, die eingebettete NUL-Zeichen enthalten.

Beachten Sie auch die Verwendung der CP_UTF8-Konstante, um anzugeben, dass die Eingabezeichenfolge in UTF-8 codiert ist.

Behandlung des Fehlerfalls Wenn bei dem vorhergehenden Funktionsaufruf ein Fehler auftritt, beispielsweise durch die Gegenwart ungültiger UTF-8-Sequenzen in der Eingabezeichenfolge, gibt die MultiByteToWideChar-API null zurück. In diesem Fall kann die Win32-Funktion „GetLast­Error“ aufgerufen werden, um weitere Details über die Fehlerursache abzurufen. Ein typischer Fehlercode, der im Fall ungültiger UTF-8-Zeichen zurückgegeben wird, ist ERROR_NO_UNICODE_TRANSLATION.

Beim Vorliegen eines Fehlers ist es nun an der Zeit, eine Ausnahme auszulösen. Dies kann eine Instanz der zuvor benutzerdefiniert entworfenen Klasse „Utf8Conversion­Exception“ sein:

if (utf16Length == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot get result string length when converting " \
    "from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
    error);
}

Zuweisen von Arbeitsspeicher für die Zielzeichenfolge Bei einem Erfolg des Win32-Funktionsaufrufs wird die erforderliche Länge der Zielzeichenfolge in der lokalen Variablen „utf16Length“ gespeichert, sodass der Zielarbeitsspeicher für die UTF-16-Ausgabezeichenfolge zugeordnet werden kann. Für UTF-16-Zeichenfolgen, die in Instanzen der Klasse „std::wstring“ gespeichert sind, wäre ein einfacher Aufruf der Methode „resize“ völlig ausreichend:

utf16.resize(utf16Length);

Beachten Sie, dass aufgrund der Tatsache, dass die Länge der UTF-8-Eingabezeichenfolge explizit an MultiByteToWideChar übergeben wurde (statt nur -1 zu übergeben und die API aufzufordern, die gesamte Eingabezeichenfolge zu scannen, bis ein NUL-Terminator gefunden wird), die Win32-API der resultierenden Zeichenfolge keinen zusätzlichen NUL-Terminator mehr anfügt: Die API verarbeitet einfach nur die exakte Anzahl Zeichen in der Eingabezeichenfolge, die durch den explizit übergebenen Längenwert angegeben wird. Daher besteht kein Anlass, „std::wstring::resize“ mit dem Wert „utf16Length + 1“ aufzurufen: Da von der Win32-API kein zusätzlicher NUL-Terminator eingebracht wird, muss für diesen im Ziel-std::wstring kein Platz reserviert werden (weitere Details dazu finden Sie in meinem Artikel aus Juli 2015).

Zweiter API-Aufruf: Ausführen der eigentlichen Konvertierung Da die UTF-16-wstring-Instanz jetzt über ausreichend Platz verfügt, um den resultierenden UTF-16-codierten Text aufzunehmen, ist jetzt der Zeitpunkt gekommen, MultiByteToWideChar ein zweites Mal aufzurufen, um die konvertierten Bits in die Zielzeichenfolge einzusetzen:

// Convert from UTF-8 to UTF-16
int result = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of source UTF-8 string, in chars
  &utf16[0],     // Pointer to destination buffer
  utf16Length    // Size of destination buffer, in wchar_ts          
);

Beachten Sie die Verwendung der Syntax „&utf16[0]“, um Schreibzugriff auf den internen Speicherpuffer des std::wstrings zu erhalten (auch das wurde bereits in meinem Artikel aus Juli 2015 behandelt).

Wenn der erste Aufruf von MultiByteToWideChar erfolgreich war, ist es unwahrscheinlich, dass bei diesem zweiten Aufruf ein Fehler auftritt. Trotzdem stellt das Überprüfen des API-Rückgabewerts natürlich eine bewährte Praxis dar, wenn es um sicheren Code geht:

if (result == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot convert from UTF-8 to UTF-16 "\
    "(MultiByteToWideChar failed).",
    error);
}

Andernfalls, bei Erfolg, kann die resultierende UTF-16-Zeichenfolge endlich an den Aufrufer übergeben werden:

 

return utf16;
} // End of Utf8ToUtf16

Syntaxbeispiel Wenn Sie also eine UTF-8-codierte Unicode-Zeichenfolge (beispielsweise aus plattformübergreifendem C++-Code) an eine Unicode-fähige Win32-API übergeben möchten, kann diese benutzerdefinierte Konvertierungsfunktion in dieser Weise aufgerufen werden:

std::string utf8Text = /* ...some UTF-8 Unicode text ... */;
// Convert from UTF-8 to UTF-16 at the Win32 API boundary
::SetWindowText(myWindow, Utf8ToUtf16(utf8Text).c_str());
// Note: In Unicode builds (Visual Studio default) SetWindowText
// is expanded to SetWindowTextW

Die Utf8ToUtf16-Funktion gibt eine wstring-Instanz zurück, die die UTF-16-codierte Zeichenfolge enthält, und die c_str-Methode wird für diese Instanz aufgerufen, um einen raw-Zeiger in C-Art auf eine NUL-terminierte Zeichenfolge zu erhalten, die an Unicode-fähige Win32-APIs übergeben werden kann.

Für die umgekehrte Konvertierung von UTF-16 in UTF-8 kann sehr ähnlicher Code erstellt werden, wobei diesmal die WideCharToMultiByte-API aufgerufen wird. Wie bereits zuvor angemerkt, erfolgen Unicode-Konvertierungen zwischen UTF-8 und UTF-16 verlustfrei – im Konvertierungsprozess gehen keine Zeichen verloren.

Unicode-Codierungskonvertierungs-Bibliothek

Das herunterladbare Archiv zu diesem Artikel enthält compilierbaren C++-Beispielcode. Dabei handelt es sich um wiederverwendbaren Code, der sowohl in 32-Bit- als auch in 64-Bit-Builds sauber bei Visual C++-Warnstufe 4 (/W4) compiliert werden kann. Er ist als C++-Bibliothek implementiert, deren Einbindung nur über den Header erfolgt. Im Wesentlichen besteht dieses Modul zur Unicode-Codierungskonvertierung aus zwei Headerdateien: „utf8except.h“ und „utf8conv.h“. Die erste enthält die Definition einer C++-Ausnahmeklasse, die zum Melden von Fehlerbedingungen während der Unicode-Codierungskonvertierungsvorgänge dient. Die zweite implementiert die eigentlichen Funktionen zur Konvertierung der Unicode-Codierung.

Beachten Sie, dass „utf8except.h“ nur plattformübergreifenden C++-Code enthält, was es möglich macht, die UTF-8-Kodierungskonvertierungs-Ausnahme in C++-Projekten an beliebiger Stelle abzufangen, einschließlich Codeteilen, die nicht Windows-spezifisch sind, sondern stattdessen entwurfsbedingt plattformübergreifendes C++ verwenden. Im Gegensatz dazu enthält „utf8conv.h“ Windows-spezifischen C++-Code, was aufgrund der direkten Interaktion mit der Win32-API-Grenze erforderlich ist.

Um diesen Code in Ihren Projekten zu verwenden, müssen Sie lediglich die genannten Headerdateien per #include einbinden. Das herunterladbare Archiv enthält eine zusätzliche Quelldatei, mit der außerdem einige Testfälle implementiert werden.

Zusammenfassung

Unicode ist der De-Facto-Standard für das Darstellen von internationalem Text in moderner Software. Unicode-Text kann in verschiedenen Formaten codiert werden: Die zwei wichtigsten sind UTF-8 und UTF-16. In C++-Windows-Code ergibt sich häufig die Notwendigkeit, zwischen UTF-8 und UTF-16 zu konvertieren, da Unicode-fähige Win32-APIs UTF-16 als native Unicode-Codierung verwenden. UTF-8-Text kann komfortabel in Instanzen der STL-Klasse „std::string“ gespeichert werden, während „std::wstring“ sich gut zum Speichern von UTF-16-codiertem Text in Windows C++-Code eignet, der für den Visual C++-Compiler bestimmt ist.

Die Win32-APIs MultiByteToWideChar und WideCharTo­MultiByte können für das Ausführen von Konvertierungen zwischen Unicode-Text verwendet werden, der in den Codierungen UTF-8 und UTF-16 vorliegt. Ich habe eine detaillierte Beschreibung des Verwendungsmusters der MultiByteTo­WideChar-API gegeben und sie mit einer modernen C++-Hilfsfunktion umschlossen, um Konvertierungen von UTF-8 in UTF-16 auszuführen. Die umgekehrte Konvertierung folgt einem sehr ähnlichen Muster, und wiederverwendbarer C++-Code zu ihrer Konvertierung steht im Download zu diesem Artikel zur Verfügung.


Giovanni Dicanio ist Computerprogrammierer mit den Spezialgebieten C++ und Windows, Pluralsight-Autor und Visual C++-MVP. Abgesehen von Programmieren und dem Verfassen von Kursen, hilft er gerne anderen in Foren und Communitys mit dem Schwerpunkt C++. Sie erreichen ihn unter giovanni.dicanio@gmail.com. Er schreibt außerdem einen Blog unter blogs.msmvps.com/gdicanio.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: David Cravey und Marc Gregoire
David Cravey ist Unternehmensarchitekt bei GlobalSCAPE, führt verschiedene C++-Benutzergruppen und ist ein viermaliger Visual C++-MVP.

Marc Gregoire ist ein leitender Softwareentwickler aus Belgien, der Gründer der belgischen C++-Benutzergruppe, Autor von „Professional C++“ (Wiley), Koautor von „C++ Standard Library Quick Reference“ (Apress), technischer Redakteur bei einer Vielzahl von Büchern und hat seit 2007 die jährliche MVP-Auszeichnung für seine umfassenden VC++-Kenntnisse empfangen. Sie können Marc unter marc.gregoire@nuonsoft.com erreichen.