Über den praxisrelevanten Einsatz der Template-Metaprogrammierung

Seite 2: Konvertierungen

Inhaltsverzeichnis

Die Konvertierung von Boost.Variant nach QVariant ist die einfachere, und zwar aus zwei Gründen: Zunächst kann jede Boost.Variant nur eine begrenzte Anzahl unterschiedlicher Typen enthalten (die bei der Instanziierung angegebenen), und QVariant kann sie, zumindest nach Registrierung, alle aufnehmen. Zweitens besitzt Boost.Variant mit boost::static_visitor eine Implementierung des Visitor-Patterns, die die Konvertierung (fast) trivial erscheinen lässt, denn der Entwickler muss lediglich das Funktions-Template qVariantFromValue(), das einen beliebigen Typ in eine QVariant überführt, in die Form eines boost::static_visitor überführen:

struct variant2qvariant_visitor
  : boost::static_visitor<QVariant> {
    // one size fits all:
    template <typename T>
    QVariant operator()( const T & t ) const {
       return qVariantFromValue( t );
    }
};

und damit boost::apply_visitor aufrufen:

boost::variant<...> bv;
QVariant qv = boost::apply_visitor( variant2qvariant_visitor(), bv );

Näheres zum Mechanismus findet sich in der Boost.Variant-Dokumentation.

Bei der Gelegenheit kann man gleich noch die String-Typen konvertieren, denn von Hause aus können die QAbstractItemViews nichts mit std::strings anfangen. Das liefert einfaches Überladen des Funktionsaufrufoperators:

struct variant2qvariant_visitor_with_string_conversion
  : boost::static_visitor<QVariant> {
    // special case: std::string -> QString -> QVariant
    QVariant operator()( const std::string & str ) const {
        return QString::fromStdString( str );
    }
    // general case:
    template <typename T>
    QVariant operator()( const T & t ) const {
       return qVariantFromValue( t );
    }
};

Das Ganze ist effizient, denn boost::apply_visitor() läuft in konstanter Zeit (O(1)); apply_visitor() ist nichts anderes als ein Switch-Statement. Die statische Zuordnung von Typ zu Typdiskriminator (siehe "Boost.Variant versus QVariant) ermöglicht das.

Auch wenn mancherorts jedes Funktions-Template bereits als TMP gefeiert wird, hat das hier noch überhaupt nichts mit TMP zu tun. Es spielt erst im Folgenden eine Rolle.

Bei der Konvertierung von QVariant nach Boost.Variant stoßen Entwickler auf zwei Probleme, die es im umgekehrten Fall nicht gab. Die Klippe, dass QVariant Typen enthalten kann, die eine gegebene Boost.Variant nicht aufnehmen kann, lässt sich damit umschiffen, dass man eine Ausnahme wirft. Etwas anderes bleibt auch kaum übrig, denn anders als QVariant (QVariant::isValid()) besitzt Boost.Variant keinen Fehlerzustand.

Das zweite Problem ist das Fehlen einer Implementierung des Visitor-Patterns in QVariant. Das allein ließe sich noch einfach lösen (auch boost::apply_visitor() kocht nur mit Wasser). Zusammen mit der Tatsache, dass der Typdiskriminator der QVariant in vielen Fällen erst zur Laufzeit zugeordnet wird (siehe "Boost.Variant versus QVariant"), setzt sich jedoch die Erkenntnis durch, dass eine O(1)-Implementierung bei Boost.Variant aussichtslos sein wird.

Als Ansatz für eine Konvertierungsfunktion lässt sich Folgendes schreiben. Gegeben sei eine QVariant qv und eine Boost.Variant-Instanziierung – Letztere sei T_Variant genannt. Für jeden Typ T, den T_Variant enthalten kann, ist zu überprüfen, ob qv einen Wert dieses Typs enthält. Wenn ja, dann muss man den Wert (mit qvariant_cast<T>(qv)) extrahieren und eine T_Variant mit diesem Wert zurückliefern. Ansonsten muss man es mit dem nächsten Typ versuchen. Wenn kein Typ passt, ist eine Ausnahme zu werfen. In Form von Pseudocode sieht das wie folgt aus:

template <typename T_Variant>
T_Variant qvariant2variant( const QVariant & qv ) {
  for( T : types(T_Variant) )
    if ( qv.type() == T )
      return T_Variant( qvariant_cast<T>( qv ) );
  throw bad_cast( "..." );
};

Es fehlen noch zwei Dinge, um aus dem Pseudocode "richtigen" Code zu erstellen: Erstens ein Weg, um einen Typ T auf den dazugehörigen QVariant-internen Diskriminator abzubilden. Das leistet das Funktions-Template qMetaTypeId():

    if ( qv.type() == qMetaTypeId<T>() )

Es muss tatsächlich eine Funktion sein, denn die Definition der Zuordnung geschieht ja (jedenfalls für benutzerdefinierte Typen) erst zur Laufzeit.

Zweitens ein Weg, um "für jeden Typ T, den T_Variant enthalten kann", abzubilden. Das lässt sich mit TMP lösen (Stichwort: Liste von Typen), was die Aufgabe für den Rest des Artikels sein wird.

Zunächst muss T_Variant mitteilen, welche Typen sie denn nun enthalten kann. Ein Blick in die Dokumentation verrät, dass T_Variant::types ein typedef für eine "MPL-kompatible Sequenz" (Meta-Programming Library) der gewünschten Typen ist.

Als Nächstes geht es darum herauszufinden, wie sich über die Sequenz von Typen iterieren lässt. Zur Erinnerung sei erwähnt, dass TMP eine funktionale Programmiersprache alter Schule ist. Insbesondere gibt es keine Zustandsvariablen und daher auch keine Schleifen. Der Entwickler muss folglich Rekursion anwenden. In jedem Rekursionsschritt muss er einen Typ aus der Liste entnehmen und abarbeiten, bis eine leere Liste übrig bleibt. Wer jetzt an mpl::fold denkt, bekommt einen Extrapunkt. Der Autor implementiert das jedoch händisch, da der Code durch das Verwenden von mpl::fold nicht wesentlich kürzer wird.

Da die MPL der STL (Standard Template Library) nachempfunden wurde, ist es ratsam, sich T_Variant::types als eine Art std::list<std::type_info> vorzustellen. Dann ließe sich schreiben:

template <typename T_Variant>
T_Variant qvariant2variant( const QVariant & qv, const
                          std::list<std::type_info> & types ) {
  // termination condition:
  if ( empty( types ) )
    throw bad_cast( "..." );
  // current iteration:
  if ( qv.type() == qMetaTypeId<front( types )>() )
    return T_Variant( qvariant_cast<front( types )>( qv ) );
  // tail-handling:
  return qvariant2variant<T_Variant>( qv, pop_front( types ) );
}

In dieser Formulierung finden sich keine Schleifen oder temporäre Variablen mehr, was die Umsetzung in eine funktionale Sprache deutlich vereinfacht. Es bleibt, von Pseudo-STL nach MPL zu übersetzen. Die Liste der erlaubten Typen lässt sich nicht als Funktions-, wohl aber als Template-Argument übergeben.  Dazu extrahiert die Hauptfunktion zunächst die Liste der erlaubten Typen aus T_Variant, um sie an eine Hilfsfunktion als explizites Argument weiterzugeben.

namespace mpl = boost::mpl;
template <typename T_Variant>
T_Variant qvariant2variant( const QVariant & qv ) {
  return qvariant2variant_helper<T_Variant,typename T_Variant::types>( qv );
}

Der Rekursionsschritt sieht wie folgt aus:

template <typename T_Variant, typename Types>
T_Variant qvariant2variant_helper( const QVariant & qv ) {
  // split Types into Head and Tail:
  typedef typename mpl::front<Types>::type Head;
  typedef typename mpl::pop_front<Types>::type Tail;
  // processing:
  if ( qv.type() == qMetaTypeId<Head>() )
    return T_Variant( qvariant_cast<Head>( qv ) );
  else
    return qvariant2variant_helper<T_Variant,Tail>( qv );
}

Es fehlt noch die Abbruchbedingung. Der erste Reflex ist die Spezialisierung von qvariant2variant_helper für den Fall, dass Types eine leere MPL-Sequenz ist.

template <typename T_Variant>
T_Variant
qvariant2variant_helper<T_Variant,/*???*/>( const QVariant & qv ) {
   throw bad_cast();
 }

Das scheitert jedoch aus zwei Gründen. Erstens ist die partielle Spezialisierung von Funktions-Templates nicht möglich, und zweitens lässt sich kein Muster angeben, das auf jede leere MPL-Sequenz passt: Die Typen, die mpl::pop_front zurückliefert, werden recht schnell unübersichtlich, ganz abgesehen davon, dass T_Variant::types keine der vordefinierten MPL-Sequenzen sein muss. Die Boost.Variant-Dokumentation sichert lediglich zu, dass T_Variant::types zu MPL-Sequenzen kompatibel ist. Man muss also mpl::empty aufrufen, um auf leere Listen zu prüfen.

Als Ausweg bietet sich bei Funktionen immer das Überladen an. Das klingt zunächst merkwürdig, denn die Signatur (Name und Argumente) von qvariant2variant_helper soll ja gleich bleiben. Mit boost::enable_if ist jedoch genau das möglich:

template <typename T_Variant, typename Types>
typename boost::enable_if< mpl::empty<Types>, T_Variant >::type
qvariant2variant_helper( const QVariant & qv ) {
  throw bad_cast();
}

Damit ist dieses Funktions-Template nur sichtbar, wenn mpl::empty<Types>::value "wahr" ist. Wie das genau funktioniert, lässt sich in der Dokumentation zu boost::enable_if nachlesen. Damit es jetzt nicht zu zweideutigen Funktionsaufrufen kommt, muss der Entwickler im Gegenzug noch den Rekursionsschritt für die leere Typliste verstecken:

template <typename T_Variant, typename Types>
typename boost::disable_if< mpl::empty<Types>, T_Variant >::type
qvariant2variant_helper( const QVariant & qv ) {
  // as before
}

Den gesamten Code inklusive eines kleinen Testprogramms kann man hier herunterladen.