Freitag, 4. Mai 2018

Konvertierung der Delphi Stringtypen

Konvertierung der Delphi Stringtypen (template, Spezialisierung)

Wir alle kennen das Problem, dass die Delphi- Stringtypen (UnicodeString oder kurz String, AnsiString) nicht unbedingt zu C++ passen, und wir immer wieder die Methode c_str() bemühen müssen. Das ist nicht nur nervig, es kann auch zu Fehlern führen. Außerdem wollen wir oft den Inhalt einen Strings in einem anderen Datentyp haben, oder dieses umgekehrt in einen Delphi- String umwandeln, zum Beispiel um diesen in einem Edit- Feld eines Formulars anzuzeigen.

Dazu kommt, dass dank der nicht unbedingt nachvollziehbaren Entscheidung von Embarcadero unter iOS und Android weiter auf ein 16 bit Zeichentyp zu setzen, anstatt den systemspezifischen wchar_t Typ zu nutzen, der bei den unixoiden Betriebssystem ja ein 32 bit Datentyp ist. Damit kann es passieren, dass wir es in einem C++- Programm unter Android plötzlich mit 3 nicht unbedingt kompatiblen Datentypen zu haben. Dabei sollte es im Businessteil ihrer Anwendung nur die Entscheidung zwischen nationalen Zeichensatz (8 bit, char, string) geben, oder dem internationalen (wchar_t, wstring). Außerdem sind die Indizes für die Delphi Stringtypen auf den Desktop- Betriebssystemen 1 basierend, auf den mobilen Betriebssystemen aber an C und C++ orientiert 0 basierend. Auch dieses kann zu Problemen und notwendigen Anpassungen führen. Alles das wäre unter C++ einfach schlichtweg unmöglich, da hier ein Industriestandard die Investitionen des Entwicklers schützt.

Ich möchte hier eine Headerdatei für die weitere Verwendung in diesem Blog erzeugen, und dieses Problem mit Hilfe von C++- templates und der Spezialisierung dieser für spezielle Datentypen.


Vielleicht dieses vorneweg. Ich war einmal, so beginnen nicht nur Märchen. Oft fangen wir als Unternehmensberater auch mit solchen Sätzen an. Ich war einmal bei einem Kunden zu einer Inhouse- Schulung. Hier arbeiten mehrere Entwickler seit einigen Jahren an einem Programm aus dem Energiesektor. Während der Schulung wurde auch das Thema behandelt, dass für die Embarcadero- Stringtypen die Konstruktoren für die Standard C++ Datentypen fehlen, beziehungsweise die Konvertierungsoperatoren für die umgekehrte Richtung nicht umgesetzt sind. Leider sind gerade die Operatoren nicht über argument dependent name lookup (ADL) nicht ergänzbar. In diesem Fall könnte der leistungsfähige C++- Compiler dieses Problem für uns lösen, und das ohne einen Fehler. Nun ist dieses für einen Delphi- Programmierer sicher auch völlig uninteressant, er dürfte nicht mal verstehen, warum ich das überhaupt kritisiere. Und damit ist vielleicht auch zu erklären, warum es fehlt. Nun ließ einer der Entwickler eine grep- Suche über den aktuellen Quelltextstand laufen, und im Laufe der Jahre hatten sich sagenhaft 2,5 Millionen Aufrufe der Methode c_str() in den Quelltexten angehäuft. Sollten jetzt Änderungen notwendig werden, zum Beispiel bei einer Migration auf ein unixoides Betriebssystem, kann sich jeder das Ausmaß der notwendigen Arbeiten vorstellen.

Ich möchte dieses Problem für die zukünftigen Beispiele in diesem Blog mit den templates __ToVCL und __FromVCL lösen. Auch hier nutze ich die vordefinierten Bedingungen für VCL bzw. FMX, da nur bei der Nutzung eines dieser Frameworks die Nutzung von Delphi- Typen sinnvoll erscheint. Danach folgen die beiden Headerdateien für Delphi- Stringtypen <system.hpp> und C++- Stringtypen <string>.

Nun braucht man für die meisten Datentypen keine internationalen Zeichen, also nutze ich für die Implementierung des templates auch nur eine nationale Variante und nutze die in der Delphi- Klassenbibltiothek automatische Konvertierung von UnicodeString zu AnsiString. Für die notwendige Typumwandlung werde sich das template boost::lexical_cast verwenden, dass eine Zeichenkette in einen beliebigen C++- Typ umwandeln kann. Dieses funktioniert auch für von ihnen selbst definierte Datentypen, wenn sie die Stream- Operatoren implementiert haben.


#ifndef MyDelphiHelperH
#define MyDelphiHelperH

#if !defined BUILD_WITH_VCL && !defined BUILD_WITH_FMX
   #error Für diese Anwendung muss eine Framework- Variante ausgewählt werden
#endif

#include <system.hpp>
#include <string>

#include <sstream>
#include <boost/lexical_cast.hpp>

template <typename ty>
inline String  __ToVCL(ty const& val) {
   std::ostringstream os;
   os << val;
   return os.str().c_str();
   }

template <typename ty>
inline ty __FromVCL(String const& text) {
   return boost::lexical_cast<ty>(AnsiString(text).c_str());
   }


#endif

Da ich es an der Oberfläche der Delphi- Frameworks immer mit UnicodeStrings zu tun habe, habe ich dieses nur für diesen Typ umgesetzt. Eine Erweiterung auf den Delphi- Typ Ansistring sollte aber für jeden sehr einfach sein. Mir geht es hier nicht um eine vollständige und funktionierende Bibliothek, sondern um das Erklären von Techniken, Problemen und Lösungsansätzen. 

Nun kann der Aufwand, die boost Bibliothek auf einem mobilen System wie Android mitzuführen, höher sein, als der Nutzen. Oder ein Compiler bietet mir nicht die Unterstützung von boost an. Vielleicht gib es hier eine einfache Möglichkeit, auch boost muss ja irgendwie einen Ansatz gefunden haben. Deshalb ändere ich das obige Beispiel wie folgt ab, und nutze wieder die bedingte Übersetzung, sollte es sich bei der Zielumgebung um Android handeln.

#ifndef MyDelphiHelperH
#define MyDelphiHelperH

#if !defined BUILD_WITH_VCL && !defined BUILD_WITH_FMX
   #error Für diese Anwendung muss eine Framework- Variante ausgewählt werden
#endif

#include <system.hpp>
#include <string>

#include <sstream>

#ifndef __ANDROID__
   #include <boost/lexical_cast.hpp>
#endif

template <typename ty>
inline String  __ToVCL(ty const& val) {
   std::ostringstream os;
   os << val;
   return os.str().c_str();
   }

template <typename ty>
inline ty __FromVCL(String const& text) {
   #ifndef __ANDROID__
      return boost::lexical_cast<ty>(AnsiString(text).c_str());
   #else
      std::istringstream ins(AnsiString(text).c_str());
      ty val;
      ins >> val;
      return val;
   #endif
   }

#endif

Nun ist diese Umwandlung nicht effektiv, kann auch zu Fehlern führen, wenn Stringtypen selber verwendet werden. Für diese Typen möchte ich Spezialisierungen implementieren. Normalerweise erzeugt der Compiler für jeden Kombination von Typparameter eine Kopie das templates und ersetzt die Typparameter durch die konkreten Typen. Dadurch entsteht am Ende sehr robuster und direkt gelinkter Code der schneller ist, als Entscheidungen zur Laufzeit. Templates können aber auch spezialisiert werden (für ausgewählte Typparameter eine abweichende Implementierung festlegen), was ich hier ausnutzen möchte.

Außerdem ist bei den Delphi- Stringtypen eine Inkonsistenz vorhanden, da die Indizes hier auf mobilen Betriebssystemen dem Standardverhalten folgt und mit 0 beginnt. Außerdem muss hier die Umwandlung von dem 16 bit Zeichensatz auf den Systemtyp wchar_t erfolgen, der bei allen unixoiden 32 bit nutzt.


#ifndef MyDelphiHelperH
#define MyDelphiHelperH

#if !defined BUILD_WITH_VCL && !defined BUILD_WITH_FMX
   #error Für diese Anwendung muss eine Framework- Variante ausgewählt werden
#endif

#include <system.hpp>
#include <string>

#include <sstream>

#ifndef __ANDROID__
   #include <boost/lexical_cast.hpp>
#endif

template <typename ty>
inline String  __ToVCL(ty const& val) {
   std::ostringstream os;
   os << val;
   return os.str().c_str();
   }

template <>
inline String  __ToVCL<std::string>(std::string const& val) {
   return val.c_str();
   }

template <>
inline String  __ToVCL<std::wstring>(std::wstring const& val) {
   return val.c_str();
   }

template <typename ty>
inline ty __FromVCL(String const& text) {
   #ifndef __ANDROID__
      return boost::lexical_cast<ty>(AnsiString(text).c_str());
   #else
      std::istringstream ins(AnsiString(text).c_str());
      ty val;
      ins >> val;
      return val;
   #endif
   }

template <>
inline std::wstring __FromVCL<std::wstring>(String const& text) {
   #ifdef __ANDROID__
      // String indexes are 1-based in desktop platforms and 0-based in mobile platforms.
      std::wstring strTemp2 = L"";
      for(int i = 0; i < text.Length(); ++i) strTemp2.push_back(static_cast<wchar_t>(text[i]));
      return strTemp2. c_str();
   #else
      return text.c_str();
   #endif
   }

template <>
inline std::string __FromVCL<std::string>(String const& text) {
   return AnsiString(text).c_str();
   }

#endif

Nun kann es im Zusammenhang mit nullterminierten Zeichenfolgen Probleme geben, da ja diese als const char* gespeichert sind, abweichend zu dem internationalen Zeichenfeldern mit wchar_t. Deshalb werde ich eine Methode mit dem selben Namen __ToVCL implementieren, die aber kein template ist. Für die spätere Anwendung spielt dieses aber keine Rolle. Und mich befreit dieser Ansatz die Varianten auseinanderzuhalten, so dass ich mich auf inhaltliche Probleme konzentrieren kann.


inline String  __ToVCL(const char* val) {
   return val;
   }

Damit habe ich die Unterschiede zwischen den mobilen Betriebssystemen und eventuelle Konvertierungen von nationalen zu internationalen Zeichensätze an einer zentralen Stelle implementiert. Sollte es Anpassungen geben, wird es keine Auswirkungen auf die Teile meiner Businessanwendung geben. Ich habe die Entscheidungen wieder in meiner Hand. Dadurch ergibt sich die Möglichkeit einer Testanwendung, die für FMX und die VCL gleich ist, und auch mit dem mobilen Betriebssystem Android als Zielplattform funktioniert.
 

#include "MainFormVCL.h"
#include "MyDelphiHelper.h"

#include <exception>
using namespace std;

void __fastcall TForm1::Button1Click(TObject *Sender) {
   try {
      int iWert   = __FromVCL<int>(Edit1->Text);
      iWert *= 5;
      Edit2->Text = __ToVCL(iWert);
      }
   catch(exception& ex) {
      ShowMessage(__ToVCL(ex.what()));
      }
   return;
   }

Nun höre ich schon die Kritik, dass ich hier ja auch zahlreiche Aufrufe von Funktionen habe, außerdem erzeugt dieser Quellcode unnötige Kopien und kann daher nicht effektiv sein. Dieses trifft auf moderne C++- Compiler aber nicht mehr zu. Ich werde das in einem späteren Blogpost erklären und ihnen auch am konkreten Beispiel zeigen. Und die Funktionsaufrufe geben mir als verantwortungsbewussten Programmierer gerade die Möglichkeit, Entscheidungen zu treffen, Inkonsistenzen aufzulösen und auf zukünftige Veränderungen zu reagieren. Und dieses nicht verteilt über den kompletten Quellcode meiner Anwendung, sondern an einer zentralen Stelle. Sollte ich diese doch nacharbeiten müssen, ist es wesentlich einfacher, im Quellcode nach __FromVCL oder ::ToVCL zu suchen.
;