Jump to content

  • Log In with Google      Sign In   
  • Create Account

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


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
19 replies to this topic

#1 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 01:55 PM

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


Sponsor:

#2 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 02:02 PM

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


#3 kloffy   Members   -  Reputation: 918

Like
0Likes
Like

Posted 12 June 2013 - 03:01 PM

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.

Edited by kloffy, 12 June 2013 - 03:14 PM.


#4 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 03:09 PM

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


#5 NightCreature83   Crossbones+   -  Reputation: 2826

Like
0Likes
Like

Posted 12 June 2013 - 03:26 PM

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.

Edited by NightCreature83, 12 June 2013 - 03:51 PM.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, Mad Max

#6 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 03:32 PM

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

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

o3o


#7 NightCreature83   Crossbones+   -  Reputation: 2826

Like
0Likes
Like

Posted 12 June 2013 - 03:50 PM

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.

Edited by NightCreature83, 12 June 2013 - 03:53 PM.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, Mad Max

#8 frob   Moderators   -  Reputation: 21318

Like
1Likes
Like

Posted 12 June 2013 - 04:06 PM

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.


Check out my personal indie blog at bryanwagstaff.com.

#9 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 04:06 PM

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


#10 phantom   Moderators   -  Reputation: 7278

Like
2Likes
Like

Posted 12 June 2013 - 04:44 PM

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.

#11 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 12 June 2013 - 05:23 PM

Uhm. The context variables will be just like normal variables. They are simply stored outside the class object.I dont see how there are hidden dependencies, the class tells you it needs certain data to be provided by the user and will produce a compile time error if they are not there. You could remove the context: thing and ot would work exactly as before, except that the object will be bigger. The problem here might be the implicit keyword i added as an extra, that can be made not so evil by requiring context variables to be marked as usable by implicit variables. Ideally they should be a single context class you inherit to lets say add heeap allocation or a logger to pass along. Its better than using globals. And if you are going to pass a class containing all your loggers and managers to every single metgod you call, might as well hide them, especially when calling 10 methods of the same object at once.

o3o


#12 phantom   Moderators   -  Reputation: 7278

Like
0Likes
Like

Posted 12 June 2013 - 05:52 PM

They aren't 'like normal variables' at all as you now have effectively 'magic' happening in the background.

Take your example to clear things up; when you call foo.setAlloc() it looks like you are performing an operation on the class but you aren't, you are doing something to a local variable which isn't even named in the expression.

Right away you have muddied the interface ("Does this function do something to foo itself? or to a context class?") and remove the clarity of what dependencies each function has ("I've created a context, I've set 4 things, I call two functions... which functions depend on what things from the context?") and, based on the reading, you need to provide ALL the details for the classes dependencies up front before you can call any functions (You no longer know what the functions are using, therefore in order to be safe all dependencies, regardless of the function you are calling them needing them are now required to be resolved.)

So, if you create an instance of a class Foo on the stack which requires a Context consisting of 4 things before you can call any function on foo you have to ensure the whole classes context dependencies are completed because you have no way of knowing what dependencies the function requires as they are now hidden away from you.

The argument of 'what if you need to pass all your loggers and managers to every single function' fails the 'what if' test right away - you are attempting to fix a 'problem' which only exists in badly designed code. In every practical situation I've come across the parameters can be either provided at construction of an object or a very limited subset of external systems are required at any given point.

#13 Álvaro   Crossbones+   -  Reputation: 13317

Like
1Likes
Like

Posted 12 June 2013 - 07:38 PM

At work we have a gigantic program that relies on an object (let's call its class "Environment", although that's not its actual name). Everything gets passed a pointer to an Environment, and from the Environment you can get access to all sorts of data. There is a global Environment that is used as default in many places if you don't specify one explicitly.

 

The resulting code is a stinking pile of dung. Everything depends on everything else and it would be impossible to have more than one object of type Environment, because the global one is used in too many places. The program is so large that nobody understands what all parts do, and refactoring this would require rethinking every part of the code, so nobody does it.

 

In my opinion this type of Environment object is an antipattern, closely related to the big ball of mud. It is much better to make the dependencies explicit by passing pointers or references to all the objects needed, even if it might seem tedious at times.



#14 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 13 June 2013 - 03:59 AM

Editor wont scroll on mobile so ill put this at start instead of end (storing dependencies inside the objects is unacceptable because of the not necessary pointer added, thats just wrong)I was thinking of the context to be used for things which should always be there, jusy as if they were passed and stored during construction. The context should be integral to the functioning of the class, and every method could potentially use them, just like it is with normal members. Any method can use any of the normal members without needing to specify which ones. This would obviously suck for a big class, but for a smaller well defined class it would work. Leaving pieces of the context out would be like not initializing a member of a class and then calling a method - the object is likely in invalid state and will fail. Of course you can just not initialize one of them and see what happens, just like with normal members.

The other alternative is using the above mentioned environment class that gets passed everywhere, and you need many of those dpending on subsystem. Passing individual context thingiea like loggers and all separately wouldnt b nice when you decide to add a new one and have to change every single method in the program - instead of just a single CommonContext class that adds a logger or something to be passed.

o3o


#15 Álvaro   Crossbones+   -  Reputation: 13317

Like
0Likes
Like

Posted 13 June 2013 - 07:57 AM


The other alternative is using the above mentioned environment class that gets passed everywhere, and you need many of those dpending on subsystem. Passing individual context thingiea like loggers and all separately wouldnt b nice when you decide to add a new one and have to change every single method in the program - instead of just a single CommonContext class that adds a logger or something to be passed.

 

That's the kind of thinking that will get you in trouble. If you decide to add a new object in your program, you need to think about what parts of the program need to know about it and pass it where appropriate. If you add it to the CommonContext, you effectively have no design to your program and every part of the program can access every other part. This makes it hard to think about individual parts, prove that they are correct or test them. It also makes it impossible to reuse some part of your program in another program.

 

Another architecture that might be worth thinking about is signals and slots. This allows each component to not know much about the others. They simply send signals that can be picked up by other components and they have slots where a signal can be plugged in, so when the signal is sent they do something in response. The program initially instantiates all the objects and plugs signals into slots. I have used this in a couple of projects and I like it quite a bit.



#16 Waterlimon   Crossbones+   -  Reputation: 2565

Like
0Likes
Like

Posted 13 June 2013 - 12:17 PM

I was thinking the CommonContext class for things which are practically globals, allocators and loggers (not much else comes to mind :c)

 

This might be too complex in its current form to be actually worth it.

I still think it would work for the above mentioned uses, seeing as you can always split the commoncontext class to have different dependencies for different subsystems or simply provide different values for the logger or allocator (one logger for physics another for networking)

 

In some of the examples i gave in the first post, the physics manager acts as a kind of "RAM". Your game objects only use it for allocation, freeing and accessing. Operations like collision detection between all physical objects are done by outside code. Id like the physics manager act similiar to RAM. I dont need to pass around references to "RAM" when i want to access my objects (because RAM is a global, how evil is that D:)

 

So, if we made RAM less evil, so that its an object (yay, now we can theoretically have multiple RAMs and write code that is meant to run on multiple machines in a single project!) and needed to access any object, how would you like to handle it?

 

I would just make all my objects inherit from "RAMContext", and before creating the "Game" or whatever class in main, set the context to a RAM object. Then all my objects could simply assume the variable containing RAM to be there, and access objects like ram.this and ram.that, no need to pass RAM to every single method. (lets ignore the fact that we need to explicitly access all objects via the RAM object)

 

I want my C++ to not have evil globals like "RAM", it hides a major(eh?) problem with the language! ,_,


o3o


#17 LorenzoGatti   Crossbones+   -  Reputation: 2704

Like
0Likes
Like

Posted 14 June 2013 - 02:08 AM

I think what's missing is proper use of objects. The OP tries to masquerade
class Foo{
public: 
SomeType doSomething(BoringManager1 m1, BoringManager2 m2, NiceParameter p);
}
as
class Foo{
public: 
SomeType doSomething(NiceParameter p);
}
but the normal way to "hide" the managers is making them the fields of an object:
class SomethingDoer{
private:
BoringManager1 m1;
BoringManager2 m2;
public: 
SomethingDoer(BoringManager1 m1, BoringManager2 m2);
SomeType doSomething(Foo f, NiceParameter p);
}
In other words, adding a SomethingDoer class to represent the "context" of doSomething() allows the client code to ignore the managers as much as possible while providing the proper means to take care of manager lifetime and associate them together.
Produci, consuma, crepa

#18 Madhed   Crossbones+   -  Reputation: 2978

Like
0Likes
Like

Posted 14 June 2013 - 06:30 AM

Hah! I was thinking about the exact same thing before and also wanted to name it context. Sometimes static members are used in this manner. Object counters, common configuration data, etc. The problem ist that this static state is global for all instances of the class. Sometimes you only want to share state between a subset of all instances.

 

But there is no need to add a new concept to C++, you can already do that with what you have at hand. You can either create a context strcuture and pass that to the constrcutor or you can create object collections that hold shared data. I much prefer the latter, as it keeps the class focused on itself and pushes responsibility for interaction into a higher level.



#19 iMalc   Crossbones+   -  Reputation: 2306

Like
2Likes
Like

Posted 14 June 2013 - 03:41 PM

http://en.wikipedia.org/wiki/Dependency_injection. 'nuf said.


"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms

#20 Telastyn   Crossbones+   -  Reputation: 3726

Like
0Likes
Like

Posted 15 June 2013 - 07:55 AM

+1 for just pass it in and/or dependency injection.

 

There's no reason for this to be part of the language. (Though C++'s lack of reflection capabilities limits your options as far as auto-magical solutions go).






Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS