由于构造函数初始化列表的优化

15

如果可能的话,构造函数应通过初始化列表初始化其所有成员对象。这比在构造函数体内通过赋值构建构造函数更有效率。

有人能否通过示例来解释为什么使用初始化列表更有效率?


为了完整起见 - 如果我们假设优化器是明智的,那么这个问题是否简化为“默认构造函数然后operator=与特定构造函数”? - Kos
8个回答

16

考虑以下程序:

#include <iostream>

struct A {
  A() { std::cout << "A::A()\n"; }
  A(int) { std::cout << "A::(int)\n"; }
  void operator=(const A&) { std::cout << "A::operator=(const A&)\n"; }
};

struct C1 {
  A a;
  C1(int i) { 
    a = i;
  }
};

struct C2 {
  A a;
  C2(int i)  : a(i) {}
};

int main() {
  std::cout << "How expesive is it to create a C1?\n";
  { C1 c1(7); }
  std::cout << "How expensive is it to create a C2?\n";
  { C2 c2(7); }
}

在我的系统上(Ubuntu 11.10,g++ 4.6.1),该程序输出如下:

How expesive is it to create a C1?
A::A()
A::(int)
A::operator=(const A&)
How expensive is it to create a C2?
A::(int)

现在,考虑为什么会这样。在第一种情况下,C1::C1(int)中,必须先对a进行默认构造,然后才能调用C1的构造函数。然后它通过operator=进行赋值。在我的简单示例中,没有可用的int赋值运算符,因此我们必须从int构造出一个A。因此,不使用初始化程序的代价是:一个默认构造函数,一个int构造函数和一个赋值运算符。

在第二种情况下,C2::C2(int)只调用了int构造函数。无论默认A构造函数的成本是多少,显然C2:C2(int)的成本都不会大于C1::C1(int)的成本。


或者,考虑以下替代方案。假设我们给A添加以下成员:

void operator=(int) { std::cout << "A::operator=(int)\n"; }

那么输出将会是:

How expesive is it to create a C1?
A::A()
A::operator=(int)
How expensive is it to create a C2?
A::(int)

现在无法一般性地说哪种形式更有效率。在你具体的类中,一个默认构造函数加上一个赋值操作的成本是否比一个非默认构造函数更昂贵?如果是这样,那么初始化列表更有效率。否则就不是。

我编写的大多数类都可以更有效地在初始化列表中初始化。但是,这只是一个经验规则,并不适用于每种可能的情况。


哇..这是一个很好的例子来解释。我理解了我的疑问,你为C++初学者提供了比问题所期望的更多的启示 :).. - nitin_cherian
1
我有一个疑问。为什么在C1 c1(7)的情况下应该调用int构造函数。只需要调用默认构造函数和赋值运算符就可以了,对吗? - nitin_cherian
@LinuxPenseur - 默认构造函数被调用来创建赋值表达式右侧的临时对象。必须创建该临时对象以调用 A::operator=(const A&)。如果 A 更完整 -- 如果它包括 A::operator=(int) -- 那么默认构造函数就不会被调用。 - Robᵩ
3
赋值运算符以参数 A 为输入,因此语句 a=7 首先会使用 A::A(int) 创建一个临时 A 对象,然后再调用赋值运算符。字面值 7 会被转换成 A 类型。 - Luc Touraille
@Rob:这不是默认构造函数,而是由一个int参数化的构造函数被调用。 - Luc Touraille
@LucTouraille - 感谢您的纠正,您是正确的,我说错了。我的评论应该是“调用int构造函数来创建...然后int构造函数将不会...”。 - Robᵩ

9

否则,您将调用默认构造函数,然后执行赋值操作。这是一步更长的过程,根据初始化的性质可能会变得非常低效。


初始化列表是否能节省 CPU 周期?您能解释一下相关内容吗? - nitin_cherian
2
根据您特定对象的默认构造和赋值的性质,它可能会节省大量的CPU周期。同样,因为在构造函数体中进行赋值意味着首先执行默认构造函数,然后丢弃(很可能是)通过分配新值完成的任何内容(或部分内容)。这显然比一开始就将正确的数据放入更需要CPU工作。 - Michael Krelin - hacker

5
因为它直接初始化,而不是默认初始化再赋值。对于POD类型,它在性能方面可能并不重要,但如果类型构造函数执行的是较重的工作,则很重要。
此外,在某些情况下,你必须使用初始化列表,因此出于一致性考虑,应始终这样做。

2
C++FAQ中了解到:
考虑使用初始化列表初始化成员对象x_的以下构造函数:Fred::Fred() : x_(whatever) { }。这样做最常见的好处是提高性能。例如,如果表达式whatever与成员变量x_的类型相同,则whatever表达式的结果直接在x_内部构建,编译器不会创建单独的对象副本。即使类型不相同,编译器通常也能够比赋值更好地处理初始化列表。
另一种(效率低下)构建构造函数的方法是通过赋值,例如:Fred::Fred() { x_ = whatever; }。在这种情况下,表达式whatever会导致创建一个单独的临时对象,并将该临时对象传递给x_对象的赋值运算符。然后,该临时对象在分号处被销毁。这是低效的。
如果这还不够糟糕,使用构造函数中的赋值还有另一个效率低下的来源:成员对象将完全由其默认构造函数构造,并且这可能会分配一些默认数量的内存或打开某个默认文件。如果任何表达式和/或赋值运算符导致对象关闭该文件和/或释放该内存(例如,如果默认构造函数没有分配足够大的内存池或者打开了错误的文件),则所有这些工作都可能是徒劳的。

0

就POD类型而言,初始值和赋值应该是等价的,因为如果没有显式执行初始化,则它们将被保留未初始化,因此唯一的操作是赋值。

对于具有默认构造函数和赋值运算符的类来说,情况就不同了:必须首先调用默认构造函数,然后调用赋值运算符(在构造函数体内)。这肯定比直接使用正确的构造函数初始化对象更加低效(需要两个步骤而不是一个步骤,并且第一个步骤 - 默认构造 - 通常完全浪费)。

直接初始化还具有一个优点,就是您可以确信,如果执行到构造函数的主体,所有各种字段已经处于其“正确”的状态。


0
为了防止重复初始化。
class B
{
//whatever
};

class A
{
   B _b;
public:
   A(B& b)
};

现在是两种情况:

//only initializes _b once via a copy constructor
A::A(B& b) : _b(b)
{
}

//initializes _b once before the constructor body, and then copies the new value
A::A(B& b)
{
   //_b is already initialized here
   //....
   //one extra instruction:
   _b = b;
}

0
假设您的类中有一个数据成员,其类型为std::string。当执行此类的构造函数时,将自动调用默认构造函数字符串类,因为对象在构造函数体之前初始化。
如果您在构造函数体内分配字符串,则会创建一个临时对象并将其赋给字符串的赋值运算符。临时对象将在赋值语句结束时被销毁。如果您在初始化程序列表中执行此操作,则不会创建临时对象。
class Person
{
public:
    Person(string s);
    ~Person();
private:
    string name;
};


// case 1
Person::Person(string s)
{
   name = s;   // creates a temporary object
}

// case 2
Person::Person(string s):name(s) {} // whereas this will not create a temporary object.

0

因为如果您正在使用初始化器列表,则调用的是该对象的构造函数副本。

而如果您在构造函数体内初始化对象,则正在执行赋值操作。

例如:在这里我调用了int的复制构造函数。


  <code>myClass::myClass( int x ) : member(x) {}</code>

while here i m calling the operator=(const int& ). the asignment


    <code>myClass::myClass( int x )
    {
         member = x;
    }</code>

通常赋值操作比简单的复制要执行更多的操作。你必须考虑到临时对象!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接