Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Question About Return Value Optimization

Tags:

c++

There are many questions on this site about return value optimization (I suppose it's a fairly confusing topic), but I can't seem to find one that answers my particular question. If I have code that looks like this:

returnType function() { stuff....}

void someOtherFunction()
{
    returnType var = function();
    more stuff...
}

I am told that the compiler may decide to not use two instances of returnType in someOtherFunction(). Logically, I would expect that function() would generate an object of type returnType, and someOtherFunction() would receive that value via copy constructor (overloaded or not) into a temporary value. I would then expect that temporary value to be copied via assignment (which could be overloaded and in theory could have any kind of functionality!) into var, which would have previously been initialized via the default constructor.

I see a potential issue here. What happens if there is not this temporary copy of returnType in someOtherFunction()? Wouldn't var have to be filled, via copy constructor, with the returned value from function() directly? If so, wouldn't the assignment operator never be called? If so, and if the assignment operator were overloaded, couldn't that change the functionality of the program? If so, does that mean that it's the programmer's responsibility to ensure that = always does the same thing as a copy constructor? And I hate to run this long chain of questions, but if so, why does C++ allow you to define copy constructors to do something other than assignment?

like image 471
Gravity Avatar asked Jul 29 '11 22:07

Gravity


2 Answers

I would then expect that temporary value to be copied via assignment (which could be overloaded and in theory could have any kind of functionality!) into var, which would have previously been initialized via the default constructor.

Well, for starters, that's quite wrong.

int x = 0;
int x(0);

Those two lines are the same thing- a constructor call. There are some differences- the first cannot call explicit constructors, I believe- but they are the same function call. There is no default construction and no assignment operator call. They are both direct constructions.

Basically, the Standard says "If you do something other than copying an object in the copy constructor, it's your own dumb fault and I laugh as your program doesn't exhibit the expected behaviour when the optimizer eliminates the calls". Those are my own paraphrased words, of course, but the Standard is very explicit on the optimizer being allowed to eliminate copies. In C++0x then this applies to moves too.

Your code fragment above is really

returnType function() { stuff....}

void someOtherFunction()
{
    returnType var(function());
    more stuff...
}

That isn't the optimized version, that's what it really is. The assignment operator is never called. And with NRVO, it looks something like

void function(void* mem) { // construct return value into mem
    new (mem) returnType;
    // Do shiz with returnType;
}
void someOtherFunction() {
    // This doesn't respect some other things like alignment
    // but it's the basic idea
    char someMemory[sizeof(returnType)];
    function(someMemory);
    // more stuff here
}

Of course, the compiler also has to deal with destructing the object even in the case of an exception, and making sure all aliases are of the correct type, and alignment, and some other things I didn't deal with in my sample, but hopefully you get the general gist.

like image 118
Puppy Avatar answered Oct 20 '22 15:10

Puppy


To answer your last few questions:

... if the assignment operator were overloaded, couldn't that change the functionality of the program? If so, does that mean that it's the programmer's responsibility to ensure that = always does the same thing as a copy constructor? And I hate to run this long chain of questions, but if so, why does C++ allow you to define copy constructors to do something other than assignment?

Yes, the assignment operator can in fact change the functionality of the program. But it's totally possible for the assignment operator to do something different from the copy constructor and yet still have the same observable behavior. For example, for a String class I may define the assignment operator in terms of the copy constructor:

class String
{
public:
    String(const wchar_t* str) : buffer(str) {}
    String(const String& rhs) : buffer(rhs.buffer) {}

    String& operator=(String rhs) // copy-and-swap idiom
    {
         Swap(rhs);
         return *this;
    }

    // ...

    void Swap(String& rhs)
    {
        buffer.Swap(rhs.buffer);
    }

private:
    // StringBuffer is an RAII wrapper for a string character array
    // allocated on the free store
    StringBuffer buffer;
};

In the above code, the copy-constructor and assignment operator do essentially the same thing. But if the receiving String has enough memory to hold the source string, it's rather wasteful to throw it away instead of simply overwriting the existing contents.

String& operator=(const String& rhs)
{
    // We don't have enough room to hold the source string.
    // Copy over to a new, bigger buffer.
    if(rhs.buffer.Length() > buffer.Length())
    {
        String temp(rhs);
        Swap(temp);
        // temp holds our old buffer now, and will be destroyed
        // when we exit this scope thanks to RAII.
    }
    else
    {
        // Instead of throwing away our existing buffer and having
        // to allocate a new one, let's just overwrite what we
        // have since our buffer is big enough.
    }
}

Clearly, assigning to a String involves completely different code from copy-constructing a String, but yet still have the same observable behavior (now we have two legitimate copies of the string). The difference is that this more complicated String assignment operator can avoid having to do more memory allocations than necessary.

If the C++ language forced you to make the copy-constructor and assignment operator do the exact same thing, I would not be able to make this kind of optimization. Yes, I am responsible for making sure the copy-constructor and assignment operator do what you think it does, but that's true for all other operators/special member functions anyway.

like image 37
In silico Avatar answered Oct 20 '22 16:10

In silico