programming c++ albero swiftree boost c++11 template configurazioni cplusplus cpp

swiftree

un modo conciso per caricare dati ad albero da file xml e json

Ho avuto a che fare per anni con applicazioni che avessero bisogno di caricare configurazioni da strutture ad albero come xml e json, ma non ho mai trovato qualcosa che mi permettesse di farlo in modo pratico e poco invasivo.

Ho usato xerces-c e tinyxml, per poi passare alla più pratica (ma meno potente) boost::ptree.

Hanno sempre funzionato egregiamente, ma non mi hanno mai soddisfatto del tutto: assegnare un valore caricato da un file alla proprietà di un oggetto dovrebbe essere banale anche in C++, come lo è in Javascript o in Ruby. E invece, a meno di non creare una macro (AAARRRGHH!) o una classe wrapper, il codice da scrivere per fare una cosa così semplice è sempre troppo.

Voglio solo mappare qualcosa scritto in un file xml (o json, o yaml) alla proprietà di un oggetto o ad una variabile locale!

Un esempio lo si può trovare nel mio progetto threescanner dove uso una classe wrapper di boost::ptree per caricare le configurazioni:

void RealProjector::setupWindow(const Config& cfg) {
  auto width = cfg.get<int>("width");
  auto height = cfg.get<int>("height");
  auto antialiasing = cfg.get<int>("antialiasing", 0);
  auto name = cfg.get<std::string>("name");

(È possibile vedere l'esempio completo qui. Qui invece il file json)

O meglio ancora in ThreephaseEngine, dove passo cfg anche alla classe base e assegno il valore direttamente ad un attributo membro:

ThreephaseEngine::ThreephaseEngine(const Config& cfg) :
    Engine(cfg),
    wrapMethod_(cfg.get<int>("wrapMethod")),

(È possibile vedere l'esempio completo qui.)

Finora boost::ptree è stato il modo più conciso che ho trovato per esprimere quello che voglio. Non è male, soprattutto se usato con auto del C++11, ma con il vecchio standard sarebbe stato

int width = cfg.get<int>("width");

dove il tipo int è ripetuto, e a me non piacciono le ripetizioni!

Così mi son deciso di trovare un'alternativa, dove magari il tipo di casting da fare viene dedotto da quello che si trova alla sinistra dell'operatore uguale.

Perché il C++ ha tipizzazione forte, quindi è di natura "macchinoso" trasformare qualcosa in qualcos'altro. E siccome i file di configurazione non sono altro che alberi di stringhe, o meglio mappe le cui chiavi sono stringhe e i valori altre mappe oppure stringhe, allora la traduzione del valore sarà da stringa a T, dove T è il tipo di cui abbiamo bisogno. Oppure da albero a T.

Per la prima c'è l'ottimo boost::lexical_cast... ma per la seconda?

Cioè... lavorando con Ogre3d mi son spesso trovato ad aver a che fare con vettori e quaternioni, per cui la soluzione sarebbe stata:

Ogre::Vector3 position(cfg.get<float>("position.x"),
  cfg.get<float>("position.y"),
  cfg.get<float>("position.z"));

Una soluzione naïve è usare una macro per esprimere il concetto (tempo fa cadevo spesso in tentazione).

#define XML_VECTOR3(key) \
  Vector3 key(cfg.get<float>(#key ".x"), \
      cfg.get<float>(#key ".y"), \
      cfg.get<float>(#key ".z"))

Che mi permette di fare una cosa del genere:

XML_VECTOR3(position);

Fin troppo conciso, ma anche pericoloso, visto che usa le macro!

Di norma, se si può risolvere un problema con una macro, probabilmente c'è una soluzione migliore che usa i template. Per la stringhizzazione (si traduce veramente così in italiano l'operatore # del preprocessore???) non c'è nulla da fare... ma per tutto il resto c'è un gran margine di manovra.

È così che sono arrivato ad ideare swiftree che mi permettere di scrivere cose come:

float speed = cfg["car.speed"];
Ogre::Vector3 position = cfg["car.position"];

O meglio ancora, con i valori di default e l'inizializzazione diretta degli attributi:

Car(const Tree& cfg) : 
  speed_(cfg.value("speed", 0)),
  position_(cfg.value("speed", Ogre::Vector3::ZERO)) {

  }

La magia è dovuta all'utilizzo dei template, mentre per la "trasformazione" implicita dei valori (letti come stringhe) in qualsiasi altro tipo dobbiamo ringraziare l'operatore di conversione:

template<class Type>
operator Type() const {
  return boost::lexical_cast<Type>(this->toString());
}

Forse è un po' esoterico, non sono assolutamente sicuro del fatto che non sia in qualche modo "pericoloso", ma sembra fare il suo lavoro!

Ok, così funziona per tutti i tipi supportati da boost::lexical_cast... ma per tutti gli altri? E per le classi, come per esempio Vector3 che usano composizioni di valori?

Possiamo specializzare il template:

namespace swiftree {
template<>
Tree::operator Ogre::Vector3() const {
    return Ogre::Vector3(
      value<float>("x"),
      value<float>("y"),
      value<float>("z")
    );
}
}

Ora possiamo creare un traduttore per qualsiasi struttura!

Perché il nome swiftree? Perché gestisce strutture ad albero (tree) ed è pratico e veloce (swift) da usare.

Per vedere tutto quello che può fare swiftree, consiglio di leggere il README del progetto su GitHub. Hai consigli o richieste? Commenta pure qui sotto o, ancora meglio, crea una issue su GitHub!

Ti è piaciuto l'articolo? Condividilo! Commentalo!

comments powered by Disqus

Articolo disponibile in altre lingue
en