Returning local automatic objects from functions

Pages: 12
I'm reading through a beginner C++ course and I got to a place dealing with returning objects from functions.

This part is about overloading operators. They give this example, which overloads the + operator for objects of a class (class V).

1
2
3
4
5
6
7
8
9
10
11
class V {
    public:
        float vec[2];
        V(float a0, float a1) { vec[0]=a0; vec[1]=a1; }
        V operator+(V &arg) {
            V res(0.0f,0.0f);
            for(int i = 0; i < 2; i++)
                res.vec[i] = vec[i] + arg.vec[i];
            return res;
        }	
};


What I'm not clear about is this:

The object "res" is basically an automatic variable. Its life span should end when the body of the "operator+" function ends.
So how can this "res" object be returned as result from this function? Is it not supposed to be destroyed automatically when the "operator+" function terminates execution?
What exactly happens when you return a locally created object from a function when the object is an automatic variable? I mean the object was not created with "new", so it's supposed to be living on the stack.
When exactly is this object destroyed?
When you return something from a function, the value is copied to the object that receives the return value of the call, and then the object in the function is destructed. If the return value of the function is not assigned to anything, no copy takes place.
Try this code for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

class A{
public:
    A(){
        std::cout << "A::A()\n";
    }
    A(const A &){
        std::cout << "A::A(const A &)\n";
    }
    ~A(){
        std::cout << "A::~A()\n";
    }
};

A f(){
    std::cout << "f()\n";
    A a;
    std::cout << "f()\n";
    return a;
}

int main(){
    std::cout << "main()\n";
    A a = f();
    std::cout << "main()\n";
}
Hi Cristian,

There's lots of ways that return types get handled (the compiler plays a huge part in here as well):

For smaller-size return types (1 - 2 bytes, and usually for built-in types only), the return value can be saved into a registry (which are like quick-access temporary memory locations). The function can then perform its cleanup tasks and when execution returns to the caller, the return value is then accessed from the registry and then used in the calculation/expression. Keep in mind that this is an optimization that the compiler makes and may/may not happen!

The general explanation that is broadly correct is that space is allocated on the stack for the return value. Before the function destroys its local variable space, it copies the return value into that specified block on the stack. When execution returns to the caller, the return value is popped off that stack and used for whatever it was needed for. This can apply to objects, built-in types variables, etc...

HOWEVER
With objects, an optimization can happen. If the caller has code like this:
Object caller = getObject();
The calling function will actually call the Copy Constructor to copy the returned object directly into the local object caller. Then it will destroy its own local copy of the Object.

I wrote a little program for you to experiment with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <cstdlib>
#include <string>

using namespace std;

class Test
{
public:
	Test(string tag) { 
		name = tag;
		cout << "Default Constructing " <<endl; 
	}
	Test(const Test &source) { 
		cout << "Copying " <<name <<endl; 
		name = "copied";
	}
	~Test() { 
		cout << "Destroying " <<name <<endl; 
	}

private:
	string name;	
};

Test returnTest()
{
	Test myTestObject("returnTestObject");
	return myTestObject;
}

int main(int argc, char *argv[])
{
	returnTest();

	Test mainObject = returnTest();

	cin.ignore();
	return EXIT_SUCCESS;
}


After you run the program, then read this part :)

The first call to returnTest() causes a copy on the stack of myTestObject to get made. The original myTestObject is then destroyed. When control returns to main, it doesn't use the copied object, so you can see that 'copied' gets destroyed as well.

The second call to returnTest() actually stores the return value in another Test object. In this part, the output will show that only ONE copy was made and that was the copy constructor being called to copy myTestObject DIRECTLY into mainObject. Then, myTestObject gets destroyed.

Note the difference in this case: The latter one did not waste a step in copying myTestObject onto the stack, and then having main copy it into mainObject. It just copied myTestObject directly into mainObject.

Once main ends, then mainObject gets destroyed.

What is proven here is that how a return value gets processed can vary depending on the compiler, the types, etc... So be sure that you don't try to dig "too deep" and get lost in the miniscule details :)

If you need any help or more materials, feel free to email me! :)

Joe
sparkprogrammer@gmail.com
www.concordspark.com
Last edited on
@LittleCaptain
The copy constructor must accept its argument by const reference, or line 36 won't compile. This is because returnTest() is an rvalue which cannot bind to a non-const lvalue reference.

Also important to mention is the [named-]return-value-optimization, which eliminates copies by operating directly upon the result object. It applies here.
See this slightly-modified version of your code (const added, unused parameters removed), and note the output - no copy constructors are called:
http://coliru.stacked-crooked.com/a/6d27f9ecd4dacb33

Since C++17, all conforming compilers must implement this optimization. In fact, elimination of calls to copy- and move- constructors is one major exception to the 'as-if' rule, where observable side-effects resulting from them can't be relied upon, because calls to them can be eliminated. See:
http://en.cppreference.com/w/cpp/language/copy_elision
Last edited on
So is the as-if rule pretty much false at this point?
No, it's still around. The as-if rule basically says the compiler may change your (well-formed) code however it wants as long as the change doesn't affect the observable behavior of the program. "Observable behavior" is very roughly what your program appears to do from the perspective of the user.

There are only two exceptions to the rule that I'm aware of. The first is copy and move elision (of which return value optimization is a particular case), and the second is that redundant new and delete pairs can be (sometimes) removed or merged.

I didn't know about the second one until @Cubbi told me about it:
( http://www.cplusplus.com/forum/general/212957/#msg994739 )

http://en.cppreference.com/w/cpp/language/as_if
http://eel.is/c++draft/intro.execution#1
Last edited on
Hmm... I don't think whether object is allocated on the stack or the heap is really an observable behavior. Is it even possible to write a program that portably checks for this?
I don't think whether object is allocated on the stack or the heap is really an observable behavior.
You're right, it isn't. But the problem is operator new(), not stack vs. heap.

In well-written code, it's a bad idea to rely on side effects in copy- and move-constructors, because it's not generally possible to determine when the compiler will include calls to them. Similarly, it's not a good idea to rely on side effects in any particular allocation function, because any corresponding new-expression may or may not actually result in a call to it.

The cppreference page has a good example of where a call to an allocation function might be eliminated:
http://en.cppreference.com/w/cpp/memory/new/operator_new#Class-specific_overloads
Last edited on
Thanks mbozzi for noticing that. I don't know why when I copied it over the const didn't get copied over!
Howly mother of programming !!!
I did not expect this big of a debate !!
And I'm even more confused than I was to begin with.
This is going to be one hypergigantic thread !!!

Let me address the answers in the order of their posting:

So, this is for helios, who replied first:

I ran your code with and without the "-std=c++0x" option (I'm in Linux and I use g++-4.9.1), and I got this same output in console:

1
2
3
4
5
6
main()
f()
A::A()
f()
main()
A::~A()


This means the copy constructor is never invoked !!!!
During execution of f(), local object "a" is created but is not destroyed when f() ends !!!
Instead, only object "a" created in main() is destroyed when main() ends !!
Or is it maybe the "a" created in f() ???

helios, do you get a different output when you compile and run this?
What compiler are you using?
Huh. Weird. I can't catch any copy- or move-construction.
You can tell GCC to not perform copy elision by passing -fno-elide-constructors on the command line. This means that all constructors will be called, and it might make things simpler to reason about. (You'll have evidence of the result object being copy-constructed, for instance.)

After doing that, @helios's code outputs
main()
f()
A::A()
f()
A::A(const A &)
A::~A()
A::A(const A &)
A::~A()
main()
A::~A()

Which lines up with his (correct) explanation

See: http://coliru.stacked-crooked.com/a/9db3c6ea46e17dbd
Last edited on
Alright, now we're getting somewhere !!

@mbozzi
I tried myself the parameter to g++ you showed above, and I got this result in console:

1
2
3
4
5
6
7
8
9
10
11
12
13
knoppix:~$ g++ -fno-elide-constructors -o test test.cpp 
knoppix:~$ ./test
main()
f()
A::A()
f()
A::A(const A &)
A::~A()
A::A(const A &)
A::~A()
main()
A::~A()
knoppix:~$


So now, @helios' code actually makes sense and it teaches me what I needed.
Many thanks @helios, many thanks @mbozzi !!
Confusion is now going away :)

I'm going to try the code from @Little Captain now, and when I'm done experimenting, I will reply again with my own results.
OK, after a decade I finally have time to deal with this again.
(I'm going through some hell lately)

I took the code from Little Captain, saved it as test.cpp and tested it.
When I compile it, I get this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
user:~$ g++ -o test test.cpp 
test.cpp: In function ‘int main(int, char**)’:
test.cpp:36:31: error: no matching function for call to ‘Test::Test(Test)’
  Test mainObject = returnTest();
                               ^
test.cpp:36:31: note: candidates are:
test.cpp:14:2: note: Test::Test(Test&)
  Test(Test &source) { 
  ^
test.cpp:14:2: note:   no known conversion for argument 1 from ‘Test’ to ‘Test&’
test.cpp:10:2: note: Test::Test(std::string)
  Test(string tag) { 
  ^
test.cpp:10:2: note:   no known conversion for argument 1 from ‘Test’ to ‘std::string {aka std::basic_string<char>}’
user:~$


which confirms mbozzi's prediction.

And yes, mbozzi's solution (Test(const Test &source) instead of Test(Test &source)) fixes the problem but I don't understand how and why.

The problem is, I don't understand what mbozzi means when he says:
This is because returnTest() is an rvalue which cannot bind to a non-const lvalue reference.

Which rvalue and which lvalue are we talking about?
I thought all l-values are supposed to be non-const.
If it's const, it cannot be an l-value.
And ANY r-value (whether const or non-const) CAN be assigned to ANY l-value.

And as far as I've read in this goddamn course I'm going through, the "const" used in front of a formal parameter only makes sure the function will not be able to modify the argument received.
That's all it supposed to do. What's that got to do with lvalues and rvalues??

As far as I CAN understand from this error message, the problem is the copy constructor expects a type "Test&" argument but instead it gets a type "Test" argument.
I'm not even sure what the damn difference is.

If I did not have anybody to advise me, I would fix this error by modifying the return type of "returnTest()".
So I changed the "returnTest()" function signature from
Test returnTest()
to
Test& returnTest()
to give the copy constructor what the hell it wants.

And when I tried to compile the error was gone, but I got instead a warning:
1
2
3
4
5
6
user:~$ g++ -fno-elide-constructors -o test test.cpp 
test.cpp: In function ‘Test& returnTest()’:
test.cpp:28:7: warning: reference to local variable ‘myTestObject’ returned [-Wreturn-local-addr]
  Test myTestObject("returnTestObject");
       ^
user:~$


And running the compiled executable gave this output in the console:

1
2
3
4
5
6
7
8
9
user:~$ ./test 
Default Constructing 
Destroying returnTestObject
Default Constructing 
Destroying returnTestObject
Copying 

Destroying copied
user:~$


And by the way, this output is the same regardles of whether I used "-fno-elide-constructors" or not ...

So now the copy constructor is only invoked when initializing "mainObject", but I'm not even sure what the hell it is initialized with, since the result of "returnTest()" is destroyed before any copy can be made to be assigned to "mainObject".

So now, to me the mess is even thicker than it was when I started this thread...
Cursed be the ***king day I was born!!
And yes, mbozzi's solution (Test(const Test &source) instead of Test(Test &source)) fixes the problem but I don't understand how and why.

The problem is, I don't understand what mbozzi means when he says:
This is because returnTest() is an rvalue which cannot bind to a non-const lvalue reference.

Which rvalue and which lvalue are we talking about?
Temporaries, such as function return values, are rvalues. An rvalue can be assigned to a const T &, but not to a T &:
1
2
3
4
5
6
7
8
T f();
void g(const T &);
void h(T &);
void i(T &&);

g(f()); //OK. f() is an rvalue and it is being assigned to a constant reference.
h(f()); //Error. Can't assign an rvalue to a constant reference
i(f()); //OK. rvalues can be assigned to rvalue references. 


I thought all l-values are supposed to be non-const.
If it's const, it cannot be an l-value.
And ANY r-value (whether const or non-const) CAN be assigned to ANY l-value.
Everything you say is correct. However, while references are lvalues, initializing a reference is an operation that's different from assigning to it. A reference can only be initialized to refer to an lvalue. If it was possible to initialize a reference to an rvalue, then this would be valid code:
1
2
3
4
5
void f(int &n){
    n = 2;
}

f(9); //Effect: ??? 


If I did not have anybody to advise me, I would fix this error by modifying the return type of "returnTest()".
So I changed the "returnTest()" function signature from
Test returnTest()
to
Test& returnTest()
to give the copy constructor what the hell it wants.
If you did that, you would be returning a reference to a local. Since locals go out of scope when the function returns, the reference becomes invalid.
1
2
3
4
5
6
7
8
9
10
11
12
int *f(){
    int n = 42;
    return &n;
}

int &g(){
    int n = 42;
    return n;
}

std::cout << *f(); //Output: garbage.
std::cout << g(); //Output: also garbage. 
> I thought all l-values are supposed to be non-const.
> If it's const, it cannot be an l-value.

With:
1
2
3
4
5
6
7
int i = 10 ;

int* pm = &i ;
int& rm = i ;

const int* pc = &i ;
const int& rc = i ;


We get:
expression        type          value-category
----------        --------      --------------

    *pm           int              lvalue
    rm            int              lvalue

    *pc           const int        lvalue
    rc            const int        lvalue


@JLBorges
Hm! How are *pc and rc supposed to be l-values?
I took your code and made a full test prog like below:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
	int i = 10 ;

	int* pm = &i ;
	int& rm = i ;

	const int* pc = &i ;
	const int& rc = i ;

	rc = 2;
	*pc = 2;
}


Compilation gives this result:

1
2
3
4
5
6
7
8
9
user:~$ g++ -o test test1.cpp 
test1.cpp: In function ‘int main()’:
test1.cpp:10:5: error: assignment of read-only reference ‘rc’
  rc = 2;
     ^
test1.cpp:11:6: error: assignment of read-only location ‘* pc’
  *pc = 2;
      ^
user:~$


Obviously *pc and rc cannot appear on the left side of the assignment operator.
They are REFERENCES, but they are NOT L-VALUES.
Am I missing something?
Last edited on
@helios

1. "A reference can only be initialized to refer to an lvalue."

This one I understand, the course I'm reading clearly says that when a formal parameter to a function is passed by reference, the actual argument to the function must be a variable (something having an address that can stay left of =), which makes perfect sense to me.

2. It's only now that I realize, that when a function has a parameter passed by reference, inside the function the formal parameter (which is a reference) is being INITIALIZED with the argument passed. And the argument must be a lvalue (something having an address that can stay left of =).

What I still can't understand is why the formal parameter must be const.
If this is so, then it means all copy constructors for a class A should be of the form:
A (const A&)
because the form
A (A&)
would be incorrect

All I know about "const" is that it's like a promise that the compiler will prevent the function from modifying (the value of) its argument passed from the outside.
Is there SOMETHING ELSE about this "const" that I don't know?

I wrote this little test program:

1
2
3
4
5
6
7
8
#include <iostream>
int a = 3;
int f() { int n = 2; return n; }
void g(int& n) { std::cout << n << std::endl; }
int main() {
//	g(f());  // uncommenting this gives compiler error
	g(a);    // but this one is fine
}


When uncommenting line 6, the compiler says this:

1
2
3
4
5
6
7
8
9
user:~$ g++ -o test test.cpp 
test.cpp: In function ‘int main()’:
test.cpp:6:6: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
  g(f());  // uncommenting this gives compiler error
      ^
test.cpp:4:6: note: in passing argument 1 of ‘void g(int&)’
 void g(int& n) { std::cout << n << std::endl; }
      ^
user:~$


In both cases (lines 6 and 7) a non-const reference of type ‘int&’ is being initialized from an rvalue of type ‘int’.

The only difference is that on line 6 the initializer is the result of a function, while on line 7 the initializer is a variable.

What the hell is the deal here?
> Am I missing something?

Yes.
lvalue does not mean 'an expression that can appear on the left side of the built-in assignment operator'. For instance, the value category of an expression that designates a function is lvalue

The (somewhat intimidating, if one is seeing it for the first time) details: http://en.cppreference.com/w/cpp/language/value_category
If this is so, then it means all copy constructors for a class A should be of the form:
A (const A&)
because the form
A (A&)
would be incorrect
Yep, that's the canonical signature for the copy constructor. Likewise, the canonical signature for the overloaded assignment operator is
 
const T &operator=(const T &);


In both cases (lines 6 and 7) a non-const reference of type ‘int&’ is being initialized from an rvalue of type ‘int’.
No. a is an lvalue. It has an address. The return value of f() is not an lvalue. That's the difference.
Pages: 12