communityWir suchen ständig neue Tutorials und Artikel! Habt ihr selbst schonmal einen Artikel verfasst und seid bereit dieses Wissen mit der Community zu teilen? Oder würdet ihr gerne einmal über ein Thema schreiben das euch besonders auf dem Herzen liegt? Dann habt ihr nun die Gelegenheit eure Arbeit zu veröffentlichen und den Ruhm dafür zu ernten. Schreibt uns einfach eine Nachricht mit dem Betreff „Community Articles“ und helft mit das Angebot an guten Artikeln zu vergrößern. Als Autor werdet ihr für den internen Bereich freigeschaltet und könnt dort eurer literarischen Ader freien Lauf lassen.

CString Management - VARIANT, STRINGTABLE und temporäre Objekte Drucken E-Mail
Benutzerbewertung: / 9
SchwachPerfekt 
Geschrieben von: StarShaper   
Dienstag, den 20. September 2005 um 18:30 Uhr
Beitragsseiten
CString Management
CString in char *
VARIANT, STRINGTABLE und temporäre Objekte
std::string und Effizienzbetrachtungen
Alle Seiten

VARIANT in CString

Tatsächlich habe ich dies bisher noch nicht gemacht. Ich arbeite nicht in COM/OLE/ActiveX wo das ein Thema ist. Aber ich habe ein Posting von Robert Quirk in der microsoft.public.vc.mfc Newsgroup gelesen wie es funktioniert. Zudem wäre es blöd dies nicht in das Tutorial zu schreiben. Deshalb hier nun die Anleitung mit ausführlicher Erklärung. Jegliche Fehler im Vergleich zum Original nehme ich auf meine Kappe.

Ein VARIANT ist ein generischer Parameter/Rückgabewert in der COM Programmierung. Sie können Methoden schreiben welche einen VARIANT Typ zurückgeben. Welchen Typ die Funktion zurückgibt kann (und das tut sie oft) von den Input Parametern der Methode abhängen (z.B. in der Automatisation, anhängig davon welche Methode aufgerufen wird, gibt IDispatch::Invoke (via einer ihrer Parameters) einen VARIANT welcher ein BYTE enthält, ein WORD, ein float, ein double, ein Datum, ein BSTR und ungefähr drei Dutzend weitere Typen zurück. Schauen sie sich dazu die Spezifikationen eines VARIANT Typs in der MSDN an. In dem unteren Beispiel wird angenommen das der Typ als VARIANT vom Typ BSTR bekannt ist, was bedeutet das der Wert im string welcher durch bstrVal referenziert wird gefunden werden kann. So wird der Vorteil ausgenutzt das die Tatsache das es einen Konstruktor gibt welcher in einer ANSI Anwendung einen Wert, mit einer Referenz eines LPCWCHAR auf einen CString, konvertiert. Im Unicode Modus ist das der nomale CString Konstruktor. Schauen sie sich die Vorbahalte zu der Standard ::WideCharToMultibyte Konvertierung an und machen sie es für sich aus ob dies für sie akzeptabel ist oder nicht. Meistens, ist es ok.

VARIANT vaData;
 
vaData = m_com.YourMethodHere();
ASSERT(vaData.vt == VT_BSTR);
 
CString strData(vaData.bstrVal);

Beachten sei das sie auch eine generischere (vielfältigarer wiederverwendbar) Umwandlungs-Routine, welche das vt Feld fallspezifisch mit einbezieht, schreiben können. In diesem Fall ist der folgende Code vielleicht etwas für sie:

CString VariantToString(VARIANT *va)
{
    CString s;
    switch(va->vt)
      { /* vt */
       case VT_BSTR:
          return CString(vaData->bstrVal);
       case VT_BSTR | VT_BYREF:
          return CString(*vaData->pbstrVal);
       case VT_I4:
          s.Format(_T("%d"), va->lVal);
          return s;
       case VT_I4 | VT_BYREF:
          s.Format(_T("%d"), *va->plVal);
       case VT_R8:
          s.Format(_T("%f"), va->dblVal);
          return s;
       ... die übrigen cases wurden als kleine Übung für sie weggelassen
       default:
          ASSERT(FALSE); // unbekannter VARIANT Typ (dieses ASSERT ist optional)
          return CString("");
      } /* vt */
}

Laden von STRINGTABLE Werten

Wenn sie ein Programm schreiben möchten welches leicht in andere Sprachen zu konvertieren sein soll, dürfen sie nicht native Sprach-Strings in ihrem Quellcode benutzen. Zum Beispiel wäre das in meinem Fall Englisch, weil das meine Muttersprache ist - aber ich kann auch ein wenig Deutsch sprechen. Wie auch immer. Es ist nicht gut diese strings an den Quellcode zu binden, so wie in dem folgenden Beispiel.

CString s = "There is an error";

Stattdessen sollten sie alle sprachspezifischen strings in eine seperate Resourcen Datei packen (außer vielleicht die strings zum Debuggen. Aber diese sollten in einem fertigen Programm sowieso nicht mehr enthalten sein). Deshalb solle man dies so ähnlich in ein Programm schreiben:

s.Format(_T("%d - %s"), code, text);

Der nachfolgende literale string ist nicht sprachsensitiv. Wie auch immer. Sie sollten sehr vorsichtig sein und strings wie diesen nicht benutzen.

// fmt ist "Error in %s file %s"
// readorwrite ist "reading" oder "writing"
s.Format(fmt, readorwrite, filename);

Ich spreche hier aus eigener Erfahrung. Ich beging diesen Fehler in meiner ersten internationalisierten Anwendung. Trotz der Tatsache das ich Deutsch kann und weiß das ein Verb im Deutschen im Gegensatz zum Englischen an das Ende eines Satzes gestellt wird. Unser deutscher Verleger beschwerte sich lautstark darüber das er verwirrende deutsche Fehlernachrichten schreiben müsse damit der formatierte Code das machte was er sollte.

Es ist viel besser (und das mache ich heute so) zwei strings zu haben. Einen für das Lesen und einen für das Schreiben und anschließend den entsprechenden zu laden. Dies erreicht man indem man sie string-parameter-insensitiv gestaltet, was bedeutet das ganze Format anstatt die einzelnen strings "reading" oder "writing" zu laden.

// fmt is "Error in reading file %s"
// "Error in writing file %s"
s.Format(fmt, filename);

Beachten sie das wenn sie mehr als eine Ersetzung haben, sie darauf achten müssen das die Reihenfolge der Wörter, also Nomen - Verb oder Verb - Nomen usw. keine Rolle spielt.

Ich werde vorerst nicht über FormatMessage sprechen, welches derzeit besser ist als sprintf/Format, aber schlecht in die CString Klasse implementiert wurde. FormatMessage löst das obige Problem indem es die Parameter anhand der Position in der Parameterliste erkennt und es erlaubt diese im Output string neu anzuordnen.

Aber wie wollen wir das nun erreichen? Indem wir wie oben schon einmal kurz erwähnt die strings in der Resourcen Datei, auch bekannt als STRINGTABLE in den Resourcen Segmenten, speichern. Um das zu tun, müssen sie zuerst einmal einen string mit dem Visual Studio Resourcen Editor erzeugen. Dem string wird eine string ID zugeordnet, normalerweise beginnend mit IDS_. Somit haben sie eine Nachricht, einen string wessen ID IDS_READING_FILE ist und einen anderen string mit der ID IDS_WRITING_FILE. Diese erscheinen in ihrer .rc Datei als:

STRINGTABLE
    IDS_READING_FILE "Reading file %s"
    IDS_WRITING_FILE "Writing file %s"
END

Achtung: Diese Resourcen werden immer als Unicode strings gespeichert, unabhängig davon wie ihr Programm kompiliert wird. Sie sind sogar auf Win9x Plattformen Unicode strings, obwohl dort normalerweise Unicode strings nicht unterstützt werden. Das gilt aber eben nicht für die Resourcen! Gehen sie nun zu der Stelle wo die strings von ihnen gespeichert wurden

// vorheriger code
CString fmt;
   if(...)
      fmt = "Reading file %s";
   else
      fmt = "Writing file %s";
...
// viel später
CString s;
s.Format(fmt, filename);

und schreiben stattdessen das

// überarbeiteter code
CString fmt;
   if(...)
      fmt.LoadString(IDS_READING_FILE);
   else
      fmt.LoadString(IDS_WRITING_FILE);
...
// viel später
CString s;
s.Format(fmt, filename);

Nun können sie ihren Code in jede Sprache konvertieren. Die LoadString Methode nimmt eine string ID auf und erhält den für diese ID stellvertretenden STRINGTABLE Wert. Anschließend wird dieser dem CString zugeordnet.

Es gibt ein kleveres Feature des CString Konstruktors welcher die Benutzung von STRINGTABLE Einträgen vereinfacht. Es ist nicht explizit in den CString::CString Spezifikationen dokumentiert, aber das Beispiel zur Benutzung des Konstruktors deutet es vage an. Warum es nicht Teil der offizellen Dokumentation ist weiß ich nicht. Das Feature ist, dass wenn sie eine STRINGTABLE ID in ein LPCTSTR casten implizit ein LoadString ausgeführt wird. Daher erreicht man mit den folgenden zwei Beispielen einen string zu generieren denselben Effekt und das ASSERT wird bei der Kompilierung im Debug Modus nicht auslösen.

CString s;
s.LoadString(IDS_WHATEVER);
CString t( (LPCTSTR)IDS_WHATEVER);
ASSERT(s == t);

Sie fragen sich nun vielleicht wie das überhaupt funktionieren kann?! Wie kann es einen gültigen Pointer von einer STRINGTABLE ID bekommen? Ganz einfach. Alle string ID's liegen in einem Bereich zwischen 1 und 65536. Das bedeutet das die höherwertigen Bits des Pointers 0 sind. Zumindest auf gängigen 32-Bit Systemen auf denen ein Pointer 4 Byte groß ist. Klingt gut, aber was wenn ich Daten in einer sehr kleinen Adresse im RAM habe? Nun, die Antwort lautet schlicht und ergreifend. Das gibt es nicht! Derart niedrige verwendete Adressen in diesem Bereich existieren einfach nicht. Jeder Versuch auf Daten in diesem Adressbereich zwischen 0x00000001 bis 0x0000FFFF (1...65535) zuzugreifen wird immer in einem Zugriffsfehler enden. Diese Adressen sind in keinem Fall, zu keiner Zeit gültige Adressen. Deshalb muss ein Wert in diesem Bereich eine STRINGTABLE ID sein.

Ich tendiere dazu das MAKEINTRESOURCE Makro für Castings zu benutzen. Ich denke das lässt den Code klarer erscheinen. Es handelt sich um ein Standard-Makro welches ansonsten in der MFC nicht viel Anwendung findet. Sie haben vielleicht bemerkt das viele Methoden entweder einen UINT oder einen LPCTSTR als Parameter annehmen, indem die C++ Überladungen genutzt werden. Das macht es unnötig, wie in C explizite Casts bei überladenen Methoden (in C sowieso nicht wirklich überladen) benutzen zu müssen. Das ist auch nützlich bei der Zuweisung von Resourcen Namen an verschiedene andere Strukturen.

CString s;
s.LoadString(IDS_WHATEVER);
CString t( MAKEINTRESOURCE(IDS_WHATEVER));
ASSERT(s == t);

Ich sage es ihnen nebenbei hier noch einmal. Ich praktiziere das was ich ihnen hier erzähle in meinen eigenen Projekten. Sie werden selten, wenn überhaupt, einen literalen string in meinen Programmen finden mit Ausnahme von gelegentlichen Debug Meldungen und natürlich jeglichen sprachunabhängigen strings.

CStrings und temporäre Objekte

Hier ein kleines Problem welches vor einer Weile in der microsoft.public.vc.mfc Newsgroup auftauchte. Ich werde es ein wenig vereinfachen. Das grundlegende Problem bestand darin das der Programmierer einen string in die Registry schreiben wollte. Nachfolgend möchte ich ihnen zwei Beiträge aus diesem Thread zeigen. Der erste stammt vom Fragesteller selbst und der zweite Beitrag von einem User welcher eine Lösung für das Problem aufzeigt:

Hallo,

ich versuche einen Registry Wert einzutragen indem ich RegSetValueEx() benutze und es ist gleichzeitig der Wert der mir auch Probleme bereitet. Wenn ich eine Variable vom Typ char[] deklariere funktioniert es ohne Probleme. Wie auch immer, ich versuche von einem CString zu konvertieren und ich erhalte nur Müll. Um genau zu sein "ÝÝÝÝ...ÝÝÝÝÝÝ". Ich habe es auch mit GetBuffer, Typumwandlung in char* und LPCSTR versucht. Die Rückgabe von GetBuffer (Debuginfo) ist der korrekte string aber wenn ich es einem char* zuweisen will (oder einem LPCSTR) ist es Datenmüll. Nachfolgend ein Teil meines Codes:

    CString Name = GetName();
    RegSetValueEx(hKey, _T("Name"), 0, REG_SZ, 
                        (CONST BYTE *) (LPCTSTR)Name,
                        (Name.GetLength() + 1) * sizeof(TCHAR));

Der Name des strings ist kleiner als 20 chars, so dass ich nicht denke das der GetBuffer Parameter schuld ist. Es ist sehr frustrierend! Ich freue mich über jede Hilfe.


Lieber Frustrierte,

sie haben sich in eine sehr subtile Fehlersituation hineinmanövriert, indem sie versucht haben etwas zu klever zu sein. Sie wissen einfach zu viel und das ist ihnen zum Verhängnis geworden. Der korrekte Code steht unten:

    CString Name = GetName();
    RegSetValueEx(hKey, _T("Name"), 0, REG_SZ, 
                        (CONST BYTE *) (LPCTSTR)Name,
                        (Name.GetLength() + 1) * sizeof(TCHAR));

Hier nun warum mein Code funktioniert und ihrer nicht. Als ihre Funktion GetName einen CString zurückgab, gab sie ein "temporäres Objekt" zurück. Mehr dazu im C++ Reference Manual §12.2.

...In einigen Situationen kann es notwendig oder von Vorteil sein das der Compiler ein temporäres Objekt erzeugt. So eine Einführung in Temporäres ist abhängig von der Implementierung. Wenn ein Compiler ein temporäres Objekt von einer Klasse die einen Konstruktor besitzt einführt, muss es sicher stellen das ein Konstrukt für das temporäre Objekt aufgerufen wird. Genauso muss für ein temporäres Objekt von einer Klasse in welcher ein Destruktor deklariert wurde ein Destrukt aufgerufen werden.

Der Compiler muss sicher stellen dass das temporäre Objekt zerstört wird. Der exakte Punkt der Zerstörung ist abhängig von der Implementierung. Diese Zerstörung muss noch vor dem Ende des Scopes, in welchem das temporäre Objekt erzeugt wurde, stattfinden...

Die meisten Compiler implementieren den impliziten Destruktor für ein temporäres Objekt an dem nächsten Programm-Sequenz-Punkt welcher seiner Erzeugung folgt, das ist normalerweise das nächste Semikolon. Daher existierte der CString als der GetBuffer Aufruf erfolgte, aber wurde mit dem darauffolgenden Semikolon zerstört. (Nebenbei gibt es keinen Grund ein Argument an GetBuffer zu übergeben und außerdem ist der Code so wie er steht nicht richtig, weil kein ReleaseBuffer durchgeführt wurde). So gab GetBuffer einen Pointer für den Text des CStrings zurück. Als der Destruktor dann beim Semikolon aufgerufen wurde, wurde das eigentliche CString Objekt freigegeben zusammen mit dem allokierten Speicher. Der MFC Debug Speicher Allokator beschreibt diesen anschließend wieder mit 0xDD, was das Symbol für "Ý" ist. Zu dem Zeitpunkt als sie zur Registry geschrieben haben wurde der string Inhalt zerstört.

Es gibt keinen besonderen Grund das Ergebnis sofort in einen char* zu casten. Es als CString zu speichern bedeutet das eine Kopie des Ergebnisses gemacht wird, so dass nachdem der temporäre CString zerstört wurde, der string trotzdem in der CString Variable existiert. Das Casting zum Zeitpunkt an dem der Registry Aufruf erfolgt ist ausreichend um den Wert des strings welcher ohnehin schon existiert zu erhalten.

Mein Code ist zusätzlich Unicode unterstützend. Der Registry Aufruf verlangt eine Byteanzahl. Beachten sie außerdem das der Aufruf lstrlen(Name+1) einen Wert zurückgibt welcher um den Faktor 2 kleiner ist als ein ANSI string. Was sie wahrscheinlich versucht haben zu schreiben war lstrlen(Name) + 1 (OK, ich gebe es zu, ich habe auch denselben Fehler gemacht). Wie auch immer, im Unicode, in welchem alle Character 2 Byte groß sind, müssen wir damit zurechtkommen. Die Microsoft Dokumentation hüllt sich dazu überaschenderweise in Schweigen: Ist der Wert der an ein REG_SZ übergeben wird eine Byteanzahl oder eine Zeicheannzahl? Ich nehme an das deren Formulierung von "Byteanzahl" genau das meint und sie das kompensieren müssen.



Zuletzt aktualisiert am Mittwoch, den 22. August 2007 um 17:22 Uhr
 
AUSWAHLMENÜ