Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате |
Форум программистов > C/C++: Для новичков > Не зная брода, не лезь в воду. Часть первая. (Си++) |
Автор: Thunderbolt 30.1.2012, 19:12 | ||||||||||||||||
Захотелось написать несколько небольших заметок о том, как программисты на Си/Си++ играют с огнём, не подозревая об этом. Первая заметка будет про попытки явно вызвать конструктор. Программисты - ленивые существа. Поэтому норовят решить задачу минимальным количеством кода. Это похвальное и хорошее стремление. Главное не увлечься процессом и вовремя остановиться. Например, программистам бывает лень создавать единую функцию инициализации в классе, чтобы затем вызывать её из разных конструкторов. Программист думает: "Зачем мне лишняя функция? Я лучше вызову один конструктор из другого". К сожалению, даже эту простую задачу программисту удается решить не всегда. Для выявлений таких неудачных попыток я как раз сейчас реализовываю в http://www.viva64.com/ru/pvs-studio/ новое правило. Вот, пример кода, который я обнаружил в проекте eMule:
Рассмотрим внимательнее реализацию последнего конструктора. Программист решил, что код
просто вызывает другой констурктор. Ничего подобного. Здесь создается и тут же уничтожается новый неименованный объект типа CSlideBarGroup. Получается, что программист действительно вызвал другой конструктор. Вот только сделал он совсем не то, что задумал. Поля класса останутся неинициализированными. Такие ошибки, это только половина беды. Некоторые знают, как все-таки действительно вызвать другой конструктор. И вызывают. Лучше бы они не знали, как это делается. ![]() Например, приведенный код, можно было бы переписать так:
или так:
Теперь действительно один конструктор для инициализации данных вызывает другой конструктор. Если увидите, программиста, который так делает, отвесьте ему один шелбан в лоб от себя и один от меня лично. Приведенные примеры являются очень опасным кодом, и нужно хорошо понимать, как они работают! Из-за мелочной оптимизации (лень писать отдельную функцию), этот код может нанести больше вреда, чем пользы. Рассмотрим подробнее, почему иногда подобные конструкции работают, но чаще нет.
Этот код будет корректно работать. Код безопасен и работает, так как класс содержит простые типы данных и не наследуется от других классов. В этом случае двойной вызов конструктора ничем не грозит. Рассмотрим другой код, где явный вызов конструктора приводит к ошибке (пример взят из http://www.viva64.com/go.php?url=790 на сайте StackOverflow):
Когда мы вызываем конструктор "new (this) Derived(bar.foo);", объект Base уже создан и поля инициализированы. Повторный вызов конструктора приведет к двойной инициализации. В 'ptr' запишем указатель на вновь выделенный участок памяти. В результате получаем утечку памяти. К чему приведет двойная инициализация объекта типа std::vector, вообще предсказать сложно. Ясно одно. Такой код недопустим. Вывод Явный вызов конструктора требуется только в крайне редких случаях. В обычном программировании, явный вызов конструктора, как правило, появляется из-за желания сокращения размера кода. Не надо этого делать! Создайте обыкновенную функцию инициализации. Вот как должен выглядеть правильный код:
P.S. Явный вызов одного конструктора из другого в C++11 (делегация) Новый стандарт С++11 позволяет вызывать одни конструкторы класса из других (так называемая делегация). Это позволяет писать конструкторы, использующие поведение других конструкторов без внесения дублирующего кода. Пример корректного кода:
|
Автор: boostcoder 30.1.2012, 20:11 | ||||
Thunderbolt, спасибо огромное за очередную статью ![]() по поводу делегирующих конструкторов, есть такой, не очевидный момент о котором многие и не подозревают:
в приведеном коде, деструктор будет вызван в любом случае.
Добавлено через 7 минут и 42 секунды зы на самом деле, никогда не возникало мысли, из конструктора вызвать конструктор ![]() |
Автор: boostcoder 30.1.2012, 22:23 |
два вопроса. 1. разве обязательно? 2. что происходит с деструктором если в конструкторе выбрасывается исключение? |
Автор: disputant 30.1.2012, 22:31 | ||
А вот с чисто теоретической точки зрения - внутри конструктора формально объект еще не создан, так имеем ли мы формальное право вызывать в нем функцию-член?... |
Автор: boostcoder 30.1.2012, 23:03 |
объект создан. а инициализирован ли он - это уже другой вопрос. |
Автор: disputant 30.1.2012, 23:30 | ||
Лень на ночь глядя рыться, но то ли у Страуструпа, то ли у Саттера этот вопрос поднимался и говорилось, насколько я помню, что формально так поступать нельзя, другое дело, что неформально любой компилятор это пропускает ![]() Выделена память - это да, базовый объект создан - да, но объект текущего типа - нет. Кажется, так. |
Автор: newbee 30.1.2012, 23:35 | ||
|
Автор: volatile 31.1.2012, 00:09 |
Вопрос действительно актуальный, я пару раз обжегся с такими вызовами. С тех пор всегда создаю init (..); и вызываю его из конструкторов. Даже если один конструктор. ![]() обжегшись на молоке, дуешь на воду. |
Автор: boostcoder 31.1.2012, 02:18 | ||
именно так. все еще надеюсь, bsa пояснит свой ответ. а то жутко интересно. |
Автор: Result 31.1.2012, 06:39 | ||||
Не поделишься ли источником информации ? К примеру в одной книжке написано
В примере Бусткодера один конструктор полностью отрабатывает, поэтому, как мне видится, объект и считается полностью сконструированным. И дело не в списке инициализации. И в конструкторе также может быть просто выделение памяти, скажем размещающим оператором new, а не только инициализация. |
Автор: Thunderbolt 31.1.2012, 08:50 |
По поводу делегирующих конструкторов: Следует заметить, что если в C++03 объект считается до конца созданным когда его конструктор завершает выполнение, то в C++11 после выполнения хотя бы одного делегирующего конструктора остальные конструкторы будут работать уже над полностью сконструированным объектом. Несмотря на это объекты производного класса начнут конструироваться только после выполнения всех конструкторов базовых классов. Взято из http://ru.wikipedia.org/wiki/C%2B%2B11#.D0.A3.D0.BB.D1.83.D1.87.D1.88.D0.B5.D0.BD.D0.B8.D0.B5_.D0.BA.D0.BE.D0.BD.D1.81.D1.82.D1.80.D1.83.D0.BA.D1.82.D0.BE.D1.80.D0.BE.D0.B2_.D0.BE.D0.B1.D1.8A.D0.B5.D0.BA.D1.82.D0.BE.D0.B2. |
Автор: bsa 31.1.2012, 10:21 |
boostcoder, за меня уже полностью отдулись Result и Thunderbolt. Именно это я и имел в виду. |
Автор: boostcoder 31.1.2012, 11:36 |
bsa, необычность в том, что делегирующий конструктор - это тоже конструктор, а не функция init Добавлено через 4 минуты и 44 секунды не помню откуда знаю это. но это факт. и тело конструктора к этому факту никак не относится. докажи обратное. |
Автор: newbee 31.1.2012, 11:45 | ||
Смотри, до finally происходит инициализация всех членов объекта. После этого в конструкторе происходит настройка объекта. По стандарту все это вместе называется инициализацией объекта. Тут я обожглась на терминологии. Выше был вопрос "можно ли вызывать методы внутри конструктора, ведь он еще не создан" - я отвечала на него, именно в этом контексте были мои слова о честной инициализации: в конструкторе рамки объекта уже четко определены, члены инициализированы, значит вызывать обычные методы (читай функции) можно. НО. Из-за наркоманского дизайна языка можно напороться на несколько косяков, связанным как раз с тем, что объект еще недосоздан. Во-первых, выброшенное из конструктора или где-нибудь глубже по стеку вызова исключение приведет к тому, что деструктор объекта вызван не будет -> нужно несколько раз подумать прежде чем выделять внутри конструктора память, открывать файловые дескрипторы и т.д. и , самая пичалька, при необходимости самому бросить исключение из конструктора придется дублировать код деструктора или выносить его в отдельную функцию. Во-вторых, вызов виртуальной функции из конструктора обернется вызовом функции этого класса, а не перекрытого. По этим причинам часто имеет место ручная двухуровневая инициализация объекта: сначала создают объект, потом вызывают в нем метод init. В частности из-за этого я считаю объектную систему С++ кривой как моя жизнь и предпочитаю процедурно-функциональный стиль, если приходится на нем писать. |
Автор: bems 31.1.2012, 12:20 | ||
|
Автор: mes 31.1.2012, 13:37 | ||
см. RAII понятие объект шире, чем совокупность его членов.. Поэтому инициализация всех членов не является достаточным условием полностью сконструированного объекта.. |
Автор: newbee 31.1.2012, 19:20 |
И в этом все С++ники, подобрали решение и делают вид, что проблемы не существует. |
Автор: bsa 1.2.2012, 10:06 |
Это компромисс между надежностью и быстродействием. |
Автор: mes 2.2.2012, 11:41 | ||
касательного данного случая проблема в том, что в С++ можно работать с голыми указателями ? ведь проблема не в выделении ресурсов, а именно в наготе последних.. В С проблемы нет, потому что все так и так возложено на программиста (как на ломовую лошадь).. а раз используешь автоматизацию С++, так следуй ей до конца.. |