Reputation: 1568
I am trying to understand c++'s const
semantics more in depth but I can't fully understand what really the constness guarantee worth is.
As I see it, the constness guarantees that there will be no mutation, but consider the following (contrived) example:
#include <iostream>
#include <optional>
#include <memory>
class A {
public:
int i{0};
void foo() {
i = 42;
};
};
class B {
public:
A *a1;
A a2;
B() {
a1 = &a2;
}
void bar() const {
a1->foo();
}
};
int main() {
B b;
std::cout << b.a2.i << std::endl; // output is 0
b.bar();
std::cout << b.a2.i << std::endl; // output is 42
}
Since bar
is const
, one would assume that it wouldn't mutate the object b
. But after its invocation b
is mutated.
If I write the method foo
like this
void bar() const {
a2.foo();
}
then the compiler catches it as expected.
So it seems that one can fairly easily circumvent the compiler with pointers. I guess my main question is, how or if I can be 100% sure that const
methods won't cause any mutation to the objects they are invoked with? Or do I have completely false expectations about const
?
Why does c++ allow invocation of non-const methods over pointers in const
methods?
EDIT:
thanks to Galik's comment, I now found this:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4372.html
Well, this was exactly what I was looking for! Thanks! And I find Yakk's answer also very helpful, so I'll accept his answer.
Upvotes: 3
Views: 496
Reputation: 206667
So it seems that one can fairly easily circumvent the compiler with pointers.
That is indeed true.
I guess my main question is, how or if I can be 100% sure that const methods won't cause any mutation to the objects they are invoked with?
The language guarantees that only in a local sense.
Your class is, indirectly, the same as the following:
struct Foo
{
int* ptr;
Foo() : ptr(new int(0)) {};
void bar() const { *ptr = 10; }
};
When you use:
Foo f;
f.bar();
the member variable of f
did not change since the pointer still points to the location after the call to f.bar()
as it did before the call. In that sense, f
did not change. But if you extend the "state" of f
to include the value of what f.ptr
points to, then the state of f
did change. The language does not guarantee against such changes.
It's our job, as designers and developers, to document the "const" semantics of the types we create and provide functions that preserve those semantics.
Or do I have completely false expectations about const?
Perhaps.
Upvotes: 1
Reputation: 597081
When a class method is declared as const
, that means the implicit this
pointer inside the method is pointing at a const
object and thus the method cannot alter any of the members of that object (unless they are explicitly declared as mutable
, which is not the case in this example).
The B
constructor is not declared as const
, so this
is of type B*
, ie a pointer to a non-const B
object. So a1
and a2
are non-const, and a1
is declared as a pointer to a non-const A
object, so the compiler allows a1
to be pointed at a2
.
bar()
is declared as const
, so this
is of type const B*
, ie a pointer to a const B
object.
When bar()
calls a1->foo()
, a1
is const
by virtue of this
pointing at a const B
object, so bar()
can't change a1
to point at something else. But the A
object that a1
is pointing at is still deemed to be non-const by virtue of a1
's declaration, and foo()
is not declared as const
, so the compiler allows the call. However, the compiler can't validate that a1
is actually pointing at a2
, a member of B
that is supposed to be const
inside of bar()
, so the code breaks the const
contract and has undefined behavior.
When bar()
tries to call a2.foo()
instead, a2
is const
by virtue of this
pointing at a const B
object, but foo()
is not declared as const
, so the compiler fails the call.
const
is just a safety catch for well-behaving code. It does not stop you from shooting yourself in the foot by using misbehaving code.
Upvotes: 0
Reputation: 62603
This is a correct observation. In const-qualified functions (bar
in your example) all data members of the class are behaving as if they are const data members when accessed from this function. With pointers, it means that the pointer itself is constant, but not the object it points to. As a matter of fact, your example can be very much simplified into:
int k = 56;
int* const i = &k;
*i = 42;
There is a big difference between pointer to constant object and constant pointer, and one needs to understand it, so that 'promises', which were not given in the first place, would not seem to be broken.
Upvotes: -1
Reputation: 120021
Constness by itself doesn't guarantee you anything. It only takes away rights of specific code to mutate an object through a specific reference. It doesn't take away rights of other code to mutate the same object through other references, right under your feet.
Upvotes: 2
Reputation: 275740
const
tells the caller "this shouldn't mutate the object".
const
helps the implementor with some errors where accidentally mutating state generates errors unless the implementor casts it away.
const
data (not references, actual data) provides guarantees to the compiler that anyone who modifies this data is doing undefined behaviour; thus, the compiler is free to assume that the data is never modified.
const
in the std
library makes certain guarantees about thread safety.
All of these are uses of const
.
If an object isn't const
, anyone is free to const_cast
away const
on a reference to the object and modify it.
If an object is const
, compilers will not reliably diagnose you casting away const
, and generating undefined behavior.
If you mark data as mutable
, even if it is also marked as const
it won't be.
The guarantees that the std
provides based off const
are limited by the types you in turn pass into std
following those guarantees.
const
doesn't enforce much on the programmer. It simply tries to help.
No language can make a hostlie programmer friendly; const
in C++ doesn't try. Instead, it tries to make it easier to write const
-correct code than to write const
-incorrect code.
Upvotes: 6