3

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.

bmk
  • 1,548
  • 9
  • 17
  • 1
    The `const` following `bar` makes the guarentee that you will not mutate any of `B`'s members, which `bar` does not. To do that, it would have to modify the *value* of `a2` or the *address* of the pointer `a1`. All it did was modify the value of the `A` that `a1` was pointing at, which is legal. – Cory Kramer Mar 14 '18 at 19:43
  • @CoryKramer: except that `a1` is pointing at `a2`, and `a1` is declared as a pointer to a non-const `A`, so the compiler allows the call to `a1->foo()` since the pointed object is not `const` and `foo()` is not declared as `const`, but it can't validate that `a1` is actually pointing at a member `B` that should be const. The code has **undefined behavior**. When `bar()` calls `a2.foo()` instead, `a2` is being accessed via the `this` pointer, which is a `const B*` since `bar()` is `const`, so `a2` is `const` and the compiler fails the call since `foo()` is not declared as `const`. – Remy Lebeau Mar 14 '18 at 19:45
  • Possible duplicate of [What is the difference between const int\*, const int \* const, and int const \*?](https://stackoverflow.com/questions/1143262/what-is-the-difference-between-const-int-const-int-const-and-int-const) – llllllllll Mar 14 '18 at 19:45
  • @RemyLebeau can you cite the standard part which would claim this behavior undefined? I am pretty sure there are none. – SergeyA Mar 14 '18 at 19:47
  • @CoryKramer But `bar` did mutate the value of `a2`, because `a1` points to `a2`. – bmk Mar 14 '18 at 19:47
  • Why would people downvote the question? It rather interesting in my view (though not without a bit of provocation) – SergeyA Mar 14 '18 at 19:50
  • Don't understand the down votes??? Whats wrong with my question? – bmk Mar 14 '18 at 19:51
  • @bmk do not know. I am afraid, people just do not understand the question (and the fact that it was asked in a certain manner does not help) – SergeyA Mar 14 '18 at 19:51
  • This is a duplicate. Not of the currently proposed duplicate but I am having trouble finding the right one. It was asked just the other day. (this is closeish) https://stackoverflow.com/questions/28732648/c-preventing-const-methods-from-changing-data-through-a-member-pointer-or-refe – Galik Mar 14 '18 at 19:52
  • @SergeyA provocation, certain manner... Can you be a bit more specific? – bmk Mar 14 '18 at 19:56
  • @bmk, in the same question, you managed to admit your limited knowledge of C++ and fail the language for failing to fulfill the promise. Since most of the time here on SO such messages are complete rubbish, knee-jerk reaction is the downvoting. I am pretty sure, were you to ask the question differently, you would end up upvoted. – SergeyA Mar 14 '18 at 19:58
  • Using [const correctness](https://isocpp.org/wiki/faq/const-correctness) allows the compiler to catch potential defects before run-time, such as mistakenly writing to an object or calling a function that writes to a constant object. – Thomas Matthews Mar 14 '18 at 20:00
  • @SergeyA I can only say, wow!!! I didn't blame or attack the C++ language itself in any way. In fact, I was hoping that I would get some helpful explanations about `const` (what it actually promises and how I can use it more effectively) from more experienced C++ programmers... Instead you are bombarded with downvotes **without any sensible explanations or feedback** – bmk Mar 14 '18 at 20:06
  • 3
    what can I say? I disagree with downvotes. (I even upvoted). If it is any consolation, your score is still above 0 with 2 downvotes and 2 upvotes. Also, having an answer from @Yakk is a reward on it's own :) – SergeyA Mar 14 '18 at 20:07

5 Answers5

6

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
2

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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
1

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.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
0

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
-1

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.

SergeyA
  • 61,605
  • 5
  • 78
  • 137