views:

110

answers:

3

We all know that things like this are valid in c++:

const T &x = T();

while:

T &x = T();

is not.

In a recent question the conversation lead to this rule. The OP had posted some code which clearly evokes UB. But I would have expect a modified version of it to work (This is the modified version):

#include <iostream>
using namespace std;

class A {
public:
    A(int k) { _k = k; };
    int get() const { return _k; };
    int _k;
};

class B {
public:
    B(const A& a) : _a(a) {}
    void b() { cout << _a.get(); }
    const A& _a;
};

B* f() {
    return new B(A(10));
}

int main() {
    f()->b();
}

This prints garbage on some machines, 10 on others... sounds like UB to me :-). But then I thought, well A is basically a glorified int all it does it initialize one and read it. Why not just call A an int and see what happens:

#include <iostream>
using namespace std;

typedef int A;

class B {
public:
    B(const A& a) : _a(a) {}
    void b() { cout << _a; }
    const A& _a;
};

B* f() {
    return new B(A(10));
}

int main() {
    f()->b();
}

It prints 10 every time. It at least seems like the const reference rule is in effect for the int version, but not for the class version. Are they both simply UB due to the use of the heap? Am I just lucky with the int version because the compile saw through all consts and just directly printed out a 10? Which aspect of the rule am I missing?

+9  A: 

It simply demonstrates that analyzing language behavior by "trying it in the compiler" doesn't normally produce any useful results. Both of your examples are invalid for the very same reason.

The lifetime of the temporary is only extended when you use that temporary as the direct initializer for a const reference - only that will establish a "lifetime" link between the reference and the temporary.

Trying to pass a temporary as a constructor's argument and attaching a const reference inside the constructor will not establish the aforementioned link and will not extend the lifetime of the temporary.

Also, in accordance with C++ standard, if you do this

struct S {
  const int &r;

  S() : r(5) {
    cout << r; // OK
  }
};

the lifetime of the temporary is only extended to the end of the constructor. Once the constructor is finished, the temporary dies, meaning that this

S s;
cout << s.r; // Invalid

is invalid.

Your experiment with int simply "appears to work", purely by accident.

AndreyT
I suggest you change "a const reference" to "a *local* const reference" to make things even clearer ;)
FredOverflow
AndreyT
@AndreyT, how does this *link* gets established at what level?
Gollum
Oh, I forgot about global variables. I guess "a *non-member* const reference" is what I actually meant :)
FredOverflow
@Gollum: This "link" is purely a compile-time concept. The compiler simply pretends that there's is an imaginary named object there instead of a nameless temporary. If it is local, it dies at the end of the block. If it is global, it does at the end of the program execution. And so on. I.e. there's absolutely no run-time effort used to maintain the link. No ref-counting, no anything.
AndreyT
@Gollum: "how does this link gets established at what level?" -> see my updated answer.
FredOverflow
Yea, my gut told me that it was UB but i wasn't seeing why. The "direct initialization" was the missing piece.
Evan Teran
It likely works because the compiler optimizes the reference out of existence and just copies the int around.
Omnifarious
+2  A: 

You just got lucky. Changing B::b to this:

void b() {
    int i = rand();
    int j = rand();
    cout << _a << endl;
}

prints out random numbers.

Pete Kirkham
Whoops, edited wrong post, rolled back :)
FredOverflow
+1  A: 

It prints 10 every time.

Modify the main function a little and it won't print 10 anymore:

int main()
{
    B* p = f();
    cout << "C++\n";   // prints C++
    p->b();            // prints 4077568
}

how does this link gets established at what level?

See 12.2 [class.temporary] §4 and §5:

Temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created.

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is [...]

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except: [...]

A temporary bound to a reference parameter in a function call persists until the completion of the full-expression containing the call.

So in your case, the temporary is destroyed after the evaluation of the full-expression new B(A(10)).

FredOverflow