const reference to temporary oddity

2019-02-09 18:47发布

问题:

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?

回答1:

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.



回答2:

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

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

prints out random numbers.



回答3:

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)).