Порядок инициализации в конструкторах

    Итак, вот небольшая программа на C++:

    #include <iostream>
    
    class A {
    private:
      int a;
      int b;
    public:
      A(int x) : b(x), a(b) {}
      void dump() {
        std::cout << "a=" << a << " b=" << b << std::endl;
      }
    };
    
    int main() {
      A a(42);
      a.dump();
      return 0;
    }
    

    Если вы считаете, что она выдаст

    a=42 b=42
    


    То вы обманываетесь, она выдаст что-то вроде

    a=4379 b=42
    

    Это произойдёт потому, что компилятор будет инициализировать переменные не в том порядке, в котором они перечислены в строке

    A(int x) : b(x), a(b)

    а сперва будет инициализирована переменная «a», и лишь потом переменная «b». Так как на момент инициализации «a», переменная «b» ещё имеет неопределённое значение, то и «a» получит неопределённое значение.

    Ситуация становится ещё драматичней, если представить, что «a» и «b» не просто int-ы, а некие сложные объекты, у которых, скажем, параметры конструктора определяют количество выделяемой памяти. Тогда грабли могут ударить в лоб очень сильно.

    А в каком же порядке идёт инициализация?


    На самом деле, порядок инициализации никак не зависит от порядка в строке

    A(int x) : b(x), a(b)

    Всё определяется порядком деклараций:

      int a;
      int b;
    

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

    Вы можете убедиться в этом, поигравшись с вот таким примером

    #include <iostream>
    
    class S {
    private:
      int data;
    public:
      S(int x) {
        std::cout << "S(int x) x=" << x << std::endl;
        data = x;
      }
      S(S& x) {
        std::cout << "S(S& x) x.data=" << x.data << std::endl;
        data = x.data;
      }
      int dump() {
        return data;
      }
      ~S() {
        std::cout << "~S() x.data=" << this->data << std::endl;
      }
    };
    
    class A {
    private:
      S a;   // попробуйте переставить местами
      S b;   // эти две декларации
    public:
      A(int x) : b(x), a(b) {}
      void dump() {
        std::cout << "a=" << a.dump() << " b=" << b.dump() << std::endl;
      }
    };
    
    int main() {
      A a(1);
      a.dump();
      return 0;
    }
    

    У меня но выдал вот такой результат:

    S(S& x) x.data=134515845
    S(int x) x=1
    a=134515845 b=1
    ~S() x.data=1
    ~S() x.data=134515845
    

    Обратите внимание, что сперва был выполнен конструктор «S(S& x)». Если же переставить местам декларации, то всё будет работать правильно.

    Что это? Баг в С++?


    Нет, конечно!

    Дело в том, что при удалении объекта, все разрушительные действия должны выполняться в порядке, строго противоположном, порядку конструирования. Вместе с тем, C++ допускает сосуществование нескольких конструкторов. В каком же порядке разрушать части объекта, если порядок инициализации в разных конструкторах различен? Помнить, как именно был создан объект, может оказаться весьма дорого. Остаётся только одно — ввести для всех конструкторов строгий порядок инициализации, не связанный с их кодом.

    Что и было сделано.

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

    Всем успехов!

    upd: на это есть стандарт тут пункт 12.6.2 #5 стр 197.
    upd2: или http://www.kuzbass.ru:8086/docs/isocpp/special.html#class.base.init (спасибо EntropiouS)
    Поделиться публикацией

    Комментарии 74

      +5
      >> А чтобы не наступить случайно на эти грабли, лучше всегда описывать инициализацию в том же порядке, в каком декларируются члены класса.

      К тому же компилятор обычно предупреждает, если пренебречь этим советом.
        +3
        вот так не ругается
        $ c++ 2.cpp

        а вот так ругается
        $ c++ -Wall 2.cpp
        2.cpp: In constructor 'A::A(int)':
        2.cpp:26: warning: 'A::b' will be initialized after
        2.cpp:25: warning: 'S A::a'
        2.cpp:28: warning: when initialized here
          +4
          Ну мы же правильные программисты, мы же всегда включаем ворнинги.
            +3
            Ворнинги мало включить, их ещё нужно внимательно читать… побожитесь, что прям всегда читаете все ворнинги ,-)
              +6
              Ужасная фраза :) Ересь :)

              Когда писал на С++ — перед тем как релизить — ни одного ворнинга не должно быть.
              Не обращать внимания на ворнинги — это большое зло. Еще большее зло — релизить чтото, компилящееся с ворнингами(ну бесят они. проект с ворнингами — грязный проект :) ).
                +3
                Это понятно :-)
                Но грабли приходится ловить за долго до того как релизить :-)
                А на счёт грязных проектов… к сожалению не всегда весь код твой… часто приходится мириться с чужими ворнингами. Очень часто. И часто это связано даже не с небрежностью программистов, а с разными версиями компилляторов, операционок, библиотек… на которых велась разработка… В общем, я не верю, что старый кросплатформенный проект может собираться прям вот везде без ворнингов.
                  0
                  Для меня, обычного виндо-юзера, «старый кросплатформенный проект» — это чтото вроде динозавра :) Большой, страшный и очень редко встречающийся. Сидел бы на линуксе, мож понял бы вас лучше.
                    +1
                    Иногда ещё авторы компиляторов «из хороших соображений» делают новые ворнинги там, где раньше обходилось.

                    Например, последние версии Visual C++ ругаются на printf() и тому подобные вещи. Хотя я, естественно, не буду из-за этого менять старый, многократно испытанный код.
                      0
                      Эти ворнинги отключаются скопом одним махом.
                        0
                        Это ясно, но смысл ворнингов ведь не в том, чтобы их отключать?
                          +2
                          Если у вас «многократно испытанный код», который вы не собираетесь изменять из-за нововведенных ворнингов компилятора, то лучше их отключить вовсе, чтобы среди них не затерялись другие.
                        –1
                        >Например, последние версии Visual C++ ругаются на printf()

                        И правильно делают. См. хотя бы «Защищённый код».
                          –1
                          Всё не так очевидно — я не спорю с тем, что в новом проекте printf() незачем. Но если код отлично работает, я не хочу, чтобы в нём появлялись новые ворнинги. Мне не нужно ломать отлаженную программу лишь из-за того, что компилятор стал более параноидальным.

                          См. рекомендацию мне выше — «отключить ворнинги вовсе». Вот так оно и работает — приходится отключать вовсе, и смысл их пропадает…
                            0
                            Вы отключаете только предупреждения новой CRT. Остальные то остаются. Ну а предупреждения новой CRT полезны в новых проектах, когда нет тонны унаследованного кода, который никто не будет переписывать, когда есть возможность написать с нуля и правильно.
                              +2
                              Тоже верно, и всё равно неоднозначно. Я это к тому говорю, что хорошего решения толком нет.
                              Да, допустим, компилятор беспокоит секьюрность. Но что он мне предлагает? printf_s(), scanf_s() и прочее — насколько мне известно, функции не из состава ANSI C++, т.е. он предлагает сломать кроссплатформенность.

                              А старые добрые функции иногда наиболее просты в использовании, бывает и такое… Я бы не сказал, что использование printf() «неправильно».

                              Если взять практически любой учебник по С++, там написано, что препроцессор — это неправильно. А теперь открываем буст, и видим, что половина его построена на препроцессоре (Boost.Preprocessor, Boost.Foreach и пр.)
                              –1
                              >Вот так оно и работает — приходится отключать вовсе

                              Ни в коем случае! Тогда уж лучше используй «#pragma warning (suppress: номер)» для однократного подавления предупреждений в одной строчке или «disable» в обрамлении «push» и «pop» (http://msdn.microsoft.com/en-us/library/2c8f766e(loband).aspx).

                              >Мне не нужно ломать отлаженную программу лишь из-за того, что компилятор стал более параноидальным.

                              Сам не сломаешь — кто-то другой сломает. Отлаженная — это как? Ты поручишься, что обошёл абсолютно все уязвимости printf'а?
                                0
                                > Ты поручишься, что обошёл абсолютно все уязвимости printf'а?

                                Абсолютно поручусь. У меня есть код, в котором с помощью sprintf() переводят целое 32-разрядное число в шестнадцатеричную запись. Вот как-то слабо верится, что здесь может случиться переполнение чего бы то ни было :) Все размеры заранее известны.
                                  0
                                  >Все размеры заранее известны.

                                  А почему тогда не использовать более безопасные аналоги sprintf'а с указанием этих размеров? Компилятор ругаться перестанет.

                                  А то и вовсе можно использовать стандартные строковые стримы, Boost.Format, Boost.LexicalCast.
                                    0
                                    Насколько мне известно, эти аналоги не кросплатформенны.

                                    Стримы и прочее — верно, активно пользуюсь. Но, положа руку на сердце, нет более простого и эффективного кода перевода числа в шестнадцатеричную строку, чем две строки: объявление буфера и вызов sprintf(). В остальном согласен :)
                      0
                      Чтобы быть уверенным, что не пропустил ни одного ворнинга, нужно включать treat warnings as errors.
                        +1
                        Читаю и исправляю обязательно, не раз это меня спасало. «Божиться» не умею.
                      +1
                      -Wall -W -Werror!
                      (hint: -Wall не все включает)
                    +2
                    «Убеждать» в таких вещах надо не простенькими примерчиками, которые показываю только то, что конкретный компилятор работает именно так, а ссылками на конкретный раздел стандарта, описывающий эту ситуацию.
                      +1
                      ISO/IEC 14882:1998 12.6.2 [class.base.init] #5 стр 197.
                      0
                      Стандарт стандартом, а убеждать надо как раз таки примерчиками. Я с таким успехом могу посылать всех читать стандарт по любому нюансу С++, вместо того чтобы писать статью.
                      +3
                      ИМХО, даже в «исправленном» виде этот код плох, ибо потенциально порождает баги!!!
                      Ибо код пишет один человек, интегрировать его может другой, а дорабатывать (через пару месяцев или лет) — третий…
                      Я-бы на код-ревю такой код «зарубил». А на второй-третий раз задумался о замене такого программиста :-(
                      Вот так это должно выглядеть в нашей «лавочке», согласно внутрифирменного стандарта:

                      A(int x): b(x), a(х) {}
                        0
                        Ну а если бы было так:
                        A(int x): b(x+1), a(b) {}
                        вы всё равно скажете, что лучше
                        A(int x): b(x+1), a(x+1) {}
                          +2
                          ну я бы сказал что да… либо если уж совсем извраты какие нить, то проще вынести в тело конструктора
                            0
                            Уж лучше так, чем баги плодить!!!
                            А ещё лучше в сложных случаях (а Вы на них намекаете :) инициализацию проводить в теле констрактора или в функции init…
                              0
                              Я полностью согласен с вами, что всегда есть выход лучше. Но чтобы понять, что именно он лучше, хорошо бы знать про возможные грабли, согласитесь ,-)

                              Кроме того, есть два важных аспекта:
                              1) Ваши предложения не всегда прокатят
                              2) Существуют ситуации, когда решение с инициализацией просто само просится. Классика:
                              A(int x): vectorSize(x+1), vector(vectorSize) {}
                              Часто, такое решение возникает эволюционно: сперва наблюдается
                              A(int x): vector(x+1) {}
                              потом оказывается, что хорошо бы хранить размер и рождется
                              A(int x): vectorSize(x+1), vector(vectorSize) {}

                              Так вот. Пост о том, что при этом возможны грабли.
                                0
                                1) когда именно не прокатит перенос инциализации в тело конструктора?
                                2) второй вариант как раз просит выноса vector(vectorSize) в тело конструктора
                                  +2
                                  1) когда нет конструктора без параметров.
                                  вот этот пример не соберётся

                                  #include class S {
                                  public:
                                  S(int x) {}
                                  };

                                  class A {
                                  private:
                                  S a;
                                  public:
                                  A() { a(1); }
                                  };
                                  int main() {
                                  A a();
                                  return 0;
                                  }

                                  а если заменить

                                  A() { a(1); }

                                  на

                                  A(): a(1) {}

                                  то всё будет хорошо.

                                  2) Опять же, vector можно засунуть в тело, только если есть конструктор vector::vector().
                                    –1
                                    чё-то я… неудачный пример написал :-)) но смысл наверно все уловили, раз вопросов нет ,-)
                                      0
                                      да я чот забыл уж про этот топик;)
                                      я бы в таких случаях скорее сделал бы скорее:
                                      class A {
                                      private:
                                      S *a;
                                      public:
                                      A() { a = new S(1); }
                                      };
                                      ~A() {delete a;}
                                      int main() {
                                      A a();
                                      return 0;
                                      }
                                        0
                                        Это совсем другой класс!
                                        Из-за ссылок для него нужно иначе писать конструкторы копирования и операции присваивания… он даже при линковке ведёт себя иначе!
                                        Это просто другой случай, не обсуждаемый здесь и заслуживающий отедльной заметки.
                                  +1
                                  Тут еще есть момент оптимизации. Если инициализировать объект в теле конструктора, то будет происходить изменение уже созданного и инициализированного (возможно, по умолчанию) объекта. Т.е. в некоторых случаях возможна даже двойная нагрузка при создании одного объекта. А конструкция типа A(int x): vectorSize(x+1), vector(vectorSize) {} инициализирует vector сразу при выделении под него память.
                                    –1
                                    по сути это микрооптимизация и нужна только в очень редких случаях (несогласные идут читать Кнута, Александреску и Саттера)…
                                      +1
                                      Герб Саттер, Андрей Александреску. Стандарты программирования на С++. Глава 48: «В конструкторах предпочитайте инициализацию присваиванию»:

                                      В конструкторах использование инициализации вместо присваивания… предохраняет от ненужной работы времени выполнения ...


                                      А инициализация переменных-членов — совсем не редкость. Кроме того:

                                      Эта методика не является преждевременной оптимизацией; это — избежание преждевременной пессимизации


                            +1
                            Благодарствую, крайне познавательный материал. Стандарт большой, наизусть не выучишь. Лично я этого «нюанса» не знал :)
                              +2
                              >Стандарт большой, наизусть не выучишь. Лично я этого «нюанса» не знал :)

                              Стандарта можешь ты не знать, но книжки Саттера чтить обязан.
                              +1
                              Написали бы заодно про базовые
                              class A { public: A(int v) : x(v) {} int x; };
                              class B
                              {
                              public:
                                B(A * p) { p->x = 0; }
                              };

                              class C : public B, public A
                              {
                              public:
                                C() : A(5), B(this)
                                {
                                  std::cout << x; // ?
                                }
                              };
                                0
                                Да я уж вижу, что писать про это можно :-)
                                  0
                                  Если будете писать про вызов конструкторов базовых классов, не забудьте про виртуальное наследование.
                                +2
                                Писал я, было время, пару статеек под «громким» названием «С++ Mythbusters» и других подбивал поддержать тему. Но вместо того чтобы вникнуть в суть идеи таких статей все начали придираться к названию и тыкать в нос «а вот Александреску об этом писал» или «в стандарте это есть» и т.п.
                                Так к чему это я… Ваша статья как раз отлично подходит под это дело ;)

                                Вы её хотя бы в блог С++ перенесите.
                                  0
                                  Перенёс.

                                  А про всяких тыкателей… я вижу два резона на то, что бы не заморачиваться по этому поводу.

                                  1) На мой взгляд, статья не обязана быть интересна всем. Если она окажется интересна 5% читателей — это уже отлично. Ну кому-то не нравится… пусть не читает или напишет так, как нравится ему. В этом, как мне кажется, нет ничего страшного.

                                  2) Нужно учитывать природу минусяторов и относиться к ним снисходительно. Большинство минусяторов ничем особо не интересуется (кроме, пожалуй, вопросов тайм-менеджмента, сводящихся к «чем же наконец заняться-то?»). Так как минусятору очень скучно и нечего делать, он слоняется по хабру и прочим интернетам, но нигде не находит ничего, что его бы потешило. Это бездарное времяпрепровождение делает его нервным, вспыльчивым и социально активным. И тут-то он начинает всех минусовать :-)

                                  Я верю, что существует множество неминусяторов. Они не очень заметны потому, что читают хабр не каждый день и пишут комменты не каждую неделю :-) Но они совершенно вменяемы и им-то как раз были бы интересны статьи, выходящие за рамки темы «как же закрыть аську и делом-то наконец заняться?» :-)

                                  Так к чему это я… не опускайте руки! :-)
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      Да вроде и не было сказано, что читающие стандарты попадают в 95% и что 95% — мудаки.
                                        0
                                        Повторение — мать учения :) Нужно читать и стандарт, и такие статейки.

                                        И, да… Никто не говорил, что эти 95% — мудаки.
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                            0
                                            Погодите! :-)
                                            Нет никакой связи между миносяторами и теми, кому не интересна статья. Первые — это дейстительно… э… закомплексованные люди. Вторые могут быть вполне вменяемыми людьми, просто они не программируют на с++. Эти множества никак не связаны… это как круглое и красное :-)
                                            При этом му[yes]ки — это третий класс, про который тут речи нет вообще.
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                                +4
                                                Всё очень просто. Достаточно сознательные люди и минусы ставят «за дело». Но! Большинство же напротив оценивает статью крайне не объективно, то бишь минусы они ставят с позиции «нравится ли лично мне», а не с позиции «полезна ли эта статья в целом»:
                                                1. Некоторые ставят минусы за то, что им не нравится стиль изложения материала (много либо, наоборот, мало юмора, «неправдоподобные» примеры и т.д.).
                                                2. Другие начинают оффтопить на тему несоответствия названия статьи её содержанию (исходя из их логики) или же вообще на темы типа:
                                                  • «Правильно писать не „в Украине”, а „на Украине”, потому что слово „Украина” произошло от …, и у Розенталя сказано „…”, поэтому … [и понеслась…]»;
                                                  • «О Боже милостивый! У Вас тире не длинное, а кавычки — не „ёлочки”! Это ставит с ног на голову все основы типографики. Да ведь ТотСамиЗнаетеКтоНоИмяКоторогоНельзяНазыватьНаХабреПотомуЧтоТакКручеИВообщеБольшинствоТакДелаетПоэтомуЯБудуПовторятьЗаВсеми упал бы с унитаза, если бы увидел это».
                                                  • Прочий троллинг в том же духе
                                                3. Третьи мнят себя излишне умными и вечно, пардон, «впаривают», что у Саттера, Александреску, Мейерса, Страуструпа, Фрейда, Сиддхартхи Гаутамы, Иешуа, Магистра Йоды или Темного Властелина Саурона это в книгах уже было. Такие люди всегда знают наизусь имена всех оленей Санты и даже Коран целиком — что уж тогда говорить про стандарт С++. Но они никогда не учитывают, что сколько бы книг ни было написано, всегда найдется человек, который ни об этих книгах, ни об авторах не слышал, а для чтения стандарта банально не знает английский язык.
                                                4. К четвертой группе относятся те, кто живет с девизом «Я прогаю на самом лучшем, самом мегапопулярном, самом продуманном, мощном, простом для восприятия, используемом еще более 9000 таких же, как я, языке PHP (Python, СиШлак, Ruby, Javascript etc. — нужное подставить), а С++ пережиток прошлого, ни одного нового проекта на нем уже нету, кроме того для его понимания нужно знать, что в компьютере есть память, и (О боги мира! Спасите меня!!!) её нужно ВРУЧНУЮ выделять и очищать, а еще в универе С++ вёл дедуля, полковник в отставке, а PHP — симпатичная девчушка, выпустившаяся на 2 года раньше, а ныне купившаяся на улыбку ради зачета. Поэтому, чтобы этот беспредел не продолжался, чтобы новые, подрастающие поколения не дай бог не выбрали «бородатый» сишный путь, я выполню свою священную миссию и влеплю минус дабы сбить рейтинг!»
                                                5. К последей группе относятся те, кто следует за большинством и ставит минус вслед за предыдущими четырьмя. Это самые унылые люди в следствие своей несознательности.


                                                Надеюсь, ничего не забыл ;)
                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                      0
                                                      Про «память» — это я утрированно ;) И в целом не бывает хороших и плохих языков.

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

                                                      А по поводу «близко к сердцу» ты вообще погорячился :) Это всего лишь мой вариант троллинга.
                                                      • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      Именно, а про такие вещи даже у Шилдта написано, но для новичков статья — очень даже ничего. Вот только она не полная, ибо есть еще туча примеров про инициализацию полей класса, список инициализации, список инициализации vs инициализация в теле конструктора и т.д., которые можно было раскрыть в этой статье.
                                        +1
                                        Помнится мне, про Шилдта Вы довольно негативно отзывались. А сами, видно, тоже через него прошли — и ничего ;)

                                        Я думаю, целью данной статьи не было полное описание способов инициализации — не учебник ведь в конце концов. Автор обратил внимание на маленький и не совсем очевидный нюанс языка, а человек мыслящий по прочтении данной статьи и сам сможет додумать, что из этого выплывает (или в крайнем случае найдет, где прочитать подробнее).
                                        +2
                                        Зря Вы перестали. Статьи были интересные и подтолкнули меня к изучению c++ :)
                                          0
                                          Да я не перестал. У меня был творческий перерыв ;)))
                                        0
                                        побочный эффект, и что?
                                          0
                                          >То вы обманываетесь
                                          Может всетаки ошибаетесь :)
                                            +2
                                            обманываетесь поэтичнее :-)
                                            0
                                            Знать всё это, вне всяких сомнений, полезно и важно. Писать код, в котором имеют место такие извороты — ни в коем случае.
                                              +1
                                              У меня за 10 лет было 3-4 случая, когда такая инициализация была необходима, были на то причины. Не стоит зарекаться.
                                                –1
                                                А у меня за те же 10 лет не было ни одного да и представить себе с трудом могу, почему они могли бы быть:
                                                1) Что мешает инициализировать переменные независимо друг от друга тем самым входным параметром конструктора?
                                                2) Что мешает вынести переменные, которые должны инициализироваться раньше в базовый класс и явно вызывать его конструкторы из конструкторов наследников?
                                                3) Что мешает инициализировать первоочередные переменные в конструкторе, а те, которые должны просчитываться позже — в какой-нибудь процедуре Initialize()

                                                — придумано за 15 секунд. Минут за 10, думаю, еще способа 3-4 избежать такого неадеквата можно найти легко.
                                              –4
                                              > Что это? Баг в С++?

                                              Нет, очередная неоднозначность и запутанность, за что этот язык многие и не любят.
                                                +1
                                                Неоднозначность — это unspecified или undefined behaviour. А тут как раз всё однозначно, порядок вызова конструкторов всегда определён.
                                                  0
                                                  Уверен, в PHP, к примеру, тоже хватает своих нюансов.
                                                    –3
                                                    Хватает. Но в Си их больше. там слишком много свободы дается программисту, можно писать всякие извраты, вроде a+++++b или директив препроцессора define, а это плохо, является источником потенциальных ошибок. Особенно все плохо в Си++: итераторы, шаблоны, мозг можно сломать. Как то слишком уж абстрактно все.

                                                    Язык (синтаксис), я считаю, должен быть как можно проще и недвусмысленней, чтобы с 1 взгляда было понятно что делает код, а не как тот же Перл где типичная команда выгллядит как $_~=s/\$\\'/'/g ну или типа того, и хрен что поймешь :(

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

                                                    При всем уважении к возможностям Си/Си++, после работы с более высокоуровненвыми я.п., врозвращаться к ручному выделению памяти (или извратно прикрученному сборщику мусора, что пожалуй хуже), не хочется.

                                                    Ах да, еще Си просто устарел — там до сих пор есть дурацкие требования определять функцию ДО ее использования, и сложная система #include. Хотя вроде есть язык D, призванный исправить подобные недостатки))

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                Самое читаемое