Understanding how C++ can return reference and bind to reference

Viewed 150
#include <iostream>

class A {
    public:
    int x = 0;
};

A& a_returner() {
    A a;
    return a;
}

int main()
{
  A& a = a_returner();
  std::cout << a.x << std::endl;
}

In this code, a_returner() returns a reference to a variable a in the stack. If we called like this:

A a = a_returner();

this wouldn't be a problem since we'd simply be copying the A object returned. However, we're both returning a reference in a_returner and binding to a reference.

The question is: why this works? A lives in the stack of a_returner.

Compiler indeed gives

Warning: reference to local variable `a` returned

but the program sucessfully prints 0. I thought that acessing a which is returned from a_returner would access invalid data.

3 Answers

You have a wrong mental model of what "works" mean.

The code you have in the question does NOT work. It didn't crash when you tried and the result was what you were expecting, but that doesn't imply (in C++) that everything is fine.

What you just discovered is the worst side of "undefined behavior", i.e. when you make a mistake in C++ (for example returning a reference to an object on the stack) anything can happen, including nothing and the code may APPARENTLY work as you meant it to work.

Still the code is NOT working and things will got horribly wrong later in the development cycle.

The main philosophical foundation of C++ is that programmers make no errors, thus instead of "runtime error angles" that catch mistakes you get "undefined behavior daemons" and in case of a mistake the program can do whatever it wants, including temporarily hiding the problem to bite back harder later. So while in other languages for example when you go outside the bounds of an array you get a runtime error, in C++ you get undefined behavior and the reason is the assumption that a C++ programmer will never go outside the bounds of an array and there's no point in wasting time checking that.

To recap: in C++ if the program crash you know the code has a problem... if the program does NOT crash and the result is what you expect still you CANNOT be sure that the code is correct (not even for the very single case that you provided as input).

Many C++ learners and some self-appointed C++ "expert" think that crashes are the enemy and that you should prevent crashes (or even worse adsorb and swallow them to keep running). Actually it's quite the opposite and crashes are your friends, it's program bugs that are the enemies. The problem is that C++ doesn't crash often enough and sometimes buggy code that should have crashed didn't crash (like your example).

In your specific case it's a programmer responsibility to ensure that if there is a reference the referenced object is still alive, i.e. the fact that there is a reference is not a guarantee that the object is also there and if it's not (like in your example) the code is undefined behavior. Oddly enough there is a very special rule in C++ that in some cases the fact that you bind a reference will "extend" the life of an object that should have been thrown away immediately... for example:

int foo() {
    return 42;
}

int bar() {
    const int& x = foo(); // Note... a const reference
    return x;
}

this code is valid and correct, but that is sort of an accident because of the strange rule that says that if you bind a const reference to a temporary the temporary life is extended to the lifetime of the reference. Note also it's a very delicate and precise rule (for example you must understand the legalese for what is a "temporary" for the C++ compiler) and unless you study and understand that rule 100% there is an high probability that you'll think it applies even when doesn't.

Just don't do that.

This is undefined behavior, anything is possible, even it seems working well.

BTW: A a = a_returner(); has undefined behavior too. The returned reference from a_returner() is always dangled, and a is tried to be initialized from the dangled reference.

This in your case (though is undefined behaviour) works because reference uses a mapping of memory and alias. (for more information visit: C++ Pointers and references). In your case the memory which is being used by the object a in the function a_returner() is not being freed up automatically as the OS might not have needed it. However that memory is left dangling, as @6502 mentions. As a result the program seems to work. However, this is what one calls undefined behaviour as @songyuanyao explains. If you take a closer took at the disassembly you will find the required explanation.

Related