Domanda

I have an abstract Node class, derived in many subclasses, such as Color, Texture, Shape, Light, etc... containing my application user data. The data consists in a large tree of these nodes. Each Node subclass has a fixed number of children, which are of fixed types. For instance, a Material could have one Color child, and one Texture child. These are stored as std::shared_ptr's

Currently, each class derives a setChild method, which takes a Node as argument. Each implementation checks the type versus its children type with dynamic_cast accordingly, and sets it if successful.

I would like to implement a generic setChild method in Node, without the need for subclassing it. For this, each subclass would declare (or register) its children in the constructor, by giving a name string, a type string (corresponding to the subclass) and a pointer to a shared_ptr to a Node.

You can see the problem now:

  • I will use a **SubClass, upcast to an **Node, which I know is bad, but since each subclass has a type() method to uniquely identify the class, and since for each registered child I know its type, I can double check to avoid storing wrong pointer types with the double pointer.
  • I will do this in fact not with a **Node, but with a *std::shared_ptr<Node>. Here I'm not sure of doing something right.

Questions:

  • Is it possible to set a shared_ptr<Subclass> with a *shared_ptr<Node> even if I'm sure of the type?
  • Is this designed the way you would have done it?

Thanks,

Etienne

È stato utile?

Soluzione

If I understand you, the following is not safe:

boost::shared_ptr<SpecificNode> realPtr;
boost::shared_ptr<Node>* castPtr;

// castPtr = &realPtr; -- invalid, cannot cast from one to the other
castPtr = reinterpret_cast<boost::shared_ptr<Node>*>(&castPtr);
castPtr->reset(anything); // illegal.  castPtr does not point
                          // to a boost::shared_ptr<Node*>

Now you may get lucky and the memory may line up, but that is not valid C++.

If you are looking to extend the set of nodes in 3rd party plugins, the only solution is a series of dynamic casts, so lets see what we can do to make registration work with that.

Instead of trying to do everything with casts, consider using templates to do typesafe actions. Have an abtract base class which accepts a shared ptr to node, and either consumes it, or doens't consume it

(from here on out, I'm going to use T::Ptr instead of boost::shared_ptr, assuming that typedef is there. This is just for stackoverflow ease of reading)

class Registration
{
    public:
        typedef boost::shared_ptr<Registration> Ptr;

        virtual bool    consume(const Node::Ptr&) = 0;
};

template <typename T>
class SpecificRegistration : public Registration
{
    public:
        SpecificRegistration(T::Ptr& inChildPtr)
        : mChildPtr(inChildPtr)
        { }

        virtual bool    consume(const Node:Ptr& inNewValue)
        {
            if(!inNewValue) {
                mChildPtr.reset();
                return true; // consumed null ptr
            } else {
                T::Ptr newValue = dynamic_pointer_cast<T>(inNewValue);
                if (newValue) {
                    mChildPtr = newValue;
                    return true; // consumed new value
                } else {
                    return false; // no match
                }
            }
        }
    private:
        T::Ptr&    mChildPtr;
};

template <typename T>
Registration::Ptr registerChild(T::Ptr& inChildPtr)
{
    return make_shared<SpecificRegistration<T> >(inChildPtr);
}

// you can also register vector<T::Ptr> if you write an
// ArraySpecificRegistration class which uses push_back when
// it consumes a node

void Node::setNode(const Node& inNode) {
    for (RegistrationList::iterator iter = mRegistration.begin(); iter != mRegistration.end(); ++iter) {
        if (mRegistration->consume(inNode))
            return;
    }
    throw runtime_error("Failed to set any children of the node");
}

class SomeThirdPartyNode
: public Node
{
    public:
        SomeThirdPartyNode()
        {
            // all they have to write is one line, and mOtherTHing is
            // registered
            addChild(registerChild(mOtherThing));
        }
    private:
        SomeOtherThirdPartyNode::Ptr  mOtherThing;
};

Altri suggerimenti

shared_pr supposed static_pointer_cast and dynamic_pointer_cast that do what you want. However, given that you have a fixed set of node types, I highly recommend the visitor pattern. Given that you seem to be doing a scene graph, I am even more inclined to recommend it, as Visitor's best usage is with scene graphs.

class Material;
class Node;
class Color;
class Texture;
class Shape;
class Light;

class Visitor
{
    public:
        virtual void visit(const shared_ptr<Material>& inNode) = 0;
        virtual void visit(const shared_ptr<Color>& inNode) = 0;
        virtual void visit(const shared_ptr<Texture>& inNode) = 0;
        virtual void visit(const shared_ptr<Shape>& inNode) = 0;
        virtual void visit(const shared_ptr<Light>& inLight) = 0;
}

class Node
{
    public:
        virtual void accept(Visitor& inVisitor) = 0;
};

class Color
: public Node
, public boost::enable_shared_from_this<Color>
{
     public:
         virtual void accept(Visitor& inVisitor)
         {
             inVisitor.visit(shared_from_this());
         }
         ...
};

class Texture
: public Node
, public boost::enable_shared_from_this<Texture>
{
     public:
         virtual void accept(Visitor& inVisitor)
         {
             inVisitor.visit(shared_from_this());
         }
         ...
};

class Shape
: public Node
, public boost::enable_shared_from_this<Shape>
{
     public:
         virtual void accept(Visitor& inVisitor)
         {
             inVisitor.visit(shared_from_this());
         }
         ...
};

class Light
: public Node
, public boost::enable_shared_from_this<Light>
{
     public:
         virtual void accept(Visitor& inVisitor)
         {
             inVisitor.visit(shared_from_this());
         }
         ...
};

The purpose of this entire pattern is to do something similar to dynamic_cast, only it is done with a pair of virtual function calls instead of an arbitrary dynamic_cast. Node::accept is responsible for calling a function which knows the exact type of the object (i.e. Light::accept knows 'this' is a Light*). It then calls the visitor's visit function with the "correct" type information.

This system is designed to support many many algorithms which operate on a small set of types. For example, your setChild function

class SetMaterialChild
: public Visitor
{
    public:
        SetMaterialChild(Material& inMaterial)
        : mMaterial(inMaterial)
        { }

        virtual void visit(const shared_ptr<Color>& inNode)
        {
            mMaterial.mColor = inNode;
        }

        virtual void visit(const shared_ptr<Texture>& inNode)
        {
            mMaterial.mTexture = inNode;
        }

        virtual void visit(const shared_ptr<Shape>& inNode)
        {
            throw std::runtime_error("Materials cannot have shapes");
        }

        virtual void visit(const shared_ptr<Light>& inLight)
        {
            throw std::runtime_error("Materials cannot have lights");
        }


    private:
        Material&    mMaterial)
};



class Material
: public Node
, public boost::enable_shared_from_this<Material>
{
     public:
         virtual void accept(Visitor& inVisitor)
         {
             inVisitor.visit(shared_from_this());
         }

         void setNode(const shared_ptr<Node>& inNode)
         {
             SetMaterialChild v(*this);
             inNode->accept(v);
         }
         ...
};

This visitor pattern is hard to approach, initially. however, it is REALLY popular for scene graphs because it is extraordinarily good at handling a small set of node types, and it is the most typesafe way to approach the problem

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top