@sipa yes, that’s exactly my understanding. Mutating a const
-declared variable is UB. Therefore compilers could make optimizations based on the fact that const
-declared variables cannot change.
const
on a member functions simply means that this function call won’t mutate the state of the object, although of course, other code could mutate the state. Therefore, the compiler can’t make any assumptions about the state of the variable not changing.
You can even mutate the state through a const
function by casting away the constness, eg:
0#include <iostream>
1
2class thing
3{
4 int x{0};
5
6public:
7 void mutate(int x_in) const
8 {
9 thing* mutable_this = const_cast<thing*>(this);
10 mutable_this->x = x_in;
11 }
12
13 int access() const { return x; }
14
15 thing(int x_in) : x(x_in) { }
16};
17
18int main()
19{
20 thing t(1);
21 std::cout << "t.x = " << t.access() << "\n";
22
23 t.mutate(2);
24 std::cout << "t.x = " << t.access() << "\n";
25
26 return 0;
27}
You can see that the state of t
has been mutated in the mutate()
function, even though that function is const
. This compiles with no warnings. I believe this is perfectly legal (although ill-advised!) c++. The c++ core guidelines advise against casting away const in almost all cases: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es50-dont-cast-away-const. Scott Meyers does use casting away const to avoid code duplication: https://stackoverflow.com/a/123995/933705 (although that book is now quite old, so I’m not sure if the advice is still current).
Going back to the main question about whether these functions should be const
, I found the isocpp.org wikipedia page on const correctness to be very useful in clarifying my thoughts. A few extracts:
The trailing const
on inspect()
member function should be used to mean the method won’t change the object’s abstract (client-visible) state. That is slightly different from saying the method won’t change the “raw bits” of the object’s struct
a const
member function must not change (or allow a caller to change) the this object’s logical state (AKA abstract state AKA meaningwise state). Think of what an object means, not how it is internally implemented. A Person’s age and name are logically part of the Person, but the Person’s neighbor and employer are not. An inspector method that returns part of the this object’s logical / abstract / meaningwise state must not return a non-const
pointer (or reference) to that part, independent of whether that part is internally implemented as a direct data-member physically embedded within the this object or some other way. (emphasis mine)
- If a method changes any part of the object’s logical state, it logically is a mutator; it should not be const even if (as actually happens!) the method doesn’t change any physical bits of the object’s concrete state.
- Conversely, a method is logically an inspector and should be const if it never changes any part of the object’s logical state, even if (as actually happens!) the method changes physical bits of the object’s concrete state.
If you are outside the class — you are a normal user, every experiment you could perform (every method or sequence of methods you call) would have the same results (same return values, same exceptions or lack of exceptions) irrespective of whether you first called that lookup method. If the lookup function changed any future behavior of any future method (not just making it faster but changed the outcome, changed the return value, changed the exception), then the lookup method changed the object’s logical state — it is a mutuator.
Based on that, I believe these functions should not be marked const
. Calling MaybeSendPing()
and MaybeSendAddr()
multiple times will result in different behaviour that is observable from outside the class, since the Peer()
objects are mutated.