Question

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 ?

Was it helpful?

Solution

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

OTHER TIPS

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.

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 :)

it can (almost) optimise the entire g() function call away, in which case your code looks like this:

A a;
A c = a;

as effectively this is what your code is doing. Now, as you pass a as a by-value parameter (ie not a reference) then the compiler almost has to perform a copy there, and then it returns this parameter by value, it has to perform another copy.

In the case of f(), as it it returning what is effectively a temporary, into a uninitialised variable, the compiler can see that it is safe to use c as the storage for the internal variable inside f().

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top