开发者

When should a member function have a const qualifier and when shouldn't it?

开发者 https://www.devze.com 2022-12-23 09:39 出处:网络
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 t

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 开发者_StackOverflow社区to resurrect the question here.


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.


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.


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


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).


General rule:

A member function should be const if it both compiles when marked as const and if it would still compile if const were transitive w.r.t pointers.

Exceptions:

  • Logically const operations; methods that alter internal state but that alteration is not detectable using the class's interface. Splay tree queries for example.
  • Methods where the const/non-const implementations differ only by return type (common with methods the return iterator/const_iterator). Calling the non-const version in the const version via a const_cast is acceptable to avoid repetition.
  • Methods interfacing to 3rd party C++ that isn't const correct, or to code written in a language that doesn't support const


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).

0

精彩评论

暂无评论...
验证码 换一张
取 消