understanding return by value.

Started by
8 comments, last by swiftcoder 11 years, 8 months ago
hey good people of gamedev, i've been studying and working in c++ for a good number of years, but their has been a key component of c++ that i've simply shyed away from. Because each attempt i made to research it, and understand it. it left me worried in how to approach situations, so in general i avoided it at all costs, in short, i'm talking about retuning a class object which isn't just a POD type.

for example: http://codepad.org/yvEu6DqW

and just incase you don't like to click away, here's the sample code of the test i made:

#include <iostream>
using namespace std;
class Test{
public:
int m_ID;
Test Make(int ID){
return Test(ID);
}
Test operator + (Test &o){
return Test(m_ID+o.m_ID);
}
Test(int id) : m_ID(id){
cout << "Created Object " << m_ID << endl;
}
~Test(){
cout << "Destroyed Object " << m_ID << endl;
}
};
int main(int argc, char **argv){
Test A = Test(10);
Test B = A.Make(11);
Test C = A+B;
cout << "Object: "<<A.m_ID<<endl;
cout << "Object: "<<B.m_ID<<endl;
cout << "Object: "<<C.m_ID<<endl;
return 0;
}


essentially, from looking at the output, i read it as this:

Test A is created by declaring it, but then when i set it = to Test(10) it destroys the old object, and create's a new one in it's place.

then, i create B, again B being declared causes the constructor to be called, then immediately the destructor is called, after A.Make is even called, so the destroyed object contains the right value, but this extra code is still being called anyway.

I understand that many compiler's might compile out the return values so instead of storing a temp version to copy into the result, it simply copy's the result directly into the return value's location.

still, it's very confusing, and the memory overhead of what's going on is what scares me into only doing this sort of stuff via passing by reference, and using new/delete instead of ever using stack memory to hold the object.
Check out https://www.facebook.com/LiquidGames for some great games made by me on the Playstation Mobile market.
Advertisement
If you want all the object creations you also need to look at the copy constructor. Currently you didn't define one, so the compiler did.

The signature looks like this:

Test::Test( const Test& rhs )


Fruny: Ftagn! Ia! Ia! std::time_put_byname! Mglui naflftagn std::codecvt eY'ha-nthlei!,char,mbstate_t>

Return by value is just copying the data. If you have an object that is 4 bytes like a int is. And you have a function
[source lang="cpp"]
void SomeFunc(int x)
{
x = x + 1;
}
[/source]
[source lang="cpp"]

[/source]

you call the function
[source lang="cpp"]
int myint = 50;

SomeFunc(myint);
[/source]

what do you think the value of myint is now? It's still 50. When you called Somefunc it copied the 4 bytes into the functions parameter, on the stack. Every function data goes onto the stack as stack frames. When the function returns all that data is freed. Even when you write your own classes, the language knows the size of your objects and will just copy them bit for bit. Every time one is copied there is also a constructor call and when these variables are destroyed there is a destructor call as well. The only way to get around this behavior is using pointers. Even though the same thing is happening to the pointer data itself. If you copy a pointer it still has the same address which is the address of some object.

So if you wanted to make changes to myint inside the function you will have to use pointers or references( references use pointers ), to refer back to the variable you're really wanting to modify.

[source lang="cpp"]

void SomeFunc(int* x)
{
*x = *x + 1;
}
[/source]

In this function body the * is the dereference operator, this is how you access the object the pointer is pointing to.

and call it like
[source lang="cpp"]

int myint = 50;
SomeFunc(&myint);
[/source]

In this snippet we create a pointer by getting the address of myint using the & operator. Since myint is an int this is an int pointer. Which points to the original object.

Your issue is not understanding the stack and heap. Understanding how these two things work in cpp and how they interact is fundamental to this language. Read up on this until you have it, but it really is fundamentally simple. So once it clicks, you'll have it for life.

If this post or signature was helpful and/or constructive please give rep.

// C++ Video tutorials

http://www.youtube.com/watch?v=Wo60USYV9Ik

// Easy to learn 2D Game Library c++

SFML2.2 Download http://www.sfml-dev.org/download.php

SFML2.2 Tutorials http://www.sfml-dev.org/tutorials/2.2/

// Excellent 2d physics library Box2D

http://box2d.org/about/

// SFML 2 book

http://www.amazon.com/gp/product/1849696845/ref=as_li_ss_tl?ie=UTF8&camp=1789&creative=390957&creativeASIN=1849696845&linkCode=as2&tag=gamer2creator-20

If you want to avoid the intermediate constructor/destructor you could explicitly use the copy constructor like so:

Test B( A.Make(11) );


To avoid the copy during method return you either have to return a reference or a pointer, but for this you'll need to guarantee that the object will survive beyond the method's scope. You could also use reference out parameters like so:

bool Make(int ID, Test& outParam )
{
outParam = Test(ID);
return true; //Return something for the user to know if creation succeeded.
}
int main(int argc, char **argv)
{
Test A = Test(10);
Test B;
A.Make(11, B);
}
Or, you can just trust your compiler to do the right thing: just return your object and let the compiler use return value optimization.
I think you're overthinking.
If a language was so subtle and inefficient it would not have remaind so dominant for so many decades.

Consider this complete and test example. Build it. Run it.
Observe the sequence in which the constructors and destructors are called.

It's important to understand the lidetime of objects in C++.
You also need to understand the cost of new/new[] and delete/delete[] if you're worried about
unnecessary resource consumption.
[source]#include <iostream>

class Test
{
public:
Test(int id)
: id_(id)
{ std::cerr << " ctor(" << id_ << ")\n"; }

Test(Test const& rhs)
: id_(rhs.id_)
{ std::cerr << " copy-ctor(" << id_ << ")\n"; }

~Test()
{ std::cerr << " dtor(" << id_ << ")\n"; }

Test& operator=(Test const& rhs)
{
std::cerr << " operator=(" << rhs.id_ << ")\n";
id_ = rhs.id_;
return *this;
}

Test& add(Test const& rhs)
{
std::cerr << " (" << id_ << ").add(" << rhs.id_ << ")\n";
id_ += rhs.id_;
return *this;
}

private :
int id_;
};

Test operator+(Test const& lhs, Test const& rhs)
{
return Test(lhs).add(rhs);
}

int
main()
{
std::cerr << "Test A(10);\n";
Test A(10);
std::cerr << "Test B = Test(11)\n";
Test B = Test(11);
std::cerr << "Test C = A + B\n";
Test C = A + B;
std::cerr << "end\n";
}[/source]
Here's the output.

Test A(10);
ctor(10)
Test B = Test(11)
ctor(11)
Test C = A + B
copy-ctor(10)
(10).add(11)
copy-ctor(21)
dtor(21)
end
dtor(21)
dtor(11)
dtor(10)

Notice that there are not extra calls to the basic constructor when using the assignment initialization form of declaration.
You might also notice the extra temporary constructed in the [font=courier new,courier,monospace]operator+()[/font] function.

Stephen M. Webb
Professional Free Software Developer


still, it's very confusing, and the memory overhead of what's going on is what scares me into only doing this sort of stuff via passing by reference, and using new/delete instead of ever using stack memory to hold the object.


Don't be scared. Until you've profiled your code, and determined that it runs too slowly as a direct result of passing by value, always pass by value (where possible.) There are two sides to the argument. Heap allocated memory comes with its own set of difficulties, and they're rather more unpleasant than the problems associated with pass by value.

I've never been more productive since I virtually eliminated heap allocations from my own code: http://purplepwny.co...s_by_value.html

Between Scylla and Charybdis: First Look <-- The game I'm working on

Object-Oriented Programming Sucks <-- The kind of thing I say

thanks for the help guys, i'm understanding alot more.

After adding the copy constructor, i can see where alot of my confusion was coming from, I couldn't figure out how the destructor twice had the correct value, when the object had only been created once. now with the copy constructor, I see what's going on better.

I've been messing around alot more with this, and thanks to you guys, I think i have a much better grasp of what's going on.

@Murdocki:
That's essentially what I have been doing for along time, but it's time I start doing things in a much better fashion.

thanks for the help guys=-)
Check out https://www.facebook.com/LiquidGames for some great games made by me on the Playstation Mobile market.
in the C++ spec, RVO is mentioned to work only with initialization of objects.

A a = MakeMeAnA(); // ok
...
A a;
a = MakeMeAnA(); // not ok.

be careful.

also, C++11 solved the issue thanks to std::move

in the C++ spec, RVO is mentioned to work only with initialization of objects.

A a = MakeMeAnA(); // ok
...
A a;
a = MakeMeAnA(); // not ok.

be careful.

That is not entirely correct, as I understand it. The second example wouldn't ever invoke a copy-constructor anyway. It would invoke the assignment operator (and side-effects in overloaded assignment operators aren't just ignored).

However, if your class does not have an explicitly defined assignment operator, GCC will use RVO to eliminate the copy (i.e. the copy constructor will not be called, even if it has non-trivial side-effects.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

This topic is closed to new replies.

Advertisement