Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How actually does a function return by value?

If I have a class A (which returns an object by value ), and two functions f() and g() having difference in just their return variables :

class A
{
    public:
    A () { cout<<"constructor, "; }
    A (const A& ) { cout<<"copy-constructor, "; }
    A& operator = (const A& ) { cout<<"assignment, "; }
    ~A () { cout<<"destructor, "; }
};
    const A f(A x)
    {A y; cout<<"f, "; return y;}

    const A g(A x)
    {A y; cout<<"g, "; return x;}

main()
{
    A a;
    A b = f(a);
    A c = g(a);
}

Now when I execute the line A b = f(a);, it outputs:

copy-constructor, constructor, f, destructor, which is fine assuming that object y in f() is created directly at the destination i.e at the memory location of object b, and no temporaries involved.

While when I execute the line A c = g(a);, it outputs:

copy-constructor, constructor, g, copy-constructor, destructor, destructor,.

So the question is why in the case of g() cant the object be directly created at memory location of c, the way it happened while calling f() ? Why it calls an additional copy-constructor ( which I presume is because of the involvement of temporary ) in the 2nd case ?

like image 569
cirronimbo Avatar asked Jun 06 '12 12:06

cirronimbo


3 Answers

Here's a little modification of your code, that will help you to perfectly understand what's going on there:

class A{
public:
    A(const char* cname) : name(cname){
        std::cout << "constructing " << cname << std::endl;
    }
    ~A(){
        std::cout << "destructing " << name.c_str() << std::endl;
    }
    A(A const& a){
        if (name.empty()) name = "*tmp copy*";
        std::cout 
            << "creating " << name.c_str() 
            << " by copying " << a.name.c_str() << std::endl;
    }
    A& operator=(A const& a){
        std::cout
            << "assignment ( "
                << name.c_str() << " = " << a.name.c_str()
            << " )"<< std::endl;
        return *this;
    }
    std::string name;
};

Here's the usage of this class:

const A f(A x){
    std::cout 
        << "// renaming " << x.name.c_str() 
        << " to x in f()" << std::endl;
    x.name = "x in f()";
    A y("y in f()");
    return y;
}

const A g(A x){
    std::cout 
        << "// renaming " << x.name.c_str()
        << " to x in f()" << std::endl;
    x.name = "x in g()";
    A y("y in g()");
    return x;
}

int main(){
    A a("a in main()");
    std::cout << "- - - - - - calling f:" << std::endl;
    A b = f(a);
    b.name = "b in main()";
    std::cout << "- - - - - - calling g:" << std::endl;
    A c = g(a);
    c.name = "c in main()";
    std::cout << ">>> leaving the scope:" << std::endl;
    return 0;
}

and here's the output when compiled without any optimization:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
creating *tmp copy* by copying y in f()
destructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

The output you posted is the output of program compiled with Named Return Value Optimization. In this case the compiler tries to eliminate redundant Copy constructor and Destructor calls which means that when returning the object, it will try to return the object without creating redundant copy of it. Here's the output with NRVO enabled:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

In first case, *tmp copy* by copying y in f() is not created since NRVO has done its job. In second case though NRVO can't be applied because another candidate for return slot has been declared within this function. For more information see: C++ : Avoiding copy with the "return" statement :)

like image 34
LihO Avatar answered Oct 13 '22 18:10

LihO


The problem is that in the second case, you're returning one of the parameters. Given that usually parameter copying occurs at the site of the caller, not within the function (main in this case), the compiler makes the copy, and then is forced to copy it again once it enters g().

From http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

Second, I’ve yet to find a compiler that will elide the copy when a function parameter is returned, as in our implementation of sorted. When you think about how these elisions are done, it makes sense: without some form of inter-procedural optimization, the caller of sorted can’t know that the argument (and not some other object) will eventually be returned, so the compiler must allocate separate space on the stack for the argument and the return value.

like image 111
Dave S Avatar answered Oct 13 '22 19:10

Dave S


The difference is that in the g case, you are returning a value that was passed to the function. The standard explicitly states under which conditions the copy can be elided in 12.8p31 and it does not include eliding the copy from a function argument.

Basically the problem is that the location of the argument and the returned object are fixed by the calling convention, and the compiler cannot change the calling convention based on the fact that the implementation (that might not even be visible at the place of call) returns the argument.

I started a short lived blog some time ago (I expected to have more time...) and I wrote a couple of articles about NRVO and copy elision that might help clarify this (or not, who knows :)):

Value semantics: NRVO

Value semantics: Copy elision

like image 31
David Rodríguez - dribeas Avatar answered Oct 13 '22 18:10

David Rodríguez - dribeas