9

Following the discussion on my answer to this question, apparently:

the following code is allowed

struct Foo {
    int x;
};

Foo f;
Foo & f_ref = f;

(&f) -> ~Foo ();
new (&f) Foo ();

int x = f_ref .x;

but the following code is not allowed

struct Foo {
    const int & x;           // difference is const reference
    Foo (int & i) : x(i) {}
};

int i;
Foo f (i);
Foo & f_ref = f;

(&f) -> ~Foo ();
new (&f) Foo (i);

int x = f_ref .x;

Because of $3.8/7

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type ...

I can understand how a reference to f.x could be invalidated when f ceases to exist, but I don't see why f_ref should be invalidated purely because one of its members is const and/or reference and not otherwise: it was a reference to a Foo before and is a reference to a Foo afterwards.

Can someone please explain the rationale behind this condition?

Edit

Thanks for the answers. I don't buy the "guarantee it doesn't change" argument because we don't currently allow optimisers to cache referands, for example:

struct Foo {
    const int & x;
    Foo (const int & i) : x(i) {}
    void do_it ();
};

int i;
Foo f (i);
const int & ii = f.x;

f .do_it (); // may modify i
std :: cout << ii; // May NOT use cached i

I don't see how do_it is allowed to invalidate referenced values but operator new isn't -- Sequence points invalidate cached values: why should delete/placement-new be exempt?

Community
  • 1
  • 1
spraff
  • 32,570
  • 22
  • 121
  • 229
  • Never mind, OP edited the code. – Kerrek SB Sep 28 '11 at 17:09
  • I don't understand, how `do_it` could modify `i`? – UncleBens Sep 28 '11 at 18:37
  • 1
    @uncleBens: for example `const_cast(x) = 1;` in `do_it` is perfectly OK, given that the referand of `x` in fact is a mutable `int` object. If that's what `do_it` does, then it would be a mistake (UB) to do `const int i; Foo f(i); f.do_it();`, but with non-const `i` it's fine. – Steve Jessop Sep 28 '11 at 18:49
  • The structure of the sentence in the quote is the reason why I hate reading the Standard. Syntactic brainfuck. – Sceptical Jule Sep 15 '22 at 09:36

4 Answers4

8

I believe the motivation is to permit the compiler to cache the values of const objects (note that's const objects, not merely referands of pointers-to-const and reference-to-const), and the addresses of referands of references, across calls to unknown code.

In your second example, the compiler can "see" firstly that the object has been created and destroyed, and secondly that it was re-created using the same value. But the authors of the standard wanted compilers to be allowed to turn this code:

struct Foo {
    const int & x;
    Foo (int & i) : x(i) {}
};

int i = 1;
Foo f(i);

some_function_in_another_TU(&f);

std::cout << f.x;

Into this:

struct Foo {
    const int & x;
    Foo (int & i) : x(i) {}
};

int i = 1;
Foo f(i);

some_function_in_another_TU(&f);

std::cout << i;           // this line is optimized

because the reference member of f cannot be reseated, and hence must still refer to i. The destruct-and-construct operation violates the non-reaseatable-ness of the reference member x.

This optimization should not be particularly controversial: consider the following example, using a const object rather than an object with a const or reference member:

const int i = 1;
some_function_in_another_TU(&i);
std::cout << i;

Here i is a compile-time constant, some_function_in_another_TU cannot validly destroy it and create another int in its place with a different value. So the compiler should be allowed to emit code for std::cout << 1; The idea is that the same should be true by analogy for const objects of other types, and for references.

If a call to unknown code could reseat a reference member, or alter the value of a const data member, then a useful invariant of the language (references are never reseated and const objects never change their values) would be broken.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Can you please comment on the edit at the bottom of the question? Thanks. – spraff Sep 28 '11 at 17:34
  • @spraff: you aren't distinguishing between caching the value of a referand, and caching the location of the referand. My example with `std::cout << i` does the latter. Your example with "May NOT use cached i" seems to me to be saying that the value of `i` cannot be cached, which is true. However, the location of the referand of `ii` is known and can be cached, only the value can change. Your example doesn't really relate to mine that I can see. – Steve Jessop Sep 28 '11 at 18:38
  • "My example with std::cout << i does the latter." -- oops, sorry, just realised I have two examples with `std::cout << i;`. In that comment, I was referring to my example where `std::cout << f.x;` can validly be transformed by the compiler to `std::cout << i;`. – Steve Jessop Sep 28 '11 at 18:52
5

As far as I can tell, it's just a matter of semantic correctness, and the adherent assumptions that the optimizer may make. Consider this:

Bar important, relevant;

Foo x(important);  // binds as const-reference

Zoo z(x);  // also binds as const reference

do_stuff(z);

x.~Foo();
::new (&x) Foo(relevant);  // Ouch?

The object z may reasonably expect its Foo member reference to be constant and thus refer to important. As the standard says, the destruction plus new construction in the last two lines "automatically updates all references to refer to the (logically) new object", so now the const-reference inside z has changed, despite the promise of being constant.

To avoid this backstabbing violation of const-correctness, the entire reconstruction-in-place is forbidden.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 2
    +1 for the term `backstabbing violation`. It's perfect example of backstabbing violation. – Nawaz Sep 28 '11 at 17:12
2

Optimization. Suppose I have:

struct Foo
{
    int const x;
    Foo( int init ) : x( init ) {}
};

int
main()
{
    Foo a( 42 );
    std::cout << a.x << std::endl;
    new (&a) Foo( 3 );
    std::cout << a.x << std::endl;
    return 0;
}

The compiler, having seen a const int object, has the right to suppose that the value doesn't change; the optimizer might simply keep the value in a register accross the placement new, and output it again.

Note that your example is actually quite different. The data member has type int const&; it is a reference (and references are always const), so the compiler can assume that the reference always refers to the same object. (The value of this object may change, unless the object itself is also const.) The compiler can make no such assumption about the value of the object it refers to, however, since i (in your case) can clearly change. It is the fact that the reference itself (like all references) is immutable that causes the undefined behavior here, not the const that you've written.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • "what it refers to may not change" - I agree with what you're saying, but my experience is that such statements are prone to misinterpretation. The value of the object it refers to can change, but which object it refers to cannot change. Some people would read, "what it refers to cannot change", and think that you're (falsely) claiming that (the value of) the object to which it refers cannot change, whereas in fact you are (correctly) saying that it still refers to the same object before and after. – Steve Jessop Sep 28 '11 at 18:45
  • @Steve Jessop My wording does seem rather unfortunate, as it can easily be understood in two different ways. I'll correct it so that it will (hopefully) be less ambiguous. – James Kanze Sep 29 '11 at 07:30
-2

If something is const-qualified, you're not supposed to modify it. Extending its lifetime is a modification with serious consequences. (For example, consider if the destructor has side-effects.)

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • If `x` is a const reference to non-const `y`, `x` can still change because `y` can change -- the restriction is that you can't *use* `x` as a means to modify `y`. – spraff Sep 28 '11 at 17:36
  • Yes, exactly. Did you think that somehow disagreed with what I said? (If *your* only connection to something is a const reference to it, *you* shouldn't be modifying it. In this case, the const reference is the only connection.) – David Schwartz Sep 28 '11 at 18:01
  • Agreed. That doesn't preclude from something *else* changing it, which is really what this whole business is about. – spraff Sep 28 '11 at 18:09
  • Not really, because the "something else" is only connected to this object through a const reference. With only that, it should not be permitted to change its lifetime. That is, you have X which has a const-reference to Y. Thus you should not be permitted to change Y's lifetime. Thus you may not extend X's lifetime because that changes Y's lifetime. – David Schwartz Sep 28 '11 at 18:20