Handling Multiple Child Block Types
In many situations a Block
might have many different child blocks. The getChildren()
function introduced in the previous section will always return all child blocks, independent of their type. To get only blocks of certain types one would have to iterate through all child blocks and use a dynamic cast to test for the different types. Fortunately Schnek provides a mechanism to simplify this task. The two class templates BlockContainer
and ChildBlock
supply the framework for this mechanism.
Let’s extend the example from the previous section. There we introduced a Particle
block that served as a child block to the NBody
simulation block. We used the getChildren()
function to obtain a list of all child blocks. This list contained members of type pBlock
which is equivalent to boost::shared_ptr<Block>
. Let’s now explicitely declare that Particle
blocks should be treated child blocks of NBody
. We do this by first making sure that NBody
inherits from the BlockContainer
class.
class NBody : public Block, public BlockContainer<Particle> { ... };
The second step is changing the inheritance of the Particle
block from the Block
class to the ChildBlock
class.
class Particle : public ChildBlock<Particle> { ... }; typedef boost::shared_ptr pParticle;
For convenience we have also added a typedef to a shared pointer of the Particle
type. One more change is needed to make sure that everything gets initialised properly. We need to change the call to evaluateParameters()
in the main()
function to a call to initAll()
.
mysim->initAll();
initAll()
will call evaluateParameters()
but will also call a number of other functions to initialise the block hierarchy. This will be detailed in the following section. With these modifications in place, we are now able to rewrite the runSimulation()
function.
void NBody::runSimulation() { Array<double,3> force(0.0,0.0,0.0); for (double time=0.0; time<=totalTime; time+=dt) { BOOST_FOREACH(pParticle particle, childBlocks()) { particle->advance(force, dt); } } BOOST_FOREACH(pParticle particle, childBlocks()) { std::cout << "Child: " << particle->getName() << std::endl; particle->display(); } }
The BlockContainer
class template provides a member function childBlocks()
. This function returns an iterator range over shared pointers to the template parameter. This means that BlockContainer<Particle>::childBlocks()
will iterate over pParticle
pointers. Notice how we have eliminated the type cast from pBlock
to pParticle
.
This is all well and good, but how does this help us in our goal to have multiple types of child blocks? It turns out that it allows us to create another type of ChildBlock
. If you wanted to add a Force
class that returns the force on a particle depending on its position, then you could create something like this.
class Force : public ChildBlock<Force> { private: Array<double,3> center; double k; protected: void initParameters(BlockParameters ¶meters) { parameters.addArrayParameter("center", center); parameters.addParameter("k", &k); } public: Array<double,3> getForce(const Array<double,3> &pos) { return k*(center-pos); } }; typedef boost::shared_ptr<Force> pForce;
This is a simple linear force that will implement a harmonic oscillator. The NBody
class needs to know that it should act as a container for this class, so we add another inheritance to it.
class NBody : public Block, public BlockContainer<Particle>, public BlockContainer<Force> { ... };
Now we have introduced a small problem. Both BlockContainer<Particle>
and BlockContainer<Force>
define a childBlocks()
member function. Fortunately C++ allows us to resolve the conflict by explicitely stating which function we want to call. The runSimulation()
function can now be modified as follows.
void NBody::runSimulation() { for (double time=0.0; time<=totalTime; time+=dt) { BOOST_FOREACH(pParticle particle, BlockContainer<Particle>::childBlocks()) { Array<double,3> force(0.0,0.0,0.0); BOOST_FOREACH(pForce F, schnek::BlockContainer<Force>::childBlocks()) { force += F->getForce(particle->getPos()); } particle->advance(force, dt); } } BOOST_FOREACH(pParticle particle, BlockContainer<Particle>::childBlocks()) { std::cout << "Child: " << particle->getName() << std::endl; particle->display(); } }
As before BlockContainer<Particle>::childBlocks()
returns an iterator range over pParticle
pointers, while schnek::BlockContainer<Force>::childBlocks()
now returns an iterator range over pForce
pointers.
All we now need to do is to register the Force
class with the setup file parser.
blocks.registerBlock("NBody").setClass<NBody>(); blocks.registerBlock("Particle").setClass<Particle>(); blocks.registerBlock("Force").setClass<Force>(); blocks("NBody").addChildren("Particle")("Force");
Here you can also see how the addChildren()
function can be chained to add multiple child block types in one go. This will now allow us to write a setup file such as the following, containing a particle and a force.
dt = 0.01; totalTime = 100; Particle A { posx = 0.0; posy = 0.2; posz = 0; velocityx = 0; velocityy = 0; velocityz = 0; mass = 10; } Force force { centerx = 1; centery = 0; centerz = 0; k = 0.5; }
We might decide that all this is not enough. We want multiple different types of forces that use different formulae to calculate the force on a particle. We can do this by using polymorphism. Instead of defining just a single Force
class, let’s say we want to have a LinearForce
and a NonLinearForce
. However, in the loop over all the forces, we don’t want to distinguish between the two. We can do this by creating a type hierarchy.
class Force : public ChildBlock<Force> { public: virtual ~Force() {}; virtual Array<double,3> getForce(const Array<double,3> &pos) = 0; }; typedef boost::shared_ptr<Force> pForce; class LinearForce : public Force { private: Array<double,3> center; double k; protected: void initParameters(BlockParameters ¶meters) { parameters.addArrayParameter("center", center); parameters.addParameter("k", &k); } public: Array<double,3> getForce(const Array<double,3> &pos) { return k*(center-pos); } }; class NonLinearForce : public Force { private: Array<double,3> center; double k; double d; protected: void initParameters(BlockParameters ¶meters) { parameters.addArrayParameter("center", center); parameters.addParameter("k", &k); parameters.addParameter("d", &d); } public: Array<double,3> getForce(const Array<double,3> &pos) { Array<double,3> delta = center-pos; return k*delta / (delta.sqr() + d*d); } };
The Force
class is now an abstract base class for our hierarchy. It still inherits from ChildBlock<Force>
so that the NBody
class can access all children of this type. The two classes LinearForce
and a NonLinearForce
simply inherit from Force
and implement the abstract getForce()
method. Again, we can register these classes with the setup file parser.
blocks.registerBlock("NBody").setClass<NBody>(); blocks.registerBlock("Particle").setClass<Particle>(); blocks.registerBlock("LinearForce").setClass<LinearForce>(); blocks.registerBlock("NonLinearForce").setClass<NonLinearForce>(); blocks("NBody").addChildren("Particle")("LinearForce")("NonLinearForce");
The runSimulation()
function does not need any modification. By the virtue of polymorphism the correct getForce()
function will be called. A possible setup file could now look like this.
dt = 0.01; totalTime = 100; Particle A { posx = 0.0; posy = 0.2; posz = 0; velocityx = 0; velocityy = 0; velocityz = 0; mass = 10; } LinearForce linear { centerx = 1; centery = 0; centerz = 0; k = 0.5; } NonLinearForce nonlinearA { centerx = 0; centery = 1; centerz = 0; k = 2; d = 0.1; } NonLinearForce nonlinearB { centerx = 1; centery = 0; centerz = 1; k = 1; d = 0.1; }
The code for this example can be downloaded here and the setup file can be found here.