开发者

What happens during initialization of a class?

Here is the code which confuses me:

#include <iostream>
using namespace std;

class B {
public:
    B() {
        cout << "constructor\n";
    }
    B(const B& rhs) {
        cout << "copy ctor\n";
    }
    B & operator=(const B & rhs) {
        cout << "assignment\n";
    }
    ~B() {
        cout << "destructed\n";
    }
    B(int i) : data(i) {
        cout << "constructed by parameter " << data << endl;
    }

private:
    int data;
};

B play(B b)
{
    return b;
}

int main(int argc, char *argv[])
{
#if 1
    B t1;
    t1 =  play(5);
#endif

#if 0
    B t1 = play(5);
#endif

    return 0;
}

Environment is g++ 4.6.0 on Fedora 15. The first code fragment output is as follows:

constructor
constructed by parameter 5
copy ctor
assignment
destructed
destructed
destructed

开发者_开发知识库And the second fragment code output is:

constructed by parameter 5
copy ctor
destructed
destructed

Why are are three destructors are called in the first example, while in the second it is only two?


First Case:

B t1;
t1 =  play(5);
  1. Creates a object t1 by calling default constructor of B.
  2. In order to call play(), A temporary object of B is created by using B(int i). 5 is passed as an and object of B is created, and play() is called.
  3. return b; inside play() causes the copy constructor to be called for returning a copy of object.
  4. t1 = calls the Assignemnt operator to assign the returned object copy to t1.
  5. First destructor, destructs the temporary object created in #3.
  6. Second destructor destructs the returned temporay object in #2.
  7. Third destructor destructs the object t1.

Second case:

B t1 = play(5);  
  1. An temporary object of class B is created by calling parameterized constructor of B which takes int as a paraemter.
  2. This temporary object is used to call the Copy constructor of class B.
  3. First destructor destructs the temporary created in #1.
  4. Second destructor destructs object t1.

One destructor call is less in Second Case because, in second Case the compiler uses Return value Optimization and elides the call to create an additional temporary object while returning from play(). Instead the Base object is created in the location where the temporary would have been assigned.


First, examine the sub-expression play(5). This expression is the same in both cases.

In a function call expression each parameter is copy-initialized from its argument (ISO/IEC 14882:2003 5.2.2/4). In this case this involves converting 5 to a B by using the non-explicit constructor taking an int to create a temporary B and then using the copy-constructor to initialize the parameter b. However, the implementation is permitted to eliminate the temporary by directly initializing b using the converting constructor from int under the rules specified in 12.8.

The type of play(5) is B and - as function returning a non-reference - it is an rvalue.

The return statement implicitly converts the return expression to the type of the return value (6.6.3) and then copy-initializes (8.5/12) the return object with the converted expression.

In this case the return expression is already of the correct type, so no conversion is required but the copy initialization is still required.


Aside on return value optimizations

The named return value optimization (NRVO) refers to the situation where the return statement is if the form return x; where x is an automatic object local to the function. When occurs the implementation is allowed to construct x in the location for the return value and eliminate the copy-initialization at the point of return.

Although it is not named as such in the standard, NRVO usually refers to the first situation described in 12.8/15.

This particular optimization is not possible in play because b is not an object local to the function body, it is the name of the parameter which has already been constructed by the time the function is entered.

The (unnamed) return value optimization (RVO) has even less agreement on what it refers to but is usually used to refer to the situation where the return expression is not a named object but an expression where the conversion to the return type and copy-initialization of the return object can be combined so that the return object is initialized straight from the result of the conversion eliminating one temporary object.

The RVO doesn't apply in play because b is already of type B so the copy-initialization is equivalent to direct-initialization and no temporary object is necessary.


In both cases play(5) requires the construction of a B using B(int) for the parameter and a copy-initialization of B to the return object. It may also use a second copy in the initialization of the parameter but many compilers eliminate this copy even when optimizations are not explicitly requested. Both (or all) of these objects are temporaries.

In the expression statement t1 = play(5); the copy assignment operator will be called to copy the value of the return value of play to t1 and the two temporaries (parameter and return value of play) will be destroyed. Naturally t1 must have been constructed prior to this statement and its destructor will be called at the end of its lifetime.

In the declaration statement B t1 = play(5);, logically t1 is initialized with the return value of play and exactly the same number of temporaries will be used as the expression statement t1 = play(5);. However, this is the second of the situations covered in 12.8/15 where the implementation is allowed to eliminate the temporary used for the return value of play and instead allow the return object to alias t1. The play function operates in exactly the same way but because it the return object is just an alias to t1 its return statement effectively directly initializes t1 and there is no separate temporary object for the return value that needs to be destroyed.


The first fragment constructs three objects:

  • B t1
  • B(5) <- from (int) constructor; this is temporary object for play function
  • return b; or B(b) <- copy ctor

This is my guess, although it looks inefficient.


Refer to what Als posted for a play-by-play of the first scenario.

I think (EDIT: wrongly; see below) the difference with the second case is that the compiler was smart enough to use the NRVO (named return value optimization) and elide the middle copy: Instead of creating a temporary copy on return (from play), the compiler used the actual "b" inside of the play function as the rvalue for t1's copy constructor.

Dave Abrahams has an article on copy elision, and here's Wikipedia on the return value optimization.

EDIT: Actually, Als added a play-by-play of the second scenario, too. :)

Further edits: Actually, I was incorrect above. The NRVO is not being used in either case, because the standard forbids eliding copies directly from function arguments (b in play) to the return value location of a function (at least without inlining), according to the accepted answer for this question.

Even if the NRVO were allowed, we can tell that it's not being used in the first case at least: If it were, the first case would not involve a copy constructor whatsoever. The copy constructor in the first case comes from the hidden copy from the named value b (in the play function) to the hidden return value location for play. The first case involves no explicit copy construction, so that is the only place where it can arise.

What's actually going on is this: NRVO is not occurring in either case, and a hidden copy is being created on return...but in the second case, the compiler was able to construct the hidden return copy directly at t1's location. So, the copy from b to the return value was not elided, but the copy from the return value to t1 was. However, the compiler had a harder time doing this optimization for the first case where t1 was already constructed (read: it didn't do it ;)). If t1 is already constructed at an address incompatible with the return value's location, the compiler isn't able to use t1's address directly for the hidden return value copy.


In your first example, you're calling three constructors:

  • The B() constructor when you declare B t1;, which is also a definition if B() is public. In other words, the compiler will try to initialize any declared objects to some basic valid state, and treats B() as the method for transforming a B-sized block of memory into said basic valid state, so that methods called on t1 won't break the program.

  • The B(int) constructor, used as an implicit conversion; play() takes a B but was given an int, but B(int) is considered a method for converting int to B.

  • The B(const B& rhs) copy constructor, which will copy the value of the B returned by play() into a temporary value so that it will have scope long enough to survive being used in an assignment operator.

Each of the above constructors must be matched with a destructor when the scope exits.

In your second example, however, your are explicitly initializing the value of t1 with the result of play(), so the compiler doesn't need to waste cycles providing a basic state to t1 before it assigns a copy of play()'s result to the new variable. So you only call

  • B(int) to get a useful argument for play(B)

  • B(const B& rhs) so that t1 will be initialized with (whatever your copy constructor decides is) a proper copy of play()'s results.

You don't see a third constructor in this case because the compiler is "eliding" the returned value of play() into t1; that is, it knew that t1 did not exist in a valid state before play() returns, so it's just writing the return value directly into the memory set aside for t1.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜