A new feature i want in my C++: Contexts

Started by
18 comments, last by Telastyn 10 years, 10 months ago

Id call it something like 'context' with a fancy word added to it somewhere.

What it attempts to solve:

Many classes need some context to function properly. Access to file managers, heap allocation, physics simulators, objects managing this and that, all kinds of managers. In order for the instances of these classes to operate with their data, they need access to these 'managers' which manage the actual data - their members are usually just indices/references to the data managed by these managers, and thus useless if the correct manager is not available.

So, for these classes to function, they need:

-Managers

-Indices to data managed by the managers

So, how do we ensure the object has access to the managers it needs?

a) The object has a reference to the required managers

*Code that uses the objects needs no knowledge of the resource pools/managers it needs

b) The managers are passed to the object from the user of the class

*In many cases, the using code is aware of at least most of the managers - storing them in the class would be a waste of memory

There is no problem with (a), thats simple. (b) would be used for example in a class that manages all your game objects. All of them probably use the same physics, graphics, files... so each storing their own references to the managers they need would be evil. So, when the game object manager does operations on your game objects, the required managers would be passed there by the game object manager.


class GameObjectManager
{
void doOperation()
{
    for object in objects
    {
        object.operation(mgr1,mgr2);
    }
}
vector<GameObject> objects;
ThisManager& mgr1;
ThatManager& mgr2;
}


class GameObject
{
void operation(ThisManager& mgr1, ThatManager& mgr2) //managers passed in instead of stored
{
    mgr1.getObject(r1).doSomething(mgr2.getObject(r2));
}

ThisComponentRef r1;
ThatComponentRef r2;
}

This is ok. But its not pretty. What happens when you need not only 2 (which already is too much) but 10 managers for the GameObject to access all its members? For a single method, not a problem, but if you have 10 methods, that means youd be passing 10 arguments to each of them, even if you were calling them one after another? Would it not feel redundant?

What we want to do, is to create a context object, that stores all the variables that the accessor knows about and that the object needs to work.


void doOperation()
{
    for object in objects
    {
        object::context c(PhysicsMgr,FileMgr,SoundMgr,Allocator...) //optionally the class containing doOperation() could get these variables from its own context - someone is calling doOperation, and is likely aware of at least some of the above managers, so why store them in the class of doOperation()?
        object.operation(c,1,true,"hue"); //is this similiar to multiple dispatch? we need both object and the context to do the operation...?
        object.doAnotherOperation(c,...);
        object...
    }
}

Now, that is already a lot of saved writing. As mentioned, you might want to 'chain' the contexts, as in doOperation() would itself receive a context, and that context would be used to fill in the context of object - something like a heap allocator would probably be used like this.

So, a method of a class that needs managers (to make its resource indices accessible), needs:

-The class (for the indices to managers and data stored directly in the class instance)

-The context (supplied by the caller of the methods of the object)

-The managers

You could say you call the method like (GameObject,Context).operation() but of course thats not possible. Maybe with some template magic.

A class would be composed of:

-Its methods

-Its directly stored variables (primitive types for example)

-Its indirectly stored variables, an index to a manager (RAM is just a global manager?)

The context is not store in the class, it is created in the scope where the classes methods are accessed, although the context should be described within the class (what types of variables are needed as context)

If we take this further, outside what is already possible within the constraints of the language, this could be even more streamlined.

For example:

-Instead of creating a separate 'context' class, the context variables would become a part of the class. This gets rid of all unneeded parameters of methods.


class Object
{
public:
void operation(){};
private:
PhysicsComponentIndex index;
...
context: //does not add to sizeof(class), stored in scope of access and must be set each time before access
PhysicsMgr& mgr;
void setPhysicsMgr(PhysicsMgr% mgr){[context/this?]->mgr=mgr};
implicit HeapAllocator& alloc; //yay a keyword, let me explain
}

-An implicit keyword (as in the above piece of code). This means the object is automatically grabbed from the context of the object whose method is using the object. If GameObjectContainers method uses GameObject, the HeapAllocator of GameObjectContainer will be automatically stored in the context of GameObject. This is to reduce typing, which is the whole point of this whole post.

-Ability to combine the context of an object and the object itself, so that code unaware of the required context can process the objects.

Lets use a too long keyword for this, "indepenent"! YAY!


Object& A=getASomehow(); //context object created to scope of this method, implicit context variables filled if possible
A.setHeapAllocator(alloc); //lets say we know this wont work implicitly for whatever reason
A.setPhysicsMgr(physics);

independent Object B=A; //takes the context,stored in the scope of this method, and puts them in an object that contains the context in itself (instead of the context being only in the scope of the method)

RandomFunctionLivingInUnawareness(B); //the function must specify to take indepenent object for this to work. If context is not used by the object, everything will work like before.

-The context of an object is of course affected by inheritance. If i want an object that can use my heap allocator, i can inherit from it. This kind of 'pure' context inheritance where all you inherit is the context description might be better to be seen as separate from normal inheritance, as it doesnt add anything to the class, just states its requirements.

-Method/function specific contexts. I already described each class having a context, but it would be nice to have a 'free' context object, which basically states that "for each object accessed after me in the body of this method, use my variables to fill in all the implicit context variables of those objects". example:


class baaar
{
foooo()
{
    std::context<HeapAllocator&> c(alloc); //each object after this will fill their implicit context variables with alloc, instead of using the allocator of baaar.

    MyObject meh(); //implicitly uses alloc, assuming MyObjects context needs a HeapAllocator
    meh.doStuff(); //can use alloc to allocate stuff
    c.magicallyInvalidate(); //now baaars context will be used
    meh.doOtherStuff(); //still uses alloc - the context was created already
    MyObject dur(); //uses otherAlloc
}
context:
HeapAllocator& otherAlloc;
}

The benefits of something like this will grow as more methods are called and more 'managers' and implicit stuff like heap allocators are used.

Im sure there are other uses than what ive described for a feature like this, as well as obviously making for a more streamlined way of writing code, with all those loggers and allocators and whatnot not in the way anymore.

Discuss.

o3o

Advertisement

To clear it up a bit, here is what the user sees, and what happens in parantheses.


class Foo
{
public:
    void operation(((FooContext c))) //parameter c not visible
    {
        Object m; //has implicit HeapAllocator& alloc;
        ((ObjectContext mc;))
        ((mc.alloc=c.alloc))
        m.doSomething()
        ((=m.doSomething(mc)))
    }
context: //i think a "private context" and "public context" etc. should be added
    HeapAllocator& alloc;
    setAlloc(blah blah){blah};
}
    
Foo foo();
((FooContext c;))
foo.setAlloc(MyHeapAllocatorThing);
((= c.setAlloc(...)))
foo.operation();
((= foo.operation(c), parameter hidden in method sig.))

o3o

It sounds like you would enjoy Scala's implicit parameters. I am somewhat torn on such features. I can certainly see where you're coming from, but - more often than not - explicit is better than implicit.

Edit: The video "Typeclasses in Scala with Dan Rosen" discusses a more realistic example built around serialization. The final solution makes use of implicit parameters.
This is not as implicit as the system described in your link. Only context variables can be implicitly grabbed (maybe they need to be marked as grabbable, or be public), and the 'free' context still is explicitly telling that the variables can be used implicitly.

Randomly grabbing any variable of the type might cause bugs and other nasty things, but here you will only have a well defined set of variables which should not cause problems.

o3o

How about you create a resource which is passed to the object on construction. This resource contains pointers/references to all managers in the game and accessors to get them. The object stores a reference to this object and all you need to do is care for the fact that all pointers in the resource need to outlive the objects it is passed into, which should be the case anyway as the system pointers it contains needs the objects to work.

If you then use transient interfaces that take a reference to this resource pointer you can hide away all the nasty manager access code in an interface that gets initialised:
class Resource; //Holds all manager pointers/references

class SimpleInterface
{
public:
    SimpleInterface() {}
    ~SimpleInterface() {}

    bool initialise(const Resource* resource); //This can take more paramater like the indexes or resource references needed

    //Add accessor members here to get the data you want
    const std::string getPlayerName()
    {
        if (m_resource->getPlayerManager())
        {
            return m_resource->getPlayerManager().getPlayer().getName();        
        }

        return "Player One";
    }
private:
    Resource* m_resource;
};

class System
{
public:
    void Update()
    {
        SimpleInterface simpleInterface;
        if (simpleInterface.initialise(m_resource) )
        {
            std::string playerName = simpleInterface.getPlayerName();
        }
    }
};
This way all your glue code doesn't have to touch most of the actual functioning code.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

But that adds 4/8 bytes to the object size :c

And it doesnt have the fancy implicit context vars o,o

o3o

But that adds 4/8 bytes to the object size :c

And it doesnt have the fancy implicit context vars o,o

Hey I could also have written this in system instead which adds nothing to the object size of System:
class System
{
public:
    void Update()
    {
        SimpleInterface simpleInterface;
        if ( simpleInterface.initialise( Resource::GetResourcePointer() ) )
        {
            std::string playerName = simpleInterface.getPlayerName();
        }
    }
};
Not saying the singleton implementation is cleaner or even preferred but there are ways around the fact that a system would have to store a resource pointer.
The fact is with the transient interface is that over time you write less code as well as you are reusing these everywhere where you would need access to the functionality it exposes.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

Looks like all you've done is add hidden dependencies.

You will still need to provide the things the class depends on, that did not go away at all.

You have only moved the point of specification from a function parameter into a local variable. And then you further complicated things by making some global variables (an additional hidden dependency) and pulling from those when a local value was not stated.

I much prefer the explicit parameter. If I need a bunch of them I'll build my own struct of pointers and pass that around.

Its possible to do this with current c++ features, but its just the syntactic sugar to make it less writing and more well defined that i want.

I want the almost-global dependencies like all kinds of managers to be accessible without needing to pass them to every single method or storing references to them where its not needed. Currently these kinds of things actually are easiest to use as globals which is evil.

o3o

I much prefer the explicit parameter. If I need a bunch of them I'll build my own struct of pointers and pass that around.


This.
1000x this.

Hidden dependencies are the devil.

They make testing harder, they make code harder to reason and they obscure what is going on; a simple function can end up doing far more than you think because it's pulling dependencies which aren't shown and doing work in them.
(Not to mention the inherent memory access issues which makes my shudder just thinking about it)

Code is for reading more than writing; being clear about what is going in is always the best goal.

This topic is closed to new replies.

Advertisement