La bibliothèque reflect-cpp offre des mécanismes puissants pour gérer la sérialisation et la désérialisation de types C++ personnalisés, même lorsque l'encapsulation de classe pose des défis. Ce guide explore des techniques avancées pour personnaliser la conversion des données, y compris l'implémentation de modèles de sérialisation et la création de processeurs de transformation.
Sérialisation via l'interface ReflectionType
Pour permettre la sérialisation d'une classe avec des membres privés, reflect-cpp propose le patron de conception ReflectionType. Cette approche requiert trois éléments :
- La définition d'un type public (souvent une structure) qui reflète les champs à sérialiser.
- Un constructeur acceptant ce type comme paramètre.
- Une méthode
reflection()qui retourne une référence constante vers l'instance du type miroir.
// Définition du type miroir avec des champs publics
struct ProfilUtilisateurReflete {
std::string identifiant;
std::string courriel;
int niveau_acces;
};
// Classe principale avec membres privés
class Utilisateur {
public:
using ReflectionType = ProfilUtilisateurReflete;
Utilisateur(const ReflectionType& _reflet) : reflet(_reflet) {}
const ReflectionType& reflection() const { return reflet; }
// Méthodes métier spécifiques à Utilisateur...
bool est_admin() const { return reflet.niveau_acces > 5; }
private:
ReflectionType reflet;
};
Une variante utilise rfl::Field dans la structure miroir pour forcer l'initialisation explicite de tous les champs, garantissant ainsi une correspondance stricte avec les types attendus lors de la construction.
Personnalisation externe avec les spécialisations de Reflector
Lorsque la modification de la classe originale est impossible, il est possible de fournir une sérialisation externe en spécialisant le modèle rfl::Reflector.
namespace rfl {
template <>
struct Reflector<Utilisateur> {
// Type utilisé pour la sérialisation intermédiaire
struct TypeIntermediaire {
std::string uid;
std::string email;
int role;
};
// Conversion depuis le type intermédiaire vers le type cible
static Utilisateur vers(const TypeIntermediaire& vi) noexcept {
return Utilisateur({vi.uid, vi.email, vi.role});
}
// Conversion depuis le type cible vers le type intermediaire
static TypeIntermediaire depuis(const Utilisateur& u) {
auto reflet = u.reflection();
return TypeIntermediaire{reflet.identifiant, reflet.courriel, reflet.niveau_acces};
}
};
}
Une assertion statique (static_assert) peut être ajoutée pour vérifier à la compilation que le nombre de champs du type intermédiaire correspond à celui attendu par la logique de conversion.
Développemnet de processeurs de données
Les processeurs (processors) dans reflect-cpp sont des transformations appliquées aux données pendant leur sérialisation ou désérialisation. Ils permettent de modifier la structure des données, par exemple pour renommer des champs ou injecter des métadonnées.
Exemple d'utilisation de processeurs intégrés
struct Mesure {
double valeur;
std::string unite;
long horodatage;
};
const auto capteur = Mesure{98.6, "°C", 1672531200};
// Utilisation du processeur pour renommer les champs
const auto json = rfl::json::write<rfl::SnakeCaseToCamelCase>(capteur);
// Résultat probable: {"valeur":98.6,"unite":"°C","horodatage":1672531200}
Enchaînement de processeurs
Plusieurs processeurs peuvent être combinés dans un pipeline de transformation.
using PipelineTraitement = rfl::Processors<
rfl::SnakeCaseToCamelCase,
rfl::AddStructName<"_type_">
>;
const auto sortie = rfl::json::write<PipelineTraitement>(capteur);
// Résultat: {"_type_":"Mesure","valeur":98.6,"unite":"°C","horodatage":1672531200}
Création d'un processeur sur mesure
Un processeur personnalisé est une structure possédant une méthode statique process qui transforme un named tuple.
struct ProcesseurAnonymisation {
template <class TypeStructure>
static auto process(auto&& tuple_nommee) {
// Logique de transformation, par exemple masquer certaines données
return std::move(tuple_nommee);
}
};
Application pratique complète
Intégrons une classe complexe, un processeur personnalisé pour la validation, et utilisons les processeurs intégrés pour la mise en forme.
struct Coordonnees {
double latitude;
double longitude;
};
struct Emplacement {
std::string nom;
Coordonnees coordonnees;
std::optional<std::string> description;
};
// Processeur pour garantir la présence d'une description
struct GarantirDescription {
template <class T>
static auto process(auto&& tn) {
if (!rfl::get<"description">(tn)) {
rfl::get<"description">(tn) = "Aucune description fournie";
}
return std::move(tn);
}
};
// Pipeline de traitement complet
using TraitementComplet = rfl::Processors<
GarantirDescription,
rfl::SnakeCaseToCamelCase
>;
const auto lieu = Emplacement{"Tour Eiffel", {48.8584, 2.2945}, std::nullopt};
const auto donnees_json = rfl::json::write<TraitementComplet>(lieu);
// Le champ "description" sera présent et renseigné dans la sortie.
Considérations de performance
- L'utilisation du processeur
rfl::NoFieldNamesréduit considérablement la taille de la sortie pour les formats binaires, mais la rend moins lisible. - Pour les types fréquemment sérialisés, privilégier les approches qui maximisent les opérations à la compilation, comme la spécialisation de
Reflector. - Les processeurs impliquant des allocations dynamiques ou des validations coûteuses doivent être placés judicieusement dans le pipeline.
En combinant ces techniques de type personnalisé et de traitement de données, reflect-cpp offre une flexibilité considérable pour répondre à des besoins complexes de sérialisation en C++.