Rephrasing the problem a bit:
- I have an instance of
MySpecialObject
- an instance of
JLP
wants to call the methods of MySpecialObject
- an instance of
BEV
wants to read the public data of MySpecialObject
- the instance of JLP shouldn't be able to read public data and the
BEV
shouldn't be able to call member functions
This should be as type-safe as possible.
A solution I see involves wrappers :)
- you write the code for
MySpecialObject
as you would normally, ignoring this extra requirement
- you write a wrapper for each aspect of your class you wish to restrict. Say,
MySpecialObject_Read
and MySpecialObject_Execute
.
- these wrappers forward the requests (method calls, getter/setters) to an underlying
shared_ptr
of MySpecialObject
- your
MySpecialObject
, MySpecialObject_Read
and MySpecialObject_Execute
classes each have (as needed) a "convert to ..." method. This method returns an appropriate wrapper over the underlying MySpecialObject
This solution provides a type-safe accessor with the desired limitations.
Each client class can choose what kind of access it needs.
(And since you write these classes, you know what access you need.)
If that is not acceptable, you could add factories that only create wrappers with certain limitations depending on a "token". That token might be the RTTI information of the calling class instance, although this could be abused.
Does this solve the original problem?
EDIT:
Remember that since you are the programmer you can instantiate an A
whenever you want. By adding your class to a friend
list or whatever...
What this solution provides is a clearer interface. Removing the explicit conversions probably makes it safer but less flexible. But, since they are explicit, you can easily look for them in the code and treat them as signs your architecture has a flaw somewhere.
Specifically, you can have code like this:
shared_ptr<A> * a = new A(parameters);
A_read aRead(a);
A_execute aExec(a);
A_write aWrite(aExec);
logger->Log(aRead);
view->SetUserData(aWrite);
controller->Execute(aExec);
Here there is an explicit conversion between an execute wrapper and a write wrapper, but you can decide on this based on your specific requirements.
But, with little effort (knowing which conversions are valid), just by looking at the call locations you can see that (with confidence!):
- the
logger
will not change the state of your A
- the
view
will not call methods on your A
(other than setters)
This is true even if those particular method calls end up calling hundreds of other methods, more than you'd like to examine by hand.
At the cost of a few thin wrappers you gain the ability to see at a glance what a particular function call will do with the parameters you send. This may help a lot during debugging by helping you eliminate some branches from your investigation and, in general, would help people trying to understand the program.
I couldn't really find other reasons for using this ACL idea, at least ones where the costs don't outweigh the benefits. However, it does seem more intuitive than the visitor solution mentioned in the other answer.
It's not a bad mental model to use as a guide but unfortunately for historical / technical reasons C++ headers conflate interface and implementation in ways that this simple mental model doesn't fully capture.
In order to make good physical design decisions when things get a little more complex, it is generally necessary to understand the way headers work in C++ at a more detailed level.
To give an example: from your API view, a header file should not include the definition of a private implementation class. In practice however, such class definitions will often appear in a header file since the compiler will need to know their size if they are contained in any public classes. Techniques exist to break this kind of dependency but they generally impose a cost in terms of code complexity, performance or both. Understanding when it's appropriate to use one of these techniques requires a deeper understanding of how headers work in C++.
Best Answer
Separate Texture interface from implementation:
Your load function:
Client code can now work with textures, without caring that a Texture is an abstract class, and the Texture interface doesn't mention anything about setting values into the object:
Note: the EditableObjectTag class is an aspect-based design: it adds an editable interface on it's template argument.