views:

199

answers:

6

About six years ago, a software engineer named Harri Porten wrote this article, asking the question, "When should a member function have a const qualifier and when shouldn't it?" I found it to be the best write-up I could find of the issue, which I've been wrestling with more recently and which I think is not well covered in most discussions I've found on const correctness. Since a software information-sharing site as powerful as SO didn't exist back then, I'd like to resurrect the question here.

A: 

when it doesn't modify the object.

It simply makes this to have type const myclass*. This guarantees a calling function that the object won't change. Allowing some optimizations to the compiler, and easier for the programmer to know if he can call it without side effects (at least effects to the object).

Javier
But what about conceptual constness? What if it doesn't modify the object itself but does modify something one of the object's pointer members points to?
SCFrench
This "when it doesn't modify the object" means that dowsn't modify a fields of class (objects it is fields and methods)
den bardadym
This is not Logical constness then, but Hardware constness.
Matthieu M.
+4  A: 

The article seems to cover a lot of basic ground, but the author still has a question about const and non-const overloads of functions returning pointers. Last line of the article is:

Many will probably answer "It depends." but I'd like to ask "It depends on what?"

To be absolutely precise, it depends whether the state of the A object pointee is logically part of the state of this object.

For an example where it is, vector<int>::operator[] returns a reference to an int. The int referand is "part of" the vector, although it isn't actually a data member. So the const-overload idiom applies: change an element and you've changed the vector.

For an example where it isn't, consider shared_ptr. This has the member function T * operator->() const;, because it makes logical sense to have a const smart pointer to a non-const object. The referand is not part of the smart pointer: modifying it does not change the smart pointer. So the question of whether you can "reseat" a smart pointer to refer to a different object is independent of whether or not the referand is const.

I don't think I can provide any complete guidelines to let you decide whether the pointee is logically part of the object or not. However, if modifying the pointee changes the return values or other behaviour of any member functions of this, and especially if the pointee participates in operator==, then chances are it is logically part of this object.

I would err on the side of assuming it is part (and provide overloads). Then if a situation arose where the compiler complains that I'm trying to modify the A object returned from a const object, I'd consider whether I really should be doing that or not, and if so change the design so that only the pointer-to-A is conceptually part of the object's state, not the A itself. This of course requires ensuring that modifying the A doesn't do anything that breaks the expected behaviour of this const object.

If you're publishing the interface you may have to figure this out in advance, but in practice going back from the const overloads to the const-function-returning-non-const-pointer is unlikely to break client code. Anyway, by the time you publish an interface you hopefully have used it a bit, and probably got a feel for what the state of your object really includes.

Btw, I also try to err on the side of not providing pointer/reference accessors, especially modifiable ones. That's really a separate issue (Law of Demeter and all that), but the more times you can replace:

A *getA();
const A *getA() const;

with:

A getA() const; // or const A &getA() const; to avoid a copy
void setA(const A &a);

The less times you have to worry about the issue. Of course the latter has its own limitations.

Steve Jessop
Accessor vs Setter is a long standing issue. The advantage of the Accessor is that syntax might be more appealing and it avoids copying (which for objects that cannot be copied is quite useful).
Matthieu M.
Sure, that's why its a "btw". In this particular example returning a non-const pointer doesn't save copying. `*(thing.getA()) = whatever;` (first defn) has the same amount of copying as `void setA(const A }` (second defn). To have less copying you do something like `void setA(A *whatever) { myAptr = whatever; }` and manage resources a lot. Obviously some accessors make perfect sense, like vector, when the element object explicitly exists and you might reasonably want a pointer to it. If it's just an abstract property of the owning object, not so much.
Steve Jessop
... for instance it would be sheer screaming insanity to merge the `size()` and `resize()` functions of vector into a single `size()` function that returns a proxy object that can be read or written to, despite the appealing syntax `vec.size() = 12;`. And yet sometimes you see objects with accessors amounting to a glorified data member, presumably written by people who have noticed that Properties are better than public fields in C#, but think this is merely because they provide a handy place to hook in some validation code before setting the "real" field ;-)
Steve Jessop
+4  A: 

One interesting rule of thumb I found while researching this came from here:

A good rule of thumb for LogicalConst is as follows: If an operation preserves LogicalConstness, then if the old state and the new state are compared with the EqualityOperator, the result should be true. In other words, the EqualityOperator should reflect the logical state of the object.

SCFrench
A: 
Joe Gauterin
if you have internal state that alters even though logically const it should be mutable
jk
A: 

I personally use a very simple Rule Of Thumb:

If the observable state of an object does not change when calling a given method, this method ought to be const.

In general it is similar to the rule mentioned by SCFrench about Equality Comparison, except that most of my classes cannot be compared.

I would like to push the debate one step further though:

When requiring an argument, a function ought to take it by const handle (or copy) if the argument is left unchanged (for an external observer)

It is slightly more general, since after all the method of a class is nothing else than a free-standing function accepting an instance of the class as a first argument:

class Foo { void bar() const; };

is equivalent to:

class Foo { friend void bar(const Foo& self); }; // ALA Python
Matthieu M.
A: 

Here are some good articles:
Herb Sutter's GotW #6
Herb Sutter & const for optimizations
More advice on const correctness
From Wikipedia

I use const method qualifiers when the method does not alter the class' data members or its common intent is not to modify the data members. One example involves RAII for a getter method that may have to initialize a data members (such as retrieve from a database). In this example, the method only modifies the data member(s) once during initialization; all other times it is constant.

I'm allowing the compiler to catch const errors during compile time rather than me catching them during run-time (or a User).

Thomas Matthews