类的初始化过程中会发生什么?

发布于 2024-12-02 05:01:34 字数 987 浏览 0 评论 0原文

这是令我困惑的代码:

#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;
}

环境是 Fedora 15 上的 g++ 4.6.0。 第一个代码片段输出如下:

constructor
constructed by parameter 5
copy ctor
assignment
destructed
destructed
destructed

第二个片段代码输出为:

constructed by parameter 5
copy ctor
destructed
destructed

为什么第一个示例中调用了三个析构函数,而第二个示例中只调用了两个?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

心房敞 2024-12-09 05:01:34

第一种情况:

B t1;
t1 =  play(5);
  1. 通过调用B的默认构造函数创建对象t1
  2. 为了调用play(),使用B(int i)创建了B的临时对象。 5 作为 an 传递,并创建 B 对象,并调用 play()
  3. play() 内的 return b; 会导致调用复制构造函数来返回对象的副本。
  4. t1 = 调用Assignemnt 运算符,将返回的对象副本分配给t1
  5. 第一个析构函数,析构在#3中创建的临时对象。
  6. 第二个析构函数析构 #2 中返回的临时对象。
  7. 第三个析构函数析构对象t1

第二种情况:

B t1 = play(5);  
  1. B的临时对象是通过调用B的参数化构造函数创建的,该构造函数将int作为参数参数。
  2. 这个临时对象用于调用类B复制构造函数
  3. 第一个析构函数析构在 #1 中创建的临时对象。
  4. 第二个析构函数析构对象t1

在第二种情况下,一次析构函数调用较少,因为在第二种情况下,编译器使用返回值优化 并在从 play() 返回时省略创建额外临时对象的调用。相反,Base 对象是在分配临时对象的位置创建的。

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.

宣告ˉ结束 2024-12-09 05:01:34

首先,检查子表达式 play(5)。该表达式在两种情况下都是相同的。

在函数调用表达式中,每个参数都是从其参数复制初始化的 (ISO/IEC 14882:2003 5.2.2/4)。在本例中,这涉及到使用非显式构造函数将 5 转换为 B,该构造函数采用 int 创建临时 B< /code> 然后使用复制构造函数初始化参数b。但是,根据 12.8 中指定的规则,允许实现通过使用 int 的转换构造函数直接初始化 b 来消除临时值。

play(5) 的类型是 B 并且 - 作为返回非引用的函数 - 它是一个右值

return 语句将返回表达式隐式转换为返回值的类型 (6.6.3),然后使用转换后的表达式复制初始化 (8.5/12) 返回对象。

在这种情况下,返回表达式已经是正确的类型,因此不需要转换,但仍然需要复制初始化。


除了返回值优化

命名返回值优化(NRVO)是指返回语句为 if 形式 return x; where x 是函数本地的自动对象。当发生时,允许实现在返回值的位置构造x,并消除返回处的复制初始化。

尽管标准中没有这样命名,但 NRVO 通常指的是 12.8/15 中描述的第一种情况。

这种特殊的优化在 play 中是不可能的,因为 b 不是函数体的本地对象,它是在调用时已经构造的参数的名称。输入函数。

(未命名的)返回值优化(RVO)对其所指的含义甚至不太一致,但通常用于指返回表达式不是命名对象而是转换为返回类型和 复制初始化可以组合起来,以便直接根据消除一个临时对象的转换结果来初始化返回对象。

RVO 不适用于 play,因为 b 已经是 B 类型,因此复制初始化是等效的直接初始化并且不需要临时对象。


在这两种情况下,play(5) 都需要使用 B(int) 作为参数构造 B 并复制初始化 B 到返回对象。它还可以在参数的初始化中使用第二个副本,但即使没有明确请求优化,许多编译器也会消除此副本。这些对象(或全部)都是临时对象。

表达式语句t1 = play(5);中会调用复制赋值运算符,将play的返回值复制到t1 并且两个临时变量(play 的参数和返回值)将被销毁。当然,t1 必须在此语句之前构建,并且其析构函数将在其生命周期结束时被调用。

在声明语句 B t1 = play(5); 中,逻辑上 t1 使用 play 的返回值进行初始化,并且将使用与表达式完全相同数量的临时变量语句t1 = play(5);。然而,这是 12.8/15 中介绍的第二种情况,其中允许实现消除用于 play 返回值的临时对象,而是允许返回对象别名 t1< /代码>。 play 函数的操作方式完全相同,但因为它的返回对象只是 t1 的别名,所以它的 return 语句有效地直接初始化 t1 并没有单独的临时对象用于需要销毁的返回值。

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.

走走停停 2024-12-09 05:01:34

第一个片段构造了三个对象:

  • B t1
  • B(5) <- from (int) constructor;这是 play 函数
  • 返回 b 的临时对象;或 B(b) <- copy ctor

这是我的猜测,尽管它看起来效率低下。

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.

半岛未凉 2024-12-09 05:01:34

请参阅 Als 发布的第一个场景的详细情况。

我认为(编辑:错误;见下文)与第二种情况的区别在于编译器足够聪明,可以使用 NRVO(名为返回值优化)并删除中间副本:而不是在返回时创建临时副本(从播放中) ),编译器使用 play 函数内部的实际“b”作为 t1 复制构造函数的右值。

Dave Abrahams 有一篇文章副本省略,这里是维基百科上的 返回值优化

编辑:实际上,阿尔斯也添加了第二个场景的详细剧情。 :)

进一步编辑:实际上,我上面错了。在这两种情况下都不会使用 NRVO,因为根据接受的答案 为此问题

即使 NRVO 被允许,我们也可以知道它至少在第一种情况下没有被使用:如果是的话,第一种情况就不会涉及任何复制构造函数。第一种情况中的复制构造函数来自从命名值 b(在 play 函数中)到 play 的隐藏返回值位置的隐藏副本。第一种情况不涉及显式的复制构造,因此这是它唯一可能出现的地方。

实际情况是这样的:在这两种情况下都不会发生 NRVO,并且在返回时创建隐藏副本...但在第二种情况下,编译器能够直接在 t1 的位置构造隐藏的返回副本。因此,从 b 到返回值的复制没有被省略,但从返回值到 t1 的复制被省略。然而,对于第一种情况,t1 已经被构造,编译器很难进行这种优化(阅读:它没有这样做;))。如果 t1 已在与返回值位置不兼容的地址处构造,则编译器无法直接将 t1 的地址用于隐藏的返回值副本。

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.

ま柒月 2024-12-09 05:01:34

在第一个示例中,您调用三个构造函数:

  • 声明 B t1; 时的 B() 构造函数,这也是一个定义 if B() 是公开的。换句话说,编译器将尝试将任何声明的对象初始化为某种基本的有效状态,并将 B() 视为转换 B 大小的内存块的方法进入所述基本有效状态,以便在 t1 上调用的方法不会破坏程序。

  • B(int) 构造函数,用作隐式转换; play() 接受 B 但被赋予 int,但 B(int) 被视为将 int 转换为 B 的方法.

  • B(const B& rhs) 复制构造函数,它将把 play() 返回的 B 的值复制到临时值,这样它就有足够长的作用域,可以在赋值运算符中使用。

当作用域退出时,上述每个构造函数都必须与析构函数相匹配。

但是,在第二个示例中,您使用 play() 的结果显式初始化 t1 的值,因此编译器不需要浪费周期来提供基本的在将 play() 结果的副本分配给新变量之前,先将状态设置为 t1。因此,您只需调用

  • 的有用参数
  • ,以便将 t1 初始化为(无论什么)您的复制构造函数决定是)的正确副本play() 的结果。

在这种情况下,您看不到第三个构造函数,因为编译器将 play() 的返回值“省略”到 t1 中;也就是说,它知道在 play() 返回之前 t1 并不以有效状态存在,因此它只是将返回值直接写入为 预留的内存中>t1

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文