web-dev-qa-db-fra.com

Problème de diamant C ++ - Comment appeler la méthode de base une seule fois

J'utilise l'héritage multiple en C++ et j'étends les méthodes de base en appelant explicitement leur base. Supposons la hiérarchie suivante:

     Creature
    /        \
 Swimmer    Flier
    \        /
       Duck

Ce qui correspond à

class Creature
{
    public:
        virtual void print()
        {
            std::cout << "I'm a creature" << std::endl;
        }
};

class Swimmer : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        void print()
        {
            Flier::print();
            Swimmer::print();
            std::cout << "I'm a duck" << std::endl;
        }
};

Maintenant, cela pose un problème - l'appel de la méthode print du canard appelle ses méthodes de base respectives, qui à leur tour appellent la méthode Creature::print(), donc elle finit par être appelée deux fois -

I'm a creature
I can fly
I'm a creature
I can swim
I'm a duck

Je voudrais trouver un moyen de m'assurer que la méthode de base n'est appelée qu'une seule fois. Quelque chose de similaire à la façon dont l'héritage virtuel fonctionne (appeler le constructeur de base lors du premier appel, puis lui attribuer un pointeur uniquement lors d'appels successifs à partir d'autres classes dérivées).

Existe-t-il un moyen intégré de le faire ou devons-nous recourir à sa mise en œuvre nous-mêmes?

Si oui, comment aborderiez-vous cela?

La question n'est pas spécifique à l'impression. Je me demandais s'il y avait un mécanisme pour étendre les méthodes de base et les fonctionnalités tout en conservant l'ordre des appels et en évitant le problème du diamant.

Je comprends maintenant que la solution la plus importante serait d'ajouter des méthodes d'assistance, mais je me demandais simplement s'il y avait une méthode plus "propre".

38
O. Aroesti

Vous demandez quelque chose comme l'héritage au niveau d'une fonction qui appelle automatiquement la fonction héritée et ajoute simplement plus de code. Vous voulez également que cela se fasse de manière virtuelle, tout comme l'héritage de classe. Pseudo syntaxe:

class Swimmer : public virtual Creature
{
     public:
        // Virtually inherit from Creature::print and extend it by another line of code
        void print() : virtual Creature::print()
        {
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        // Virtually inherit from Creature::print and extend it by another line of code
        void print() : virtual Creature::print()
        {
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        // Inherit from both prints. As they were created using "virtual function inheritance",
        // this will "mix" them just like in virtual class inheritance
        void print() : Flier::print(), Swimmer::print()
        {
            std::cout << "I'm a duck" << std::endl;
        }
};

Donc, la réponse à votre question

Existe-t-il un moyen intégré de le faire?

est non. Quelque chose comme ça n'existe pas en C++. De plus, je ne connais aucune autre langue qui a quelque chose comme ça. Mais c'est une idée intéressante ...

3
sebrockm

Il s'agit très probablement d'un problème XY. Mais ... n'appelez pas ça deux fois.

#include <iostream>

class Creature
{
public:
    virtual void identify()
    {
        std::cout << "I'm a creature" << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a swimmer\n";
    }

    virtual void tell_ability()
    {
        std::cout << "I can swim\n";
    }
};

class Flier : public virtual Creature
{
public:
    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a flier\n";
    }

    virtual void tell_ability()
    {
        std::cout << "I can fly\n";
    }
};

class Duck : public Flier, public Swimmer
{
public:
    virtual void tell_ability() override
    {
        Flier::tell_ability();
        Swimmer::tell_ability();
    }

    virtual void identify() override
    {
        Creature::identify();
        tell_ability();
        std::cout << "I'm a duck\n";
    }
};

int main()
{
    Creature c;
    c.identify();
    std::cout << "------------------\n";

    Swimmer s;
    s.identify();
    std::cout << "------------------\n";

    Flier f;
    f.identify();
    std::cout << "------------------\n";

    Duck d;
    d.identify();
    std::cout << "------------------\n";
}

Production:

I'm a creature
------------------
I'm a creature
I can swim
I'm a swimmer
------------------
I'm a creature
I can fly
I'm a flier
------------------
I'm a creature
I can fly
I can swim
I'm a duck
------------------
50
Swordfish

Nous pouvons laisser la classe de base garder une trace des attributs:

#include <iostream>
#include <string>
#include <vector>

using namespace std::string_literals;

class Creature
{
public:
    std::string const attribute{"I'm a creature"s};
    std::vector<std::string> attributes{attribute};
    virtual void print()
    {
        for (auto& i : attributes)
            std::cout << i << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    Swimmer() { attributes.Push_back(attribute); }
    std::string const attribute{"I can swim"s};
};

class Flier : public virtual Creature
{
public:
    Flier() { attributes.Push_back(attribute); }
    std::string const attribute{"I can fly"s};
};

class Duck : public Flier, public Swimmer
{
public:
    Duck() { attributes.Push_back(attribute); }
    std::string const attribute{"I'm a duck"s};
};

int main()
{
    Duck d;
    d.print();
}

De même, si ce n'est pas seulement l'impression que nous recherchons, mais plutôt les appels de fonction, nous pourrions laisser la classe de base suivre les fonctions:

#include <iostream>
#include <functional>
#include <vector>

class Creature
{
public:
    std::vector<std::function<void()>> print_functions{[this] {Creature::print_this(); }};
    virtual void print_this()
    {
        std::cout << "I'm a creature" << std::endl;
    }
    void print()
    {
        for (auto& f : print_functions)
            f();
    }
};

class Swimmer : public virtual Creature
{
public:
    Swimmer() { print_functions.Push_back([this] {Swimmer::print_this(); }); }
    void print_this()
    {
        std::cout << "I can swim" << std::endl;
    }
};

class Flier : public virtual Creature
{
public:
    Flier() { print_functions.Push_back([this] {Flier::print_this(); }); }
    void print_this()
    {
        std::cout << "I can fly" << std::endl;
    }
};

class Duck : public Flier, public Swimmer
{
public:
    Duck() { print_functions.Push_back([this] {Duck::print_this(); }); }
    void print_this()
    {
        std::cout << "I'm a duck" << std::endl;
    }
};

int main()
{
    Duck d;
    d.print();
}
23
wally

Un moyen simple consiste à créer un groupe de classes d'assistance qui imitent la structure d'héritage de votre hiérarchie principale et effectuent toutes les impressions dans leurs constructeurs.

 struct CreaturePrinter {
    CreaturePrinter() { 
       std::cout << "I'm a creature\n";
    }
 };

 struct FlierPrinter: virtual CreaturePrinter ... 
 struct SwimmerPrinter: virtual CreaturePrinter ...
 struct DuckPrinter: FlierPrinter, SwimmerPrinter ...

Ensuite, chaque méthode d'impression dans la hiérarchie principale crée simplement la classe d'assistance correspondante. Pas de chaînage manuel.

Pour la maintenabilité, vous pouvez faire imbriquer chaque classe d'imprimante dans sa classe principale correspondante.

Naturellement, dans la plupart des cas réels, vous voulez passer une référence à l'objet principal comme argument au constructeur de son assistant.

9
n. 'pronouns' m.

Vos appels explicites aux méthodes print constituent le nœud du problème.

Une solution consiste à supprimer les appels print et à les remplacer par say

void queue(std::set<std::string>& data)

et vous accumulez les messages d'impression dans le set. Peu importe que ces fonctions de la hiérarchie soient appelées plusieurs fois.

Vous implémentez ensuite l'impression de l'ensemble dans une seule méthode dans Creature.

Si vous souhaitez conserver l'ordre d'impression, vous devez remplacer le set par un autre conteneur qui respecte l'ordre d'insertion et rejette les doublons.

6
Bathsheba

Si vous voulez cette méthode de classe moyenne, n'appelez pas la méthode de classe de base. Le moyen le plus simple et le plus simple consiste à extraire des méthodes supplémentaires, puis à réimplémenter Print est facile.

class Creature
{
    public:
        virtual void print()
        {
            std::cout << "I'm a creature" << std::endl;
        }
};

class Swimmer : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I can swim" << std::endl;
        }
};

class Flier : public virtual Creature
{
     public:
        void print()
        {
            Creature::print();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I can fly" << std::endl;
        }
};

class Duck : public Flier, public Swimmer
{
     public:
        void print()
        {
            Creature::Print();
            Flier::detailPrint();
            Swimmer::detailPrint();
            detailPrint();
        }

        void detailPrint()
        {
            std::cout << "I'm a duck" << std::endl;
        }
};

Sans détails, quel est votre problème réel, il est difficile de trouver une meilleure solution.

6
Marek R

Utilisation:

template<typename Base, typename Derived>
bool is_dominant_descendant(Derived * x) {
    return std::abs(
        std::distance(
            static_cast<char*>(static_cast<void*>(x)),
            static_cast<char*>(static_cast<void*>(dynamic_cast<Base*>(x)))
        )
    ) <= sizeof(Derived);
};

class Creature
{
public:
    virtual void print()
    {
        std::cout << "I'm a creature" << std::endl;
    }
};

class Walker : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can walk" << std::endl;
    }
};

class Swimmer : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can swim" << std::endl;
    }
};

class Flier : public virtual Creature
{
public:
    void print()
    {
        if (is_dominant_descendant<Creature>(this))
            Creature::print();
        std::cout << "I can fly" << std::endl;
    }
};

class Duck : public Flier, public Swimmer, public Walker
{
public:
    void print()
    {
        Walker::print();
        Swimmer::print();
        Flier::print();
        std::cout << "I'm a duck" << std::endl;
    }
};

Et avec Visual Studio 2015, la sortie est:

I'm a creature
I can walk
I can swim
I can fly
I'm a duck

Mais is_dominant_descendant n'a pas de définition portable. Je souhaite que ce soit un concept standard.

4
Red.Wave