Jump to content
  • Advertisement
Sign in to follow this  
deadimp

C++ Functions 'return' - Specifics

This topic is 4314 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I have mulled this topic around in my head some, and I have yet to figure it out: What are the specifics of returning an object by a function (not a pointer/reference)? The reason why I question/ask this is that I have written a string class (for practice, not meant to be better than STL's), and I have been confused as to how to create an efficient concatenation method. I have this code:
class string {
 ...
 string& operator+=(const string &x) { ... } //With overloads for 'char' and 'char*'
};

string operator+(const string &lhs,const string &rhs) {
 string temp(lhs);
 return temp+=rhs;
}


However, I question it's efficiency. I feel as though I'm copying strings more than needed. The contextual question is: Is the function returning 'temp', or is it returning a string copying temp? [Following questions assume latter is true] How could I get rid of this excess copying? Is there a way to access the return variable directly? (Iffy, I know) Would inline-ing 'operator+' do anything, or would it still copy again? Is there a way to make it more efficient and keep it in the source file? NOTE: I know that this does not incur enough processing to relatively slow anything down, but I would be more at ease if there was something more I could do. Using MinGW 3.7 (GNU GCC 3.4). [I posted this in the CPP Workshop, but seeing as how that might not have been the best place, I moved it here]

Share this post


Link to post
Share on other sites
Advertisement
Looks alright, thats generally the way the addition operator is done.
By adding two exsisting strings together your asking for a third containing
both, the new one. Inlining the call may or may not speed it up, but you need
to return the new string.

Edit:
Actually, looking at it again, try

{
string both(lhs)
both += rhs;
return both;
}

Share this post


Link to post
Share on other sites
The operator+ function will create a new 'string' object when it returns, and what happens when it does that depends on how you implemented the copy constructor 'string( const string& )'

Like for example, the code


class Foo
{
public:
Foo( const Foo& f )
: myName( f.myName + " (copy)" )
{
printf( "%s constructor\n", myName.c_str() );
}
Foo( const char* name )
: myName( name )
{
printf( "%s constructor\n", myName.c_str() );
}
~Foo()
{
printf( "%s destructor\n", myName.c_str() );
}

Foo Bar()
{
Foo f( "f" );
return f;
}

std::string myName;
};

int _tmain(int argc, _TCHAR* argv[])
{
Foo foo( "foo" );
Foo h = foo.Bar();
return 0;
}




outputs

foo constructor
f constructor
f (copy) constructor
f destructor
f (copy) destructor
foo destructor


I've never implemented a string class, but I think you are pretty much forced to make the copy in that situation. The only thing I can think of for improving efficiency is using string pooling, but I don't think that it's really that costly of an operation the way you have it written now.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
There is this trick that lets the compiler optimize a return by value. Make a constructor call as the expression for the return statement. For this to work, we need the help of a tailor-made constructor that does the exact job.

class string{
public:
string(const string& s1, const string& s2){
... // construct a concatenation of s1 and s2.
// this lets you control how much copying is done.
}
};

string operator+(const string& s1, const string& s2){
return string(s1, s2);
}

Most modern compilers identify that the return expression invokes the constructor of the return type. Thus, it will not generate code to call the copy constructor. Rather, it will simply construct the string using the specified constructor.

Regards,
yfed

Share this post


Link to post
Share on other sites
iMalc >> I've looked at the flipcode page, but I see that it's inline, and extremely complex, too many work-arounds. However, for later on, I will most likely use this.

Anonymous >> It's a plausible option, but not entirely what I'm looking for. Plus, if I needed to make multiple two-argument operators [strings were only used as an example in context] it would be quite odd. It has too many work-arounds, plus I prefer to refrain from inlining everything (sorry if I wasn't clear on that earlier).

NumberXaero >> What's the difference in the second one, other than the name?

Thanks for the help!

I've been messing around with the -S option in MinGW to get the assembly (and in the process I'm learning ASM), and I've discovered that MinGW only adds the value to be returned right before the leave-ret instructions are executed.
Right now, I'm trying to devise a workable work-around (ugly macros and inline assembly) just for the fun of it.

Now that I'm rethinking it, I'm not so sure that I'll be able to do this, depending on how the compiler structures its return commands. I hope I understand what I just said.

Share this post


Link to post
Share on other sites
The easiest thing to do here is take advantage of (N)RVO, or (Named) Return Value Optimization, something the C++ standard allows compiler implementations to do - and which modern compilers *do* do.

Targeting this compiler optimization is almost a little backwards. Taking an example, compiling on my iBook with GCC 4.0.1:

#include <iostream>
using namespace std;

struct foo {
foo & operator+=( const foo & ) { return *this; }
foo operator+( const foo & other ) { foo f(*this); return f += other; }

~foo() { cout << "~foo()" << endl; }
};

int main() {
foo a,b;
foo c = a + b;
}


We will see that compiling using g++ (with and without -O3) will result in 4 calls to ~foo() - 1 extra than the absolute minimum number of objects we could reasonably expect to exist without. So I did what any programmer would do in such a situation - play around with operator+:

foo operator+( const foo & other ) { return foo(*this) += other; } //nope, still 4
foo operator+( const foo & other ) { foo f(*this); f += other; return f; } //only 3 calls to ~foo()!


What's this? The most drawn out version is the best optimized one?

Well, kind of. I'm not 100% certain the standard allows for optimizing the other case, since we're depending on operator+='s by-reference return to return from operator+ in the examples in the full snippet and the 1st alternative example. There may be some real imputus behind the Value part of the (N)RVO name. At the very least, it's not hard to see that the compiler would have an easier time optimizing a local or temporary rather than a by-reference return from another function.

If your compiler supports RVO but not NRVO (e.g. optimizes returned temporaries, but not a returned local), then you'll want to go with the AP's constructor method (even if you make it private and pretend it dosn't exist from the "outside world").

More details will be implementation dependant, so feel free to hit up some of the documentation you can find with [google].

Share this post


Link to post
Share on other sites
Another possible solution is to implement copy-on-write semantics. Whenever you would have normally made a deep copy, you instead made a shallow copy and keep track of how many shallow copies exist. In the destructor, you decrement the count and only free the buffer containing the actual string if no more copies exist. Deep copying of the actual character buffer only occurs in mutating operations, which decrement the copy count, free the buffer if there were no copies, and set their own (not shared) copy count to 0.

When returning such a string by value, the string copied to the correct position on the stack is only a shallow copy. The count of the number of copies will be incremented, then the destructor will be called on the original copy, decrementing the counter.

Really it's just reference counting on the character buffer that is backing the string.

Of course if the compiler supports the optimizations in the above posts, those methods will be better. Copy on write will only be worth it if the compiler can't do those optimizations or if you make copies of the string that are never actually altered (something that can be avoided through good design).

Share this post


Link to post
Share on other sites
Quote:
Original post by deadimp
iMalc >> I've looked at the flipcode page, but I see that it's inline, and extremely complex, too many work-arounds. However, for later on, I will most likely use this.
Yes I agree that the code in that article is very complex and has many special cases and is difficult to learn and get right.
However, in all truthfulness, expression templates really are the best solution to get the generated code to be as optimal as possible. None of the other options mentioned will able to do as well for anything but fairly simple expressions.

Also, unfortunately the link I gave you is very nasty and uses an unnecesarily complicated number of methods. It is very outdated too, and includes loop unrolling metaprograms, which are not necessary when using such techniques with newer compilers as they already take care of that, and do an even better job too.

I have seen other links which explain it better or have cleaner code, but didn't have those links in my favourites.

Actually I myself have written a more modern, more flexible, simpler and easier to follow version of expression templates for vector math. I also made debugging easier by only using expression templates for release builds.
I am yet to publish it in any form, but I could perhaps send you what I have so far.

Share this post


Link to post
Share on other sites
In the family of copy-on-write optimizations, Alexandrescu has written something called a move constructors library that helps reduce unnecessary copying. It depends a little on Loki, but just for some template specialization things.

Move Constructors [cuj.com]

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!