J'ai un vector de unique_ptr aux objets qui partagent une classe de base commune. Je voudrais parcourir le vecteur et appeler la surcharge correcte d'une fonction, en fonction du type stocké. Le problème est que cette fonction n'est pas membre de la classe (pour ceux d'entre vous qui aiment parler des modèles de conception: imaginez que j'implémente une classe de visiteur et que f est la méthode Visit). Prenons l'exemple de code suivant (ou essayez-le en ligne):

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Base{ public: virtual ~Base() {}; };
class A : public Base { public: virtual ~A() {} };
class B : public Base { public: virtual ~B() {} };
class C : public Base { public: virtual ~C() {} };

void f(Base* b) { cout << "Calling Base :(\n"; }
void f(A* a) { cout << "It is an A!\n"; }
void f(B* b) { cout << "It is a B\n"; }
void f(C* c) { cout << "It is a C!\n"; }

template<class Derived>
void push(vector<unique_ptr<Base>>& v, Derived* obj)
{
    v.push_back(std::unique_ptr<Derived>{obj});
}

int main() {
    vector<unique_ptr<Base>> v{};
    push(v, new A{});
    push(v, new B{});
    push(v, new C{});

    for(auto& obj : v)
    {
        f(obj.get());
    }

    return 0;
}

Il y a des différences superficielles avec mon code (f est une méthode de classe au lieu d'une fonction libre, je n'utilise pas using namespace std) mais cela montre l'idée générale. je vois

Calling Base :(
Calling Base :(
Calling Base :(

Alors que j'aimerais aimer voir

It is an A!
It is a B!
It is a C!

Je voudrais savoir si je peux obtenir la surcharge correcte de f à appeler (je voudrais me débarrasser complètement de la version f(Base*)).

Une option serait la vérification manuelle du type

     if(dynamic_cast<A>(obj) != nullptr) f((A*)obj);
else if(dynamic_cast<B>(obj) != nullptr) f((B*)obj);
...

Mais c'est tout simplement moche. Une autre option serait de déplacer f vers Base, mais comme je l'ai dit, j'implémente un modèle de visiteur et je préférerais garder la méthode Visit hors de l'arborescence d'objets que je visite.

Merci!

EDIT: Apparemment, mon exemple de code a donné l'impression que mes types doivent être non virtuels - en fait, je n'ai pas d'objections fondamentales à l'ajout d'une méthode virtuelle, j'ai donc ajouté cela à l'exemple de code.

0
CompuChip 2 janv. 2016 à 17:23

4 réponses

Meilleure réponse

Merci pour toutes les bonnes réponses les gars. Le problème semble avoir été, que j'ai réalisé trop tard (en éditant mon OP et en écrivant des commentaires à vos demandes de clarification), je mettais en œuvre un modèle de conception existant. Et il s'avère que j'ai eu le modèle à l'envers.

Ce que j'aurais faire, c'est de créer une classe d'interface au-dessus de f qui me permet de sélectionner une autre implémentation:

class IVisitor 
{ public:
    virtual void visit(const A*) const = 0; 
    virtual void visit(const B*) const = 0; 
    virtual void visit(const C*) const = 0; 
};

// Example implementation: 
class TypePrinter: public IVisitor
{ public:
    // Base* version not needed - yay!
    // virtual void visit(const Base*) const { std::cout << "Called Base :(\n"; }
    virtual void visit(const A* a) const override { cout << "It is an A!\n"; }
    virtual void visit(const B* b) const override { cout << "It is a B\n"; }
    virtual void visit(const C* c) const override { cout << "It is a C!\n"; }
};

Ensuite, donnez à Base et à toutes les sous-classes une méthode à appeler visit:

struct A : public Base 
{ public: 
    // This line copied across to `B` and `C` as well:
    virtual void accept(const IVisitor& v) override { v.visit(this); } 
};

Et maintenant, comme @LogicStuff l'a correctement remarqué, le polymorphisme résoudra bien le problème de surcharge:

TypePrinter visitor;
for(auto& obj : v)
{
    obj->accept(visitor);
}

Regarde ma, non dynamic_cast s! :)

0
CompuChip 2 janv. 2016 à 17:20

Pour pouvoir sélectionner la bonne fonction au moment de l'exécution, vous devez disposer de certaines fonctions virtuelles (à moins que vous ne souhaitiez écrire vous-même une fonction d'information de type dans votre objet et ajouter une surcharge de répartition).

L'approche la plus simple serait de faire de f () une fonction membre virtuelle de votre classe de base et de fournir une version remplacée pour chacun des types dérivés. Mais vous avez éliminé cette approche.

Une autre solution possible pourrait être d'utiliser une technique de type double expédition en utilisant une fonction de distribution virtuelle comme celle-ci:

class Base { 
public:  
   virtual void callf() { f(this); } 
   virtual ~Base() {}
}
class A : public Base {
public: 
   void callf() override { f(this) };  // repeat in all derivates !  
}
class B : public Base {
public: 
   void callf() override { f(this) };  // repeat in all derivates !   
}
...
void F(Base *o) {  // this is the function to be called in your loop 
    o->f();        
}

L'astuce est que le compilateur trouvera la bonne fonction f() dans chaque fonction callf(), en utilisant le type réel de this.
La fonction F() appellera alors la fonction de répartition virtuelle, en s'assurant que c'est celle qui correspond au type réel de l'objet au moment de l'exécution.

2
Christophe 2 janv. 2016 à 14:56

Voici deux solutions possibles.


1) Utilisez l'effacement de type avec std::function et évitez l'héritage:

class element
{
private:
    std::function<void(element&)> _f;

public:
    template<typename TF>
    element(TF&& f) : _f(std::forward<TF>(f)) { }

    void call_f() { _f(*this); }
};

void print_a(element& e) { std::cout << "a\n"; }    
void print_b(element& e) { std::cout << "b\n"; }

auto make_element_a() { return element{&print_a}; }    
auto make_element_b() { return element{&print_b}; }

int main()
{
    std::vector<element> es;
    es.emplace_back(make_element_a());
    es.emplace_back(make_element_a());
    es.emplace_back(make_element_b());
    es.emplace_back(make_element_a());

    for(auto& e : es) e.call_f();
    // Will print: "a a b a".
}

2) Utilisez le mot clé virtual pour activer le polymorphisme d'exécution:

class element
{
private:
    virtual void f() { }

public:
    void call_f() { _f(*this); }
    virtual ~element() { }
};

void print_a(element& e) { std::cout << "a\n"; }    
void print_b(element& e) { std::cout << "b\n"; }

class a : public element
{
    void f() override { print_a(*this); }
};

class b : public element
{
    void f() override { print_b(*this); }
};

int main()
{
    std::vector<std::unique_ptr<element>> es;
    es.emplace_back(std::make_unique<a>());
    es.emplace_back(std::make_unique<a>());
    es.emplace_back(std::make_unique<b>());
    es.emplace_back(std::make_unique<a>());

    for(auto& e : es) e.call_f();
    // Will print: "a a b a".
}
0
Vittorio Romeo 2 janv. 2016 à 14:49

L'héritage est la classe de base du mal.

En masquant la relation entre ces objets dans l'implémentation interne d'une classe de handle, nous pouvons créer une interface commune à tous les objets même lorsqu'ils ne sont pas dérivés d'une base commune.

Voici une ré-expression de votre problème dans cette veine:

#include <iostream>
#include <memory>
#include <vector>
#include <type_traits>
#include <utility>

class A {};  // note! no inheritance at all
class B {};
class C {};

void f(A& a) { std::cout << "It is an A!\n"; }
void f(B& b) { std::cout << "It is a B\n"; }
void f(C& c) { std::cout << "It is a C!\n"; }

struct fcaller
{
    struct concept
    {
        virtual void call_f() = 0;
        virtual ~concept() = default;
    };

    template<class T>
    struct model final : concept
    {
        model(T&& t) : _t(std::move(t)) {}

        void call_f() override {
            f(_t);
        }
        T _t;
    };

    template<class T, std::enable_if_t<not std::is_base_of<fcaller, std::decay_t<T>>::value>* = nullptr >
    fcaller(T&& t) : _impl(std::make_unique<model<T>>(std::forward<T>(t))) {}

    void call_f()
    {
        _impl->call_f();
    }

private:


    std::unique_ptr<concept> _impl;

};

int main() {
    using namespace std;

    vector<fcaller> v{};
    v.emplace_back(A{});
    v.emplace_back(B{});
    v.emplace_back(C{});

    for(auto& obj : v)
    {
        obj.call_f();
    }

    return 0;
}

Production:

It is an A!
It is a B
It is a C!
0
Richard Hodges 2 janv. 2016 à 15:00