• [^] # Re: C'est vrai que c'est plus verbeux en c++

    Posté par (site web personnel) . En réponse au journal Sortie de GHC 8.0.2 et une petite histoire de typage statique. Évalué à 5.

    Je ne vais pas te dire que l'héritage c'est débile et que les ADT sont mieux, c'est juste des cas d'usage différents et des débats d'opinion ;) Personnellement j'ai plus souvent besoin d'un ADT que d'un héritage.

    Le débat "héritage" versus "ADT" est souvent proche du problème du couplage et de la notion de comment les choses peuvent évoluer, notamment l'ajout de cas où de comportement.

    L'héritage est une hiérarchie ouverte et à ce titre, n'importe qui peut rajouter un enfant avec son propre comportement. Ce n'est pas forcement souhaitable dans un cas où justement le but de ton type est de restreindre la liste des options. Rajouter un cas dans un ADT n'est pas coûteux, mais on ne peut pas le faire sans que tu sois au courant.

    Entre les deux, le couplage est différent. L'héritage regroupe ensemble des comportements (parfois sans relation). Les ADT regroupent ensemble des cas de figures, et pour chaque comportement, le traitement de chaque cas de figure.

    Dans le cas de l'héritage, si tu veux rajouter un comportement (une méthode), tu dois modifier ta classe de base et chercher toutes les classes enfant pour ajouter ce comportement. Dans le cas d'un ADT, si tu veux rajouter un comportement, tu l'écris où tu veux en listant tous les cas.

    Technique

    Si on veut parler technique, l'ADT, avec sa liste de cas qui est fixée, peut être optimisé par le compilateur. (Je ne dis pas que c'est fait, mais c'est le cas du std::variant). Par exemple, dans le cas du carré et du réctangle, la solution à base d'héritage et de polymorphisme vas te coûter, en ignorant le coût de la vtable, vas te coûter entre 20 et 24 octet par objet:

    • 16 octets alloués sur la pile, le pointeur d’objet et le pointeur de vtable
    • 8 octets pour le rectangle et 4 pour le carré, alloué sur le tas.

    L'appel à la méthode "surface" vas te coûter, en gros, entre 20 et 32 octets à lire (il faudra aller lire la vtable), et cela à trois endroits différents en mémoire, donc tu consommes 3 lignes de cache pour faire ce traitement.

    Maintenant regardons comment peut être layouté un ADT en mémoire, en gros, simplifié, boost::variant ressemblera à :

    struct
    {
     union
     {
     Carre carre;
     Rectangle rectangle;
     };
     int which;
    }

    Ce qui donne une taille de 12 bytes, sur la pile, versus entre 20 et 24 bytes, avec une allocation sur le tas. L'appel de la méthode surface te coûtera la lecture de ces 12 bytes (contigus en mémoire) et un branchement sur which.

    Les compilateurs actuels implémentent une méthode de devirtualization pour améliorer cette situation, mais le virtuel garde un coût non négligeable.

    Tout cela n'est pas comparable à Haskell pour lequel chaque objet est alloué sur le tas et avec un énorme overhead ;)