Всегда ли список инициализации обрабатывается перед кодом конструктора?

Другими словами, будет ли следующий код всегда печатать <unknown>, а сконструированный класс будет иметь "известный" как значение для source_ (если глобальная переменная something равна true) ?

class Foo {
  std::string source_;
public:
  Foo() : source_("<unknown>") {
    std::cout << source_ << std::endl;
    if(something){
      source_ = "known";
    }
  }
};
18
Frank 7 Апр 2009 в 06:29

3 ответа

Лучший ответ

Да, согласно C++11: 12.6.2 /10 (тот же раздел в C++14, 15.6.2 /13 в C++17):


В конструкторе без делегирования инициализация происходит в следующем порядке (выделено жирным шрифтом):

  • Во-первых, и только для конструктора самого производного класса (1.8) виртуальные базовые классы инициализируются в том порядке, в котором они появляются при обходе направленного ациклического графа базовых классов в глубину слева направо, где «left- to-right »- это порядок появления базовых классов в списке базовых спецификаторов производных классов.

  • Затем прямые базовые классы инициализируются в порядке объявления, как они появляются в списке-спецификаторах базы (независимо от порядка инициализаторов памяти).

  • Затем не статические члены данных инициализируются в порядке, в котором они были объявлены в определении класса (опять же, независимо от порядка mem-инициализаторов).

  • Наконец, выполняется составной оператор тела конструктора.


Основная причина использования списков инициализации - помочь компилятору в оптимизации. Списки инициализации для неосновных типов (т. Е. Объектов класса, а не int, float и т. Д.), Как правило, могут быть созданы на месте.

Если вы создаете объект, а затем назначаете его в конструкторе, это обычно приводит к созданию и уничтожению временных объектов, что неэффективно.

Init-списки могут избежать этого (если компилятор, конечно, готов, но большинство из них должно быть).

Следующая полная программа выведет 7, но это для конкретного компилятора (CygWin g ++), поэтому это не гарантирует такого поведения больше, чем образец в исходном вопросе.

Однако, как следует из ссылки в первом абзаце выше, стандарт действительно гарантирует это.

#include <iostream>
class Foo {
    int x;
    public:
        Foo(): x(7) {
            std::cout << x << std::endl;
        }
};
int main (void) {
    Foo foo;
    return 0;
}
22
paxdiablo 15 Июн 2018 в 09:58
Это заставляет меня задуматься, почему люди просто не пытаются сначала скомпилировать исходный код :)
 – 
arul
7 Апр 2009 в 06:37
2
@arul: @dehmann может иметь только один компилятор. Если это поведение определяется реализацией, компиляция не поможет. Вам нужно будет обратиться к стандарту.
 – 
paxdiablo
7 Апр 2009 в 06:40
1
Да, простая компиляция и проверка не говорят вам, будет ли это работать одинаково везде.
 – 
Frank
7 Апр 2009 в 06:51
Одна из причин, по которой я задал этот вопрос, заключается в том, что я, кажется, помню, что в конструкторе я должен ссылаться на аргументы конструктора, но не на переменные-члены, которые только что были инициализированы, например: Foo (std :: string str): str_ ( str) {// теперь ссылаемся на str, а не на str_}. Но похоже, что это неправильно.
 – 
Frank
7 Апр 2009 в 06:52
1
Я не согласен с тем, что оптимизация является основной причиной для списков инициализации, а скорее с построением баз и членов, которые не могут быть построены по умолчанию (чьи типы требуют параметров для конструктора). Это слишком сложное обсуждение, чтобы вести его только в разделе комментариев. Не буду голосовать за / против
 – 
David Rodríguez - dribeas
7 Апр 2009 в 14:14

Да, C ++ конструирует все члены перед вызовом кода конструкции.

7
lothar 7 Апр 2009 в 06:31

Как уже было сказано, списки инициализации полностью выполняются до входа в блок конструктора. Таким образом, совершенно безопасно использовать (инициализированные) члены в теле конструктора.

В принятом ответе вы сделали комментарий о необходимости ссылаться на аргументы конструктора, но не на переменные-члены внутри блока конструктора. Вы этого не сделаете.

Возможно, вы ошибочно приняли тот факт, что вы должны ссылаться на параметры, а не на атрибуты членов внутри списка инициализации. В качестве примера, учитывая класс X, который имеет два члена (a_ и b_) типа int, следующий конструктор может быть некорректно определен:

 X::X( int a ) : a_( a ), b( a_*2 ) {}

Возможная проблема здесь в том, что построение элементов в списке инициализации зависит от порядка объявления в классе, а не от порядка, в котором вы вводите список инициализации. Если бы класс был определен как:

class X
{
public:
   X( int a );
private:
   int b_;
   int a_; 
};

Затем, независимо от того, как вы вводите список инициализации, факт в том, что b_ (a_ * 2) будет выполняться до инициализации a_, поскольку объявление членов сначала b_, а затем a_. Это создаст ошибку, поскольку ваш код считает (и, вероятно, зависит) от того, что b_ в два раза больше значения a_, а на самом деле b_ содержит мусор. Самое простое решение не относится к членам:

 X::X( int a ) : a_( a ), b( a*2 ) {} // correct regardless of how X is declared

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

7
David Rodríguez - dribeas 7 Апр 2009 в 10:05