3

According to §7.1.​5.1/4:

Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.

So my question becomes: when is an object a const object?

In particular, is a const member in a non-const object considered a const object?

class Foo {
    const Bar bar;

    void replaceBar(Bar bar2) {
        *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
    }
}

This comes up because I have an immutable class (all fields are const), but I want to have a move constructor, which technically modifies the value passed in. I'm ok with "cheating" in that case, since it doesn't break logical constness.

Martin C. Martin
  • 3,565
  • 3
  • 29
  • 36
  • Yes, it is indeed UB. – T.C. Dec 04 '14 at 14:58
  • 1
    If you are defining a move constructor then presumably one of your members is a pointer. In which case would it be acceptable to have a mutable pointer to a const object instead of a const pointer to a const object? If it isn't a pointer or non-trivially copyable then a move constructor is unlikely to help. – sjdowling Dec 04 '14 at 14:59
  • 1
    @sjdowling Not necessarily. A `std::string` would be nice to move from. – T.C. Dec 04 '14 at 15:00
  • 2
    I disagree with the "technically" categorisation. It *does* modify the value passed in, and it *does* break what you call "logical constness". Why shouldn't compilers assume that in `Foo a; Foo b(move(a));`, `a.bar` is not modified, if you defined it as `const`? If any destructors for `a` get inlined, conditions in those destructors can easily be optimised away if the effect of the constructor is known at compile-time. –  Dec 04 '14 at 15:23

2 Answers2

2

The simple rule is: it is ok to cast away constness if the original object is not const. So if you have a non-cont object and, say, you pass the const reference to it to a function, it is legal to cast away constness in the function. In your example the original object is const, so casting constness away is undefined behaviour.

Wojtek Surowka
  • 20,535
  • 4
  • 44
  • 51
  • To be more precise: casting constness away is allowed, but misusing that to attempt to modify the object is not. –  Dec 04 '14 at 15:02
  • @hvd ... IF the original object was const. (If it wasn't const, casting away constness and modifying is rude, but legal) (to be even more precise!) (and to add even more brackets, like these: () (())() ((()())())). – Yakk - Adam Nevraumont Dec 04 '14 at 15:03
  • @Yakk Quite right. The object that both this answer and I were referring to *is* defined as `const`, though. :) –  Dec 04 '14 at 15:05
  • Scott Meyers has a very good example of when casting constness away is ok, as described in [this SO](http://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func). I wouldn't recommend doing that in any other case though. – Giovanni Botta Dec 04 '14 at 15:07
0

Let us make this a full example:

struct Bar { int x; };

struct Foo {
  const Bar bar;
  Foo( int x ):bar(x) {}

  void replaceBar(Bar bar2) {
    *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
  }
};

now, let us break the world.

int main() {
  Foo f(3);
  Bar b = {2};
  f.replaceBar(b);
  std::cout << f.bar.x << "\n";
}

the above can and probably should output 3, because a const object Bar was created with x=3. The compiler can, and should, assume that the const object will be unchanged throughout its lifetime.

Let's break the world more:

struct Bar {
  int* x;
  Bar(int * p):x(p) {}
  ~Bar(){ if (x) delete x; }
  Bar(Bar&& o):x(o.x){o.x=nullptr;}
  Bar& operator=(Bar&& o){
    if (x) delete x;
    x = o.x;
    o.x = nullptr;
  }
  Bar(Bar const&)=delete;
  Bar& operator=(Bar const&)=delete;
};

struct Foo {
  const Bar bar;
  Foo( int* x ):bar(x) {}

  void replaceBar(Bar bar2) {
    *(const_cast<Bar *>&bar) = bar2;  // Undefined behavior?
  }
};

now the same game can result in the compiler deleting something twice.

int main() {
  int* p1 = new int(3);
  Foo f( p1 );
  Bar b( new int(2) );
  f.replaceBar(std::move(b));
}

and the compiler will delete p1 once within replaceBar, and should delete it also at the end of main. It can do this, because you guaranteed that f.bar.x would remain unchanged (const) until the end of its scope, then you violated that promise in replaceBar.

Now, this is just things the compiler has reason to do: the compiler can literally do anything once you have modified an object that was declared const, as you have invoked undefined behavior. Nasal demons, time travel -- anything is up for grabs.

Compilers use the fact that some behavior is undefined (aka, not allowed) to optimize.

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