the problem you
have shown with the example is not related to the class B, but to the
derived classes of A. That is the primary source where you should be
looking at. What B does or does not is not relevant, AFAICT.
Hi Abhishek,
Even if you don't like my idea, I'm glad you're taking the time to
talk about it. It gets clearer.
So thanks!
A is not necessarily a generic object intended to be published in a
library that anyone can call for any reason. Most applications have
architectures in layers, where only certain objects participate with
others. If A.cpp and B.cpp are the only files that include A.h, you
can't say a major decision of what to expose in A doesn't affect B!
So think of A and B as being designed specifically to work together.
If it helps you conceptually accept that, then maybe B should be a
friend or A should be a member class of B. Then every decision about
the design is relevant to what B does or does not do. I might change
the sample code.
As I think, the problem you are
addressing is functions that are part of an interface but have a
particular call order.
Actually, it's not the problem I'm looking at. I merely gave call
ordering as another example of a contract one might have in one's mind
that C++ has no intrinsic support for. A naive contract would be
implemented by comments! We both agree it's better to restructure
your objects so the ordering requirement is met by how the objects are
naturally used. Sometimes it means you might make 3 objects where
before you had 1, and it might seem more complicated than "just
trusting" the callers... but it's almost always worth it to enforce
the rule in a better way than a comment.
one should enforce the design constraints but not
using const-correctness. You are talking of a valid problem but the
solution is not appropriate.
Let's get away from function ordering and onto the *actual* constraint
I am trying to solve. It really is about not letting a method trigger
another, even indirectly. You may not think it is an interesting
contract, but I have examples in which it is interesting to me. At
the implementation level, I'm trying to get an "object mode" language
feature that C++ does not have, which might look like:
__modes(red,yellow,green)
class Stoplight
{
// only allow us to take speeder's photograph if light is red
__modes(red) void TakeTrafficPhoto(float miles_per_hour);
...
};
void GreenLightTransitionCallback(__mode(green) Stoplight s)
{
Car c;
ForAllCarsInIntersection(c)
{
s.TakeTrafficPhoto(c.Speed()); // caught a bug!
...
}
}
If you want to do these kinds of checks, you must check them at
runtime or you make separate classes (Stoplight_Red, Stoplight_Yellow,
Stoplight_Green) and deal with the added complexity. But if you only
have two modes, then const or non-const have a similar function.
So should red stoplights be const and yellow/green ones be non-const?
Odds are that will not make sense. Yet other examples might be
reasonable: a const IntersectionPath could be one that cars aren't
allowed to travel down, while a non-const one permits traffic. If you
are passing around IntersectionPath-s all over the place and this
distinction is important, it could be worth it to catch bugs by
defining it this way. It might not be worth it.
I merely point out that const is the only tool C++ has that is like
this... and if it *does* make sense, you can get leverage from it.
What if the BCannotTrigger and BCanTrigegr functions must
be a non-const? (...) Const-correctness and function call ordering
are orthogonal design points. They are independent of each other.
I'm suggesting that if you have two modes for an object, and you think
"hmmm, this mode doesn't really need to allow changes to the object's
members" then you might design your objects differently. That's why I
gave the example of a library designed so that read-only TextFiles
would be "const TextFile". (I know of course that the standard
library does not work this way!)
You are accustomed to the idea that file objects have internal state
that needs to be modified by client objects, like the current seek
position after a read. But it didn't *have* to be designed that
way... a TextFilePos object could be passed around separately, and you
could have a non-const TextFilePos operating on a const TextFile.
void DoSomething(const TextFile& tf)
{
TextFilePos tfp = tf.HeadOfFile();
string s = tf.ReadLine(&tfp); // updates tfp to new position
}
I'm certainly not saying one should throw out the C++ standard library
to use my "wacky" idea for files. Just giving it as an example of how
const can mean something more significant, and help do massive compile-
time checking in architectures. Using const as merely "the member
variables don't change" seems like you are exposing an implementation
detail, and isn't as interesting to me as larger semantic notions of
immutability. They're not at odds.
Well, Except: clearly there is a hiccup at object creation/destruction
time. But you can quarantine this peculiarity with a factory pattern
so that only the factory "breaks the rule"... kind of like how you can
occasionally break rules with mutable if you are the implementor and
know what you're doing.
class TextFileFactory
{
...
const TextFile& ReadOnlyTextFile(string filename);
TextFile& ReadWriteTextFile(string filename);
... // maybe have close methods, maybe handle automatically
}
I'm not sure about why one should worry about "orthogonality".
Architectural decisions sometimes come together in a way where you do
something for more than one reason...
Regarding your problem itself, what if BCanTrigger "needs" to be a non-
const member in that it modifies the state of the object A or any
derived of A?
You are right. It should only be used if you can really be
comfortable with the notion that one of your two "object modes" has a
reasonable correspondence to the C++ "constness" we are all familiar
with. It might take some maneuvering to get it, but I actually feel
like TextFilePos makes sense as a separate object... a lot of
situations are similar and can be rethought like that.
I didn't use this method for TextFiles, I used it for my document/view
architecture... and it catches bugs, really it does. const enforces
architectural rules, even when the bugs are very, very "far away" from
the object that defined the contract.
I hope this has made the motivations more clear, and if you know
another way of doing the same thing that would be interesting... but
saying "you can't want that contract" isn't going to work because I do
want it.
Thanks again,