When does a member function have a constant determinant, and when should it not be?

About six years ago, a software engineer named Harry Porten wrote in this article asking the question: "When should a const member function exist, and when should it not?" I found that this is the best review I can find in this issue, which I have been dealing with recently, and which, it seems to me, is not well covered in most of the discussions that I found on const correctness. Since a software sharing site as powerful as SO did not exist then, I would like to raise this question here.

+7
c ++ design
source share
6 answers

The article, apparently, considers a lot of basic premises, but the author still raises the question of constant and non-constant overloading of functions that return pointers. The last line of the article:

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

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

As an example, where it is located, vector<int>::operator[] returns a reference to int. Int referand is a "part" of the vector, although it is not actually a member of the data. So the const-overload idiom is applied: change the element and you have changed the vector.

For an example where this is not the case, consider shared_ptr . It has a member function T * operator->() const; , because the constructive pointer const for a non-constant object makes logical sense. Referand is not part of the smart pointer: changing it does not change the smart pointer. Thus, the question of whether it is possible to β€œreset” a smart pointer to a call to another object does not depend on whether the link is and is const.

I don’t think that I can provide any complete recommendations allowing you to decide whether the bridgehead is logically part of the object or not. However, if changing the pointer changes the return values ​​or other behavior of any member functions from this , and especially if the bridgehead is involved in operator== , then the likelihood that it is logically part of the this object.

I would be mistaken in assuming that it is part (and provides overload). Then, if there was a situation where the compiler complains that I am trying to change the object A returned from the const object, I would think if I really should do this or not, and if this changes the design, so only the -to-A pointer conceptually, it is part of the state of the object, not A. itself. This, of course, requires ensuring that the modification of A does nothing that violates the expected behavior of this const object.

If you publish an interface, you may have to figure this out beforehand, but in practice, returning from const overloads to const-function-return-not-const-pointer is unlikely to break the client code. In any case, by the time you publish the interface, which we hope have used it a bit, and probably felt that it really included the state of your object.

Btw, I'm also trying to make a mistake on the side of not providing pointers / reference accessories, especially modifiable ones. This is a really separate issue (the Law of Demeter and all that), but the more times you can replace:

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

from:

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

The less you have to worry about the problem. Of course, the latter has its limitations.

+5
source share

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

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

+4
source share

I personally use a very simple Thumb rule:

If the observed state of the object does not change when this method is called, this method must be const .

All in all, this is similar to the rule mentioned by SCFrench about equality comparison, except that most of my classes cannot be compared.

I would like to emphasize the debate once again:

When an argument is required, the function must accept it with the const descriptor (or copy) if the argument remains unchanged (for an external observer)

This is a little more general, because after all the methods of the class there is nothing more than an independent function that takes an instance of the class as the first argument:

 class Foo { void bar() const; }; 

equivalent to:

 class Foo { friend void bar(const Foo& self); }; // ALA Python 
+1
source share

when it does not change the object.

It just makes this have a type of const myclass* . This guarantees a call function that the object will not change. Allow some optimizations to the compiler and it is easier for the programmer to find out if he can name it without side effects (at least the effect for the object).

0
source share

General rule:

A member function must be const if it compiles when marked as const , and if it compiles if const are transitive pointers wrt

Exceptions:

  • Logical operation const; methods that change the internal state, but this change is not detected using the class interface. For example, Splay tree queries.
  • Ways in which const / non-const implementations differ only in the return type (common with return iterator / const_iterator methods). Invoking a non-const version in a const version using const_cast is valid to avoid repetition.
  • Ways to interact with third-party C ++, which is not correct, or code written in a language that does not support const
0
source share

Here are some good articles:
Herb Sutter GotW # 6
Herb Sutter and const for optimization
Additional tips for correct constants
From Wikipedia

I use const qualifiers when the method does not change the data members of the class or its general intention is not to change the data members. One example includes RAII for the getter method, which may need to initialize data members (for example, retrieve from a database). In this example, the method only modifies the data member once during initialization; all other times constantly.

I allow the compiler to catch const errors at compile time, rather than catch them at runtime (or by the user).

0
source share

All Articles