The transitive power of C++'s const keyword

H

hostilefork

Hi there comp.lang.c++,

I am not what you'd call a C++ expert (I have to pretty much
constantly refer to the STL documentation to remember the method
names). But one thing I really did get obsessed with while
programming C++ was how I could aggressively use const to enforce
rules at compile time. Specifically, I was using the "transitive"
nature to get access control at a level more powerful than the public/
private/protected keywords.

I wrote an essay a long time ago that I finally posted on my blog--and
I thought maybe someone here would have comments or critiques:

http://hostilefork.com/2005/02/10/transitive-power-of-const-in-cpp/

I don't know if this is what you'd conisder innovative, or an abuse,
or an "obvious" application. So I just wanted to share the idea.
Please feel free to comment. There are a few other C++ related
articles on the site as well.

Best wishes,
HF
 
A

Abhishek Padmanabh

Hi there comp.lang.c++,

I am not what you'd call a C++ expert (I have to pretty much
constantly refer to the STL documentation to remember the method
names). But one thing I really did get obsessed with while
programming C++ was how I could aggressively use const to enforce
rules at compile time. Specifically, I was using the "transitive"
nature to get access control at a level more powerful than the public/
private/protected keywords.

I wrote an essay a long time ago that I finally posted on my blog--and
I thought maybe someone here would have comments or critiques:

http://hostilefork.com/2005/02/10/transitive-power-of-const-in-cpp/

I don't know if this is what you'd conisder innovative, or an abuse,
or an "obvious" application. So I just wanted to share the idea.
Please feel free to comment. There are a few other C++ related
articles on the site as well.

Best wishes,
HF

I will post the relevant example code (and some context) here for a
quick reference:

____________

Imagine that you are designing an architecture where you have base
classes A and B. Objects derived from B are not supposed to be able to
access the BCannotTrigger() method of A objects...however they must be
able to use the BCanTrigger() method. Objects derived from A need
access to both BCannotTrigger() and BCanTrigger():

class A
{
...
protected:
// can't let classes derived from B invoke this
virtual void BCannotTrigger();

public:
// it's ok for classes derived from B to use this
virtual void BCanTrigger();
...
};

class B
{
...
// implement in your derived class
virtual void Callback(A* input_ptr) = 0;
...
};

This looks good on first inspection, but it offers a rather "shallow"
protection. Imagine a class derived from A:

class SubclassOfA : public A
{
void BCanTrigger()
{
// do some stuff
...

// now call useful routine
BCannotTrigger();
}
}

Unwittingly, the programmer has given B a back door into A. If you
think it should be obvious that they were making a mistake, ...
<snipped>
___________________

Nice observation, but I have a little different view on this. Since,
BCannotTrigger is a function that is protected in base A, it means
that classes derived from A can access the method directly or
indirectly via the non-virtual interface pattern (if there is such an
interface in A) even if the method is BCannotTrigger. For the current
case of BCannotTrigger, since it is protected - it establishes a
contract between A and classes publicly derived from A that they can
use BCannotTrigger in anyway they feel it right. Now, SubclassOfA
accesses this function as an implementation detail of its interface
BCanTrigger - for users of this class (B or classes derived from B
this implementation detail is unknown and hence can change as it wants
to evolve without affecting them). This is private to any external
code that is not part of the hierarchy forming over base A. I don't
consider it to be a back-door into A and hence not even a mistake. B
from the public interface of A or derived of A cannot access that
publicly and in reality they don't even know if BCannotTrigger is even
being used somehow. They are isolated of this detail.

For derived classes of A, the protected interface is like a public
interface. If you really did not want any derived class of A to access
it, it really should have been private member. What you really are
saying is that using another member function to implement another
member function is a flaw in design - which I don't think is
convincingly true. How functions are implemented is an implementation
detail of that member and is bound to change without notice and should
not affect their usage by other code.

That is for the problem which I think does not really exist. Now, in
the context of application of const. I find it really useful to think
of const member functions as members that do not change the observable
state of the object (meaning, it does not change the non-mutable, non-
static data members of the class) or don't give back to the caller
some part of the held state that can be modified (for ex, think of
operator[] overloads for std::vector). If there is a function that
needs to work on them (change them) - it is a candidate for non-const
else it should be const. If one just follows this, everything else
starts falling right in place.

You can see the short-coming of your fix that you applied here:

class A
{
...
public:
virtual void BCannotTrigger();
virtual void BCanTrigger() const;
...
};

class B
{
...
virtual void Callback(const A* input_ptr) = 0;
...
};

What if the callback has to be such that it needs to modify the state
of the object being pointed to by input_ptr? You cannot make the
argument const just on the basis that BCannotTrigger should be
inaccessible to be called from BCanTrigger. BCanTrigger cannot call
BCannotTrigger to begin with, irrespective of whether the call-back
argument object is const or not. A const member function cannot call a
non-const member function. (of course, there's const_cast<> but it has
its own set of restrictions).
 
A

Abhishek Padmanabh

On Dec 14, 7:41 am, (e-mail address removed) wrote:
<..snipped..> Since,...
BCannotTrigger is a function that is protected in base A, it means
that classes derived from A can access the method directly or
indirectly via the non-virtual interface pattern (if there is such an
interface in A) even if the method is BCannotTrigger.

Missing keyword private. Read as:
 
T

Tomás Ó hÉilidhe

I am not what you'd call a C++ expert (I have to pretty much
constantly refer to the STL documentation to remember the method
names).

| A reporter asks if he can have the
| great man's phone number. "Certainly",
| replies Einstein. He picks up the phone
| directory, looks up his number, writes
| it on a slip of paper, and hands it to
| the reporter. Dumbfounded, the reporter
| says, "You're considered to be the
| smartest man in the world and you can't
| remember your own phone number?"
|
| Einstein replies, "Why should I memorize
| something when I know where to find it?"


I've used this argument many times when it comes to doing exams. Why
should I memorise the formula for the charge time of a capacitor when I
can just stick it on a sheet of paper with 50 other forumlae and laminate
it?
 
E

Erik Wikström

I will post the relevant example code (and some context) here for a
quick reference:

____________

Imagine that you are designing an architecture where you have base
classes A and B. Objects derived from B are not supposed to be able to
access the BCannotTrigger() method of A objects...however they must be
able to use the BCanTrigger() method. Objects derived from A need
access to both BCannotTrigger() and BCanTrigger():

class A
{
...
protected:
// can't let classes derived from B invoke this
virtual void BCannotTrigger();

public:
// it's ok for classes derived from B to use this
virtual void BCanTrigger();
...
};

class B
{
...
// implement in your derived class
virtual void Callback(A* input_ptr) = 0;

Don not use pointers unless there is a compelling reason to do so. Use
references instead.
 
M

Matthias Buelow

I've used this argument many times when it comes to doing exams. Why
should I memorise the formula for the charge time of a capacitor when I
can just stick it on a sheet of paper with 50 other forumlae and
laminate it?

And it's true, isn't it?

If you really need to use some formula regularly, you'll memorize it
automatically. Exam questions that ask for memorized stuff like that are
actually a question posed at the examiner and they come already graded,
and the grade is: FAILED.
 
H

hostilefork

For the current
case of BCannotTrigger, since it is protected - it establishes a
contract between A and classes publicly derived from A that they can
use BCannotTrigger in anyway they feel it right.

Hi Abhishek,

If you are saying that the contract between library author and client
code is ideally equal to the set of constraints which the compiler can
check, I think we agree. Having bizarre hidden rules about what order
methods need to be called in (for instance) is no good, you should
design your objects so that it will be safe for methods to be called
in any order. However, if you *do* have a weird constraint, you
should rewrite your objects so that the API forces programmers to
follow the constraint.

(As an aside, I have another article about that here, which you are
also welcome to review: http://hostilefork.com/2005/03/15/psuedo-functional-programming-cpp-tricks/
)

But the thrust of *this* article is to use the compiler's ability to
check const in order to enforce a "deep" contract. When I used the
word "Trigger" as opposed to "Call" I really did mean that I was
trying to prevent B objects appearing above the A objects in the call
stack, as the upstream cause of running the BCannotTrigger method. It
comes up in things I work on.

What you really are
saying is that using another member function to implement another
member function is a flaw in design - which I don't think is
convincingly true.

Doesn't it depend on the functions and what they do? Just as there
are cases where you might want member functions to be called with
particular ordering rules, you should be able to redesign your object
hierarchy to support that constraint.

Consider a TextFile. Very few developers would think to make "const
TextFile" correspond to a file which is read only while "TextFile"
corresponds to read/write. They'd make separate objects and duplicate
a lot of interface and a lot of code... or just have runtime checks.
I'm pointing out that if you do use const for this "object mode bit",
suddenly the methods you use inside your object will become self
checking in terms of whether it is legal to use each other in their
implementation.

BCanTrigger cannot call
BCannotTrigger to begin with, irrespective of whether the call-back
argument object is const or not. A const member function cannot call a
non-const member function.

Quite true. I think you have properly re-interpreted what I am trying
to say as a question about whether an object's methods can be
implemented in terms of each other... but I was focusing specifically
on the fact that doing this with const requires picking a semantic
meaning about "object mode" the object presents to the outside world.
In this case, the mode bit was "BCanTrigger"/"BCantTrigger"...

Yet whatever you choose the mode to mean, you should not violate the
const/non-const meanings that C++ generally intends. Even though you
can, using mutable. And I've been tempted. :)
 
A

Abhishek Padmanabh

Hi Abhishek,

If you are saying that the contract between library author and client
code is ideally equal to the set of constraints which the compiler can
check, I think we agree. Having bizarre hidden rules about what order
methods need to be called in (for instance) is no good, you should
design your objects so that it will be safe for methods to be called
in any order. However, if you *do* have a weird constraint, you
should rewrite your objects so that the API forces programmers to
follow the constraint.

Yes, of course, one should enforce the design constraints but not
using const-correctness. You are talking of a valid problem but the
solution is not appropriate. As I think, the problem you are
addressing is functions that are part of an interface but have a
particular call order. Note that, the problem with 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.

But the thrust of *this* article is to use the compiler's ability to
check const in order to enforce a "deep" contract. When I used the
word "Trigger" as opposed to "Call" I really did mean that I was
trying to prevent B objects appearing above the A objects in the call
stack, as the upstream cause of running the BCannotTrigger method. It
comes up in things I work on.


Doesn't it depend on the functions and what they do? Just as there
are cases where you might want member functions to be called with
particular ordering rules, you should be able to redesign your object
hierarchy to support that constraint.

Yes, so if the problem is ordering rules, how is const correctness
important? What if the BCannotTrigger and BCanTrigegr functions must
be a non-const? That means what if there is a strict call ordering for
non-const function? Making one of those const just doesn't fit here.
Const-correctness and function call ordering are orthogonal design
points. They are independent of each other. If the function call
ordering is important, you should either not expose such interface -
meaning expose one function that internally handles the ordering. Or,
it throws an exception or reports a failure on unordering calling. For
example, consider std::vector<T> and the code below:

std::vector<int> my_vec; //create vector object - empty
my_vec[10] = 10; //modify 10th element.

The second statement above is an error. There is ordering problem in a
way. That is, there should be atleast 11 elements in the vector for
this statement to be valid. So, there should be atleast 11 independent
inserts after which this call should follow. How do you resolve such
an issue? Or do you even think this as an issue? The member function
at() solves that! It throws an exception when such access is made. If
you just made operator[]() or at() a const-member, it would not solve
the problem.

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? How would you solve the problem with the ordering of
function call?
Consider a TextFile. Very few developers would think to make "const
TextFile" correspond to a file which is read only while "TextFile"
corresponds to read/write. They'd make separate objects and duplicate
a lot of interface and a lot of code... or just have runtime checks.
I'm pointing out that if you do use const for this "object mode bit",
suddenly the methods you use inside your object will become self
checking in terms of whether it is legal to use each other in their
implementation.

I am not sure what TextFile you are referring to but if you take the
standard fstream - std::ifstream class to read a file and make it a
const object, it won't work. fstream objects need to be non-const as
they modify their internal state to notify errors etc upon read/write
etc. The read/write access is controlled by the open mode flags passed
to the constructor or open() member.
 
R

Roland Pibinger

But one thing I really did get obsessed with while
programming C++ was how I could aggressively use const to enforce
rules at compile time. Specifically, I was using the "transitive"
nature to get access control at a level more powerful than the public/
private/protected keywords.

I wrote an essay a long time ago that I finally posted on my blog--and
I thought maybe someone here would have comments or critiques:

http://hostilefork.com/2005/02/10/transitive-power-of-const-in-cpp/

I don't know if this is what you'd conisder innovative, or an abuse,
or an "obvious" application. So I just wanted to share the idea.
Please feel free to comment. There are a few other C++ related
articles on the site as well.

The only drawback of 'const' is that C++ uses the wrong default:
Everything should be 'const' unless declared 'mutable'.
You may be interested in Dan Saks' articles about constness:
http://www.ddj.com/article/printableArticle.jhtml?articleID=184403650&dept_url=/cpp/
http://www.dansaks.com/articles.htm
 
H

hostilefork

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,
 
J

James Kanze

[...]
The only drawback of 'const' is that C++ uses the wrong default:
Everything should be 'const' unless declared 'mutable'.

That would have been very hard to do and maintain C
compatibility:). In fact, I don't think the importance of
const was fully realized at the beginning; as late as 1990,
"const correctness" was still being treated as a more or less
revolutionary new idea.

For value oriented type, however, I would agree that most
functions (all but the assignment operators, usually) should be
const, and since C++ is first and foremost value oriented, it
would make sense for this to be the default. For things other
than functions, I'm less sure: you certainly wouldn't want to
have to explicitly declare mutable on a local variable (e.g. a
loop index), for example. And in the end, I'm not sure it makes
that much of a difference; you still really have to ask the
question for each and very function, parameter and return value,
which suggests that there maybe shouldn't be a default at all:
you should have to specify every time. But again, I don't see
this as reasonable outside of function declaration.
 
T

Tomás Ó hÉilidhe

Matthias Buelow said:
If you really need to use some formula regularly, you'll memorize it
automatically.


True that. I've got probably less than 10 formulae memorised, and it's
only because I use them regularly (or at some time used to use them
regularly). Things such as:

* The 3 physics formula (v = u + at, etc.)
* Ohm's law (I remember it as virgin with a
capital V, i.e. V=IR), and then just rearrange
it in my head if I want something in terms of
something else.
* The "minus b formula" for calculating the roots
of a quadratic equation.

They're the main ones I can think of.

And even if you did have a particular formula memorised, you'd probably
be foolish if you were working in industry and didn't double-check it
before applying it. Unless of course you're one of those super-human
people that can memorise the order of 73 decks of cards. . .
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,537
Members
45,022
Latest member
MaybelleMa

Latest Threads

Top