26 août 2016

Les variants C++

Design & Code

Thibault

Fresnet

Image d'un ordinateur avec du code, un livre et des lunettes.

26 août 2016

Les variants C++

Design & Code

Thibault

Fresnet

Image d'un ordinateur avec du code, un livre et des lunettes.

26 août 2016

Les variants C++

Design & Code

Thibault

Fresnet

Image d'un ordinateur avec du code, un livre et des lunettes.

Le variant est un type de donnée qui n'est pas fixe au cours du temps. Ce type a été introduit avec C++17 mais son mécanisme peut être adapté aux versions inférieures à l'aide d'une enum et d'une union.
On peut parler de polymorphisme statique en opposition au polymorphisme traditionnel et qui ont les propriétés suivantes :


  • Polymorphisme statique :

    • Les types sont connus à la déclaration

    • Nécessite en général des templates afin de spécialiser des comportements

    • Peut utiliser un type pour définir des metadatas

    • Utilisable traditionnellement en C avec void* et un index

    • Correspond aux surcharges de fonctions

  • Polymorphisme dynamique :

    • Principe de l'héritage

    • Utilise une vtable pour référencer les spécialisations de comportement

    • Peut utiliser dynamic_cast


Le polymorphisme statique se traduit en général par des performances accrues notamment dans les cas où le compilateur inline les fonctions et améliore la localité de cache CPU.

Dans cet article nous présenterons un exemple contenant quelques types de base, une comparaison avec lestd::variant et un cas d'utilisation possible dans une application open-source dans la bibliothèque d'accès à la base de données Soci.


Le code

La structure de base

La structure basique de notre variant est :
Une enum, dans notre cas: { NONE, STRING, DOUBLE, INT64, BOOL }
Une union qui contiendra la valeur de notre variant ainsi que ses différents accesseurs.



Les concepts avancés

La grande particularité d'utiliser une enum qui contient un type utilisateur est que ce type partage la mémoire
avec les autres types. Dans ce cas, le constructeur de l'objet n'est pas appelé durant l'allocation.


Dans notre cas, la spécialisation de l'operateur d'assignation permet d'optimiser les opérations de destruction
et construction de std::string.


Dans la même idée, le destructeur de std::string n'est pas appelé à la destruction de l'union.


Les Getters sont aussi les bienvenus pour accéder aux différentes variables de l'enum.

struct Variant {
    template<typename T> const T& get() const
    { static_assert(std::is_same<T, void>::value); return T(); }
    template<> const std::string& get<std::string>() const { return value.s; }
    template<> const double& get<double>() const { return value.d; }
    template<> const int64_t& get<int64_t>() const { return value.i; }
    template<> const bool& get<bool>() const { return value.b; }
    Type getType() const { return type; }
};
    Variant(42).get<int>(); // ERROR
    Variant(42).get<int64_t>


Le test

Pour tester notre variant, nous le comparons à un cas d'utilisation que nous aurions pu avoir face au standard.
Pour notre variant, nous utilisons l'operateur d'assignation avec un nouveau variant qui va se charger de supprimer pour nous l'instance précédente et pour le variant standard, nous utilisons une assignation de la nouvelle valeur.

using StdVariant = std::variant<std::monostate, std::string, int64_t, bool>;
template<typename T>
void print(const Variant& customVariant, const StdVariant& stdvariant) {
    std::cout << customVariant.get<T>()
        <<"\t\t" << std::get<T>(stdvariant)
        << "\t\t" << (customVariant.get<T>() == std::get<T>(stdvariant))
        << "\n";
}
int main() {
    using namespace std::literals;
    std::cout << std::boolalpha << "CustomVariant\tstdVariant\tSame?\n";
    Variant customVariant = Variant((int64_t)42);
    StdVariant stdVariant((int64_t)42);
    print<int64_t>(customVariant, stdVariant);
    customVariant = Variant("TOTO"s);
    stdVariant = "TOTO"s;
    print<std::string>(customVariant, stdVariant);
    customVariant = Variant("TATA"s);
    stdVariant = "TATA"s;
    print<std::string>(customVariant, stdVariant);
    customVariant = Variant((int64_t)40);
    stdVariant = (int64_t)40;
    print<int64_t>

La compilation de notre test et ses résultats se font comme suit :

> $CC -std=c++17 *.cpp -o variant.exe
> variant.exe
CustomVariant stdVariant Same?
42 42 true
TOTO TOTO true
TATA TATA true
40 40 true


Cas d'utilisation Open-Source

Durant mes contributions à la bibliothèque d'accès à des bases de données Soci, j'ai pu constater que l'implémentation des mappeurs de valeurs entre la base de données et le code client repose sur une structure virtuelle contenant un pointeur, le type-holder.h (Soci v4.0.3).

La construction de cet objet est systématiquement effectuée par une fonction template.

La récupération passe par un dynamic_cast et renvoie un std::bad_cast en cas d'échec de l'association.

Ce cas correspond à l'utilisation d'un std::variant et apporte les avantages suivants :

  • Une standardisation de l'objet (mais nécessite C++17)

  • Une suppression des opérations de dynamic_cast

  • Une suppression des opérations de gestion mémoire liés aux pointeurs donc :

    • Une meilleure localité de cache

    • Une meilleure gestion de la mémoire de l'objet

    • Une meilleure conformité avec le principe RAII (définition Wikipedia)