由于构造函数初始值设定项列表而优化

Optimization due to constructor initializer list

本文关键字:列表 优化 构造函数      更新时间:2023-10-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)

现在不可能笼统地说哪种形式更有效。在您的特定类中,默认构造函数的成本加上赋值的成本是否比非默认构造函数更贵?如果是这样,那么初始化列表的效率会更高。否则就不是了。

我写过的大多数类都会在init列表中更有效地初始化。但是,这是一条经验法则,可能并非适用于所有可能的情况。

否则,您将调用默认构造函数并,然后执行赋值。这需要一步的时间,并且可能会变得非常低效,这取决于初始化的性质。

因为它直接初始化,而不是默认初始化然后分配。这可能对POD的性能没有影响,但如果类型构造函数正在做繁重的工作,则会影响性能。

此外,在某些情况下,必须使用init列表,因此为了一致性,应该始终这样做。

来自C++常见问题解答:

考虑以下使用初始化列表初始化成员对象x_的构造函数:Fred::Fred((:x_(随便({}。这样做最常见的好处是提高性能。例如,如果表达式whatever与成员变量x_的类型相同,则whatever表达式的结果直接在x_内部构造——编译器不会单独复制对象。即使类型不相同,编译器通常也能更好地处理初始化列表,而不是赋值。

构建构造函数的另一种(低效(方法是通过赋值,例如:Fred::Fred(({x_=whatever;}。在这种情况下,表达式whatever导致创建一个单独的临时对象,并且这个临时对象被传递到x_对象的赋值运算符中。然后在;处对该临时对象进行销毁;。这是低效的。

似乎这还不够糟糕,在构造函数中使用赋值时,还有另一个效率低下的原因:成员对象将由其默认构造函数完全构造,例如,这可能会分配一些默认内存量或打开一些默认文件。如果whatever表达式和/或赋值运算符导致对象关闭该文件和/或释放该内存(例如,如果默认构造函数没有分配足够大的内存池,或者打开了错误的文件(,那么所有这些工作都可能是徒劳的。

防止双重初始化。

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

就POD类型而言,初始化和赋值应该是等效的,因为如果没有显式执行初始化,它们将处于未初始化状态,因此唯一的操作仍然是赋值。

对于具有默认构造函数和赋值运算符的类来说,情况有所不同:首先必须调用默认构造函数,然后调用赋值运算符(在构造函数的内部(,而不是直接以正确的状态创建对象。这肯定比从一开始就用正确的构造函数初始化对象效率更高(两步而不是一步,第一步-默认构造-通常完全浪费了(。

直接初始化还带来了一个优势,即可以确保,如果执行到达构造函数的主体,则所有不同的字段都已处于"正确"状态。

假设您的类中有一个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.

因为如果您使用的是inizalizer列表,那么您调用的是该对象的构造函数副本。

而如果在构造函数体内部初始化对象,则表示正在执行赋值。

示例:这里我调用int.的复制构造函数


  myClass::myClass( int x ) : member(x) {}

而在这里我调用运算符=(const int&(。旁白


    myClass::myClass( int x )
    {
         member = x;
    }

通常,asignment所做的操作比简单的复制要多。您必须同时保留帐户中的临时对象!