[C++] std::unique_ptr<Base> et std::make_unique<Derived> avec appel du constructeur de l'objet dérivé, le tout passé par argument

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Voilà maintenant 3 jours que je parcours le net à la recherche d’une réponse (Francophone et anglophone), même la doc du langage.

Voici le code que je suis en train de tester et ma question qui le succède:

#include <string>
#include <unordered_map>
#include <memory>

class BaseNode
{   
    private:
        std::string     m_identifier;

    protected:
        virtual void deleteNode() = 0;
    
    public:
        BaseNode(){}
        BaseNode(std::string const& p_identifier) : m_identifier(p_identifier){}
        BaseNode(BaseNode const& rhs){ m_identifier = rhs.getIdentifier(); }

        virtual ~BaseNode(){}

        std::string const& getIdentifier() const { return m_identifier; }
        void setIdentifier(std::string const& p_identifier) { m_identifier = p_identifier; }
};

class NodeHandler : public BaseNode
{
    protected:
        std::unordered_map<std::string, std::unique_ptr<BaseNode>>      m_handler;

        virtual void deleteNode() = 0;
        virtual void clear() = 0;
    
    public:
        NodeHandler(std::string const& p_identifier) : BaseNode(p_identifier) {
            m_handler.clear();
        }

        virtual ~NodeHandler(){}

        virtual void loadNewNode(BaseNode const& p_newNode) = 0;

        virtual BaseNode* getNode(std::string const& p_identifier) = 0;
};

class AssetNode : public BaseNode
{   
    private:
        std::string   m_filePath;

    protected:
        virtual void deleteNode() = 0;
            
    public:
        AssetNode(){}
        AssetNode(std::string const& p_identifier, std::string const& p_filePath) : BaseNode(p_identifier), m_filePath(p_filePath) {}
        AssetNode(AssetNode const& rhs) : BaseNode(rhs.getIdentifier()), m_filePath(rhs.getFilePath()) {}

        virtual ~AssetNode(){}

        std::string const& getFilePath() const { return m_filePath; }
        void setFilePath(std::string const& p_filePath) { m_filePath = p_filePath; }
};

class TextureNode : public AssetNode
{
    private:
        virtual void deleteNode() override {}
    public:
        TextureNode(){}
        TextureNode(std::string const& p_identifier, std::string const& p_filePath) : AssetNode(p_identifier, p_filePath) {}
        TextureNode(TextureNode const& rhs) : AssetNode(rhs.getIdentifier(), rhs.getFilePath()) {}
        
        ~TextureNode() { deleteNode(); }
};

class TextureHandler : public NodeHandler
{
    private:
        virtual void deleteNode() override {}
        virtual void clear() override { m_handler.clear(); }

    public:
        TextureHandler(std::string const& p_identifier) : NodeHandler(p_identifier) { clear(); }
        ~TextureHandler() { clear(); }

        virtual void loadNewNode(BaseNode const& p_newNode) override {
            m_handler.insert({p_newNode.getIdentifier(), std::make_unique<TextureNode>(p_newNode)});
        }

        virtual BaseNode* getNode(std::string const& p_identifier) override {
            return (TextureNode*)m_handler[p_identifier].get();
        }
};

int main() {

    TextureHandler t_handler("Texture Handler");

    t_handler.loadNewNode(TextureNode("Texture", "texture.png"));

    return 0;
}

En compilant avec g++ testnode.cpp -std=c++17 -Wall -Wextra -pedantic -O2 -o testnode.out j’obtiens la baffe dans la tronche suivante:

In file included from /usr/include/c++/8/memory:80,
                 from testnode.cpp:3:
/usr/include/c++/8/bits/unique_ptr.h: In instantiation of ‘typename std::_MakeUniq<_Tp>::__single_object std::make_unique(_Args&& ...) [with _Tp = TextureNode; _Args = {const BaseNode&}; typename std::_MakeUniq<_Tp>::__single_object = std::unique_ptr<TextureNode, std::default_delete<TextureNode> >]’:
testnode.cpp:86:97:   required from here
/usr/include/c++/8/bits/unique_ptr.h:831:30: error: no matching function for call to ‘TextureNode::TextureNode(const BaseNode&)’
     { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
testnode.cpp:70:9: note: candidate: ‘TextureNode::TextureNode(const TextureNode&)’
         TextureNode(TextureNode const& rhs) : AssetNode(rhs.getIdentifier(), rhs.getFilePath()) {}
         ^~~~~~~~~~~
testnode.cpp:70:9: note:   no known conversion for argument 1 from ‘const BaseNode’ to ‘const TextureNode&’
testnode.cpp:69:9: note: candidate: ‘TextureNode::TextureNode(const string&, const string&)’
         TextureNode(std::string const& p_identifier, std::string const& p_filePath) : AssetNode(p_identifier, p_filePath) {}
         ^~~~~~~~~~~
testnode.cpp:69:9: note:   candidate expects 2 arguments, 1 provided
testnode.cpp:68:9: note: candidate: ‘TextureNode::TextureNode()’
         TextureNode(){}
         ^~~~~~~~~~~
testnode.cpp:68:9: note:   candidate expects 0 arguments, 1 provided

Voici donc ma question: Comment puis-je procéder afin qu’en passant n’importe quel objet dérivé de BaseNode à la fonction TextureHandler::loadNewNode(BaseNode const& p_newNode) il utilise le constructeur de copie de l’objet dérivé et non celui de la classe abstraite BaseNode (Ligne 86) ? Je pense que j’ai dû louper une marche quelque part. Je ne souhaite pas utiliser un pansement Template, sinon la hiérarchie de ces objets n’aurait aucun sens.

Désolé pour le pavé(César)

Merci d’avance.

EDIT: Le seul moyen que j’ai trouvé pour régler le problème est de déclarer dans chaque handler dérivé de NodeHandler une fonction loadNewNode() et getNode() avec directement le type dérivé en paramètres. Dans ce cas là, la classe NodeHandler ne fait qu’encapsuler la déclaration du std::unique_ptr et l’appel à la fonction clear() du std::unordered_map<> dans son constructeur. Cela casse un peu le rythme de la POO non ?

+0 -0

C’est le problème classique de la copie avec les classe à sémantique d’entité. Si tu fais une recherche sur le forum, tu devrais trouver des explications plus détaillé (en particulier dans la discussion sur le cours C++).

Pour résumer le problème, quand tu écris :

std::make_unique<TextureNode>(p_newNode)})

comment sais tu que p_newNode est de type TextureNode et donc que c’est une copie ?

En C++, comme tu as un typage à la compilation, si tu veux faire une copie d’un type polymorphique en écrivant :

make_unique<???>(object)

il faut connaître le type dynamique de object (ie le type concret de object pendant l’exécution) à la compilation, ce qui n’a pas trop de sens.

Du coup, il faut résoudre dynamiquement le type, par exemple avec un code comme ça :

if ("object est de type OBJECT1") 
    make_unique<OBJECT1>(object)
else if ("object est de type OBJECT2") 
    make_unique<OBJECT2>(object)
etc.

avec "object est de type OBJECT1" qui peut être un dynamic_cast ou une fonction virtuelle type() ou autre.

Une autre solution est d’utiliser le design pattern clone https://en.wikipedia.org/wiki/Prototype_pattern. Mais c’est en fait exactement le même principe (résolution dynamique du type), sauf que cela utiliser le mécanisme des fonctions virtuelles plutôt que dynamic_cast.

Dans tous les cas, on a 2 problèmes :

  • surcoût a l’exécution
  • perte de maintenabilité du code

C’est pour cela qu’on dit en général que la copie n’est pas compatible avec la sémantique d’entité.

Pour résoudre ce problème :

  • soit ne pas faire ça et corriger le design
  • soit accepter le coût des solutions précédentes
+2 -0

Bonsoir,

Je te remercie de ta réponse, un plaisir que ce soit toi, je suis surpris d’ailleurs lol

J’ai effectivement lu le cours de C++ en instance d’écriture ici même, cette partie en particulier je n’étais donc pas très loin :P

En effet, le typage s’opère à la compilation et y pensant c’est vrai que la copie n’est pas compatible avec la sémantique d’entité mais j’ai dû sauter une ou deux ligne à la lecture là je crois :/

Dans mon cas, ce serait un coût de maintenabilité du code si je garde ce modèle qui n’est pas dénué de sens dans ma vision des choses où je vais pouvoir stocker tous les objets dits "lourds" (Textures, Audio, Polices d’écriture, etc…) dans des handlers acceptant des dérivés d’une classe mère à toutes les entités du programme. Sachant que les handlers possèdent des comportements identiques (Allouer l’espace, donner un accès à un asset) je me suis dit que c’était légitime de coller des interfaces à un handler abstrait.

Très bien, content de cette réponse, je vais réfléchir à une solution en prenant en compte ces informations.

Merci monsieur !

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte