Оптимизация за счет списка инициализаторов конструктора

Конструкторы должны инициализировать все свои объекты-члены через список инициализаторов, если это возможно. Это более эффективно, чем создание конструкторов с помощью присваивания внутри тела конструктора.

Может ли кто-нибудь объяснить, почему более эффективно использовать список инициализаторов с помощью примера?


person nitin_cherian    schedule 05.12.2011    source источник
comment
Ради полноты: если мы предположим, что оптимизатор вменяемый, сводится ли эта проблема не более чем к конструктору по умолчанию, а затем к оператору = по сравнению с конкретным конструктором?   -  person Kos    schedule 05.12.2011


Ответы (8)


Рассмотрим эту программу:

#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, поэтому нам нужно построить A из int. Таким образом, стоимость неиспользования инициализатора составляет: один конструктор по умолчанию, один конструктор 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)

Сейчас вообще нельзя сказать, какая форма эффективнее. В вашем конкретном классе стоимость конструктора по умолчанию плюс стоимость присваивания дороже, чем конструктор не по умолчанию? Если это так, то список инициализации более эффективен. В противном случае это не так.

Большинство классов, которые я когда-либо писал, были бы более эффективно инициализированы в списке инициализации. Но это эмпирическое правило, и оно не может быть верным для каждого возможного случая.

person Robᵩ    schedule 05.12.2011
comment
Вау... отличный способ объяснить на примере. Я понял свои сомнения, и вы пролили больше света для новичка на С++, чем ожидает вопрос :).. - person nitin_cherian; 05.12.2011
comment
Я сомневаюсь. Почему конструктор int должен вызываться в случае C1 c1(7). Просто вызов конструктора по умолчанию и оператор присваивания должны были быть вызваны, верно? - person nitin_cherian; 05.12.2011
comment
@LinuxPenseur — конструктор по умолчанию вызывается для создания временного файла, который является правой частью выражения присваивания. Это временное должно быть создано для вызова A::operator=(const A&). Если бы A был более полным — если бы он включал A::operator=(int) — конструктор по умолчанию не вызывался бы. - person Robᵩ; 05.12.2011
comment
@LinuxPenseur: оператор присваивания принимает A в качестве параметра; следовательно, оператор a = 7 сначала создает временный A (используя A::A(int)) перед вызовом оператора присваивания. Литерал 7 преобразуется в A. - person Luc Touraille; 05.12.2011
comment
@Rob: вызывается не конструктор по умолчанию, а конструктор, параметризованный int. - person Luc Touraille; 05.12.2011
comment
@LucTouraille - Спасибо за исправление, вы правы, я оговорился. В моем комментарии следовало прочитать, что конструктор int вызывается для создания... тогда конструктор int не имел бы.... - person Robᵩ; 05.12.2011
comment
@LucTouraille: спасибо, что развеяли мои сомнения :) - person nitin_cherian; 06.12.2011

В противном случае вы вызываете конструктор по умолчанию и затем выполняете присваивание. Это на один шаг больше и может стать действительно неэффективным в зависимости от характера инициализации.

person Michael Krelin - hacker    schedule 05.12.2011
comment
Сохраняет ли список инициализаторов циклы процессора? Не могли бы вы объяснить, связанные с этим? - person nitin_cherian; 05.12.2011
comment
В зависимости от характера конструкции по умолчанию и назначения для ваших конкретных объектов это может сэкономить массу циклов процессора. Опять же, потому что присваивание в теле конструктора означает сначала выполнение конструктора по умолчанию, затем отбрасывание (вероятно) всего (или части того), что было сделано путем присвоения нового значения. Очевидно, что это больше работы для процессора, чем размещение правильных данных в первую очередь. - person Michael Krelin - hacker; 05.12.2011

Потому что он инициализируется напрямую, а не инициализируется по умолчанию, а затем назначается. Вероятно, это не будет иметь значения с точки зрения производительности для POD, но будет иметь значение, если конструктор типов выполняет тяжелую работу.

Кроме того, в некоторых случаях вы должны использовать список инициализации, поэтому вы всегда должны делать это для согласованности.

person Cat Plus Plus    schedule 05.12.2011

Из Часто задаваемые вопросы по C++:

Рассмотрим следующий конструктор, который инициализирует объект-член x_ с помощью списка инициализации: Fred::Fred() : x_(независимо) { }. Наиболее распространенным преимуществом этого является повышение производительности. Например, если выражение what имеет тот же тип, что и переменная-член x_, результат выражения what создается непосредственно внутри x_ — компилятор не создает отдельную копию объекта. Даже если типы не совпадают, компилятор обычно лучше справляется со списками инициализации, чем с присваиваниями.

Другой (неэффективный) способ создания конструкторов — это присваивание, например: Fred::Fred() { x_ = what; }. В этом случае выражение what вызывает создание отдельного временного объекта, и этот временный объект передается в оператор присваивания объекта x_. Затем этот временный объект уничтожается в точке ;. Это неэффективно.

Как будто этого было недостаточно, есть еще один источник неэффективности при использовании присваивания в конструкторе: объект-член будет полностью создан своим конструктором по умолчанию, и это может, например, выделить некоторый объем памяти по умолчанию или открыть некоторый объем памяти по умолчанию. файл. Вся эта работа может оказаться напрасной, если любое выражение и/или оператор присваивания заставят объект закрыть этот файл и/или освободить эту память (например, если конструктор по умолчанию не выделил достаточно большой пул памяти или если он открыл неправильный файл).

person Drahakar    schedule 05.12.2011

Чтобы предотвратить двойную инициализацию.

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;
}
person Luchian Grigore    schedule 05.12.2011

Что касается типов POD, инициализация и присваивание должны быть эквивалентны, поскольку они остаются неинициализированными, если инициализация не выполняется явно, поэтому единственной операцией остается присваивание.

Иначе обстоит дело с классами, имеющими конструкторы по умолчанию и операторы присваивания: вместо того, чтобы создавать объект непосредственно в правильном состоянии, сначала должен быть вызван конструктор по умолчанию, а затем оператор присваивания (внутри тела конструктора). Это, безусловно, более неэффективно, чем инициализация объекта с помощью правильного конструктора с самого начала (два шага вместо одного, и первый шаг — построение по умолчанию — обычно полностью теряется).

Прямая инициализация также дает то преимущество, что вы можете быть уверены, что, если выполнение достигло тела конструктора, все различные поля уже находятся в своем «правильном» состоянии.

person Matteo Italia    schedule 05.12.2011

Предположим, у вас есть член данных в вашем классе, который имеет тип 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.
person Sanish Gopalakrishnan    schedule 05.12.2011

потому что, если вы используете список инициализаторов, вы вызываете копию конструктора этого объекта.

Хотя, если вы инициализируете объекты внутри тела конструктора, вы выполняете присваивание.

пример: здесь я вызываю конструктор копирования int.


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

в то время как здесь я вызываю operator=(const int& ). назначение


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

обычно присваивание выполняет больше операций, чем простая копия. Вы должны учитывать также временный объект!

person andrea.marangoni    schedule 05.12.2011