Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Для новичков > Не зная брода, не лезь в воду. Часть первая. (Си++)


Автор: Thunderbolt 30.1.2012, 19:12
Захотелось написать несколько небольших заметок о том, как программисты на Си/Си++ играют с огнём, не подозревая об этом. Первая заметка будет про попытки явно вызвать конструктор.

Программисты - ленивые существа. Поэтому норовят решить задачу минимальным количеством кода. Это похвальное и хорошее стремление. Главное не увлечься процессом и вовремя остановиться.

Например, программистам бывает лень создавать единую функцию инициализации в классе, чтобы затем вызывать её из разных конструкторов. Программист думает: "Зачем мне лишняя функция? Я лучше вызову один конструктор из другого". К сожалению, даже эту простую задачу программисту удается решить не всегда. Для выявлений таких неудачных попыток я как раз сейчас реализовываю в http://www.viva64.com/ru/pvs-studio/ новое правило. Вот, пример кода, который  я обнаружил в проекте eMule:

Код
class CSlideBarGroup
{
public:
  CSlideBarGroup(CString strName,
    INT iIconIndex, CListBoxST* pListBox);
  CSlideBarGroup(CSlideBarGroup& Group);
  ...
}

CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
  CSlideBarGroup(
    Group.GetName(), Group.GetIconIndex(), Group.GetListBox());
}


Рассмотрим внимательнее реализацию последнего конструктора. Программист решил, что код

Код
CSlideBarGroup(
  Group.GetName(), Group.GetIconIndex(), Group.GetListBox());


просто вызывает другой констурктор. Ничего подобного. Здесь создается и тут же уничтожается новый неименованный объект типа CSlideBarGroup.

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

Такие ошибки, это только половина беды. Некоторые знают, как все-таки действительно вызвать другой конструктор. И вызывают. Лучше бы они не знали, как это делается. smile

Например, приведенный код, можно было бы переписать так:

Код
CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
  this->CSlideBarGroup::CSlideBarGroup(
    Group.GetName(), Group.GetIconIndex(), Group.GetListBox());
}


или так:

Код
CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
  new (this) CSlideBarGroup(
    Group.GetName(), Group.GetIconIndex(),
    Group.GetListBox());
}


Теперь действительно один конструктор для инициализации данных вызывает другой конструктор.

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

Приведенные примеры являются очень опасным кодом, и нужно хорошо понимать, как они работают!

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

Код
class SomeClass
{
  int x,y;
public:
  SomeClass() { new (this) SomeClass(0,0); }
  SomeClass(int xx, int yy) : x(xx), y(yy) {}
};


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

Рассмотрим другой код, где явный вызов конструктора приводит к ошибке (пример взят из http://www.viva64.com/go.php?url=790 на сайте StackOverflow):

Код
class Base 

public: 
 char *ptr; 
 std::vector vect; 
 Base() { ptr = new char[1000]; } 
 ~Base() { delete [] ptr; } 
}; 
 
class Derived : Base 

  Derived(Foo foo) { } 
  Derived(Bar bar) { 
     new (this) Derived(bar.foo); 
  } 
}


Когда мы вызываем конструктор "new (this) Derived(bar.foo);", объект Base уже создан и поля инициализированы. Повторный вызов конструктора приведет к двойной инициализации. В 'ptr' запишем указатель на вновь выделенный участок памяти. В результате получаем утечку памяти. К чему приведет двойная инициализация объекта типа std::vector, вообще предсказать сложно. Ясно одно. Такой код недопустим.

Вывод

Явный вызов конструктора требуется только в крайне редких случаях. В обычном программировании, явный вызов конструктора, как правило, появляется из-за желания сокращения размера кода. Не надо этого делать! Создайте обыкновенную функцию инициализации.

Вот как должен выглядеть правильный код:

Код
class CSlideBarGroup
{
  void Init(CString strName, INT iIconIndex,
            CListBoxST* pListBox);
public:
  CSlideBarGroup(CString strName, INT iIconIndex,
                 CListBoxST* pListBox)
  {
    Init(strName, iIconIndex, pListBox);
  }
  CSlideBarGroup(CSlideBarGroup& Group)
  {
    Init(Group.GetName(), Group.GetIconIndex(),
         Group.GetListBox());
  }
  ...
};


P.S. Явный вызов одного конструктора из другого в C++11 (делегация)

Новый стандарт С++11 позволяет вызывать одни конструкторы класса из других (так называемая делегация). Это позволяет писать конструкторы, использующие поведение других конструкторов без внесения дублирующего кода. Пример корректного кода:

Код
class MyClass {
  std::string m_s;
public:
    MyClass(std::string s) : m_s(s) {}
    MyClass() : MyClass("default") {}
};


Автор: boostcoder 30.1.2012, 20:11
Thunderbolt, спасибо огромное за очередную статью smile 

по поводу делегирующих конструкторов, есть такой, не очевидный момент о котором многие и не подозревают:
Код

#include <iostream>
#include <vector>

struct Bar { 
   int foo;
   
   Bar():Bar(33) {
      std::cout << __PRETTY_FUNCTION__ << std::endl;
      throw 1; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
   }
   Bar(int v):foo(v) {
      std::cout << __PRETTY_FUNCTION__ << std::endl;
   }
   ~Bar() {
      std::cout << __PRETTY_FUNCTION__ << std::endl;
   }
};

int main() {
   try {
      Bar bar;
   } catch (...) {
      std::cout << "catching" << std::endl;
   }
}


в приведеном коде, деструктор будет вызван в любом случае.
Цитата

C:\test>delegat
Bar::Bar(int)
Bar::Bar()
Bar::~Bar()
catching


Добавлено через 7 минут и 42 секунды
зы
на самом деле, никогда не возникало мысли, из конструктора вызвать конструктор smile 

Автор: bsa 30.1.2012, 21:47
Цитата(boostcoder @  30.1.2012,  21:11 Найти цитируемый пост)
по поводу делегирующих конструкторов, есть такой, не очевидный момент о котором многие и не подозревают:

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

Автор: boostcoder 30.1.2012, 22:23
Цитата(bsa @  30.1.2012,  21:47 Найти цитируемый пост)
конструктор класса полностью инициализирует объект

два вопроса.
1. разве обязательно?
2. что происходит с деструктором если в конструкторе выбрасывается исключение?

Автор: disputant 30.1.2012, 22:31
Код

class CSlideBarGroup
{
  void Init(CString strName, INT iIconIndex,
            CListBoxST* pListBox);
public:
  CSlideBarGroup(CString strName, INT iIconIndex,
                 CListBoxST* pListBox)
  {
    Init(strName, iIconIndex, pListBox);
  }


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

Автор: boostcoder 30.1.2012, 23:03
Цитата(disputant @  30.1.2012,  22:31 Найти цитируемый пост)
объект еще не создан

объект создан. а инициализирован ли он - это уже другой вопрос.

Автор: disputant 30.1.2012, 23:30
Цитата(boostcoder @ 30.1.2012,  23:03)
Цитата(disputant @  30.1.2012,  22:31 Найти цитируемый пост)
объект еще не создан

объект создан. а инициализирован ли он - это уже другой вопрос.

Лень на ночь глядя рыться, но то ли у Страуструпа, то ли у Саттера этот вопрос поднимался и говорилось, насколько я помню, что формально так поступать нельзя, другое дело, что неформально любой компилятор это пропускает smile

Выделена память - это да, базовый объект создан - да, но объект текущего типа - нет. Кажется, так.

Автор: newbee 30.1.2012, 23:35
Цитата(disputant @  31.1.2012,  00:30 Найти цитируемый пост)
Выделена память - это да, базовый объект создан - да, но объект текущего типа - нет. Кажется, так.
После списка инициализации объект должен считаться полностью и честно инициализированным. В конструкторе происходит :after инициализация.

Автор: volatile 31.1.2012, 00:09
Вопрос действительно актуальный, я пару раз обжегся с такими вызовами.
С тех пор всегда создаю  init (..); и вызываю его из конструкторов.
Даже если один конструктор.  smile
обжегшись на молоке, дуешь на воду.


Автор: boostcoder 31.1.2012, 02:18
Цитата(newbee @ 30.1.2012,  23:35)
Цитата(disputant @  31.1.2012,  00:30 Найти цитируемый пост)
Выделена память - это да, базовый объект создан - да, но объект текущего типа - нет. Кажется, так.
После списка инициализации объект должен считаться полностью и честно инициализированным. В конструкторе происходит :after инициализация.

именно так.

все еще надеюсь, bsa пояснит свой ответ. а то жутко интересно.

Автор: Result 31.1.2012, 06:39
Цитата(newbee @ 30.1.2012,  23:35)
После списка инициализации объект должен считаться полностью и честно инициализированным. В конструкторе происходит :after инициализация.

Не поделишься  ли источником информации ? К примеру в одной книжке написано 
Код

В языке C++ удаляются только полностью сконструированные объекты, то есть такие, конструкторы которых уже
завершили выполнение кода.

В примере Бусткодера один конструктор полностью отрабатывает, поэтому, как мне видится, объект и считается полностью 
сконструированным. И дело не в списке инициализации. И в конструкторе также может быть просто выделение памяти, скажем размещающим оператором 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 секунды
Цитата(Result @  31.1.2012,  06:39 Найти цитируемый пост)
поделишься  ли источником информации ?

не помню откуда знаю это. но это факт. и тело конструктора к этому факту никак не относится.
докажи обратное.


Автор: newbee 31.1.2012, 11:45
Цитата(Result @  31.1.2012,  07:39 Найти цитируемый пост)
Не поделишься  ли источником информации ?


Цитата
Initialization shall proceed in the following order:
— First, and only for the constructor of the most derived class as described below, virtual base classes shall be
initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base
classes, where “left-to-right” is the order of appearance of the base class names in the derived class base-specifier-
list.
— Then, direct base classes shall be initialized in declaration order as they appear in the base-specifier-list (regardless
of the order of the mem-initializers).
— Then, non-static data members shall be initialized in the order they were declared in the class definition (again
regardless of the order of the mem-initializers).
— Finally, the body of the constructor is executed.


Смотри, до finally происходит инициализация всех членов объекта. После этого в конструкторе происходит настройка объекта. По стандарту все это вместе называется инициализацией объекта. Тут я обожглась на терминологии. Выше был вопрос "можно ли вызывать методы внутри конструктора, ведь он еще не создан" - я отвечала на него, именно в этом контексте были мои слова о честной инициализации: в конструкторе рамки объекта уже четко определены, члены инициализированы, значит вызывать обычные методы (читай функции) можно. НО. Из-за наркоманского дизайна языка можно напороться на несколько косяков, связанным как раз с тем, что объект еще недосоздан. Во-первых, выброшенное из конструктора или где-нибудь глубже по стеку вызова исключение приведет к тому, что деструктор объекта вызван не будет -> нужно несколько раз подумать прежде чем выделять внутри конструктора память, открывать файловые дескрипторы и т.д. и , самая пичалька, при необходимости самому бросить исключение из конструктора придется дублировать код деструктора или выносить его в отдельную функцию. Во-вторых, вызов виртуальной функции из конструктора обернется вызовом функции этого класса, а не перекрытого. По этим причинам часто имеет место ручная двухуровневая инициализация объекта: сначала создают объект, потом вызывают в нем метод init.

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

Автор: bems 31.1.2012, 12:20
Цитата(Thunderbolt @  31.1.2012,  08:50 Найти цитируемый пост)
Следует заметить, что если в C++03 объект считается до конца созданным когда его конструктор завершает выполнение, то в C++11 после выполнения хотя бы одного делегирующего конструктора остальные конструкторы будут работать уже над полностью сконструированным объектом. Несмотря на это объекты производного класса начнут конструироваться только после выполнения всех конструкторов базовых классов.

Взято из Wikipedia. Улучшение конструкторов объектов.. 
шикарно улучшили

Автор: mes 31.1.2012, 13:37
Цитата(newbee @  31.1.2012,  10:45 Найти цитируемый пост)
нужно несколько раз подумать прежде чем выделять внутри конструктора память, открывать файловые дескрипторы и т.д. и , самая пичалька, при необходимости самому бросить исключение из конструктора придется дублировать код деструктора или выносить его в отдельную функцию.

см. RAII

Цитата(newbee @  31.1.2012,  10:45 Найти цитируемый пост)
Тут я обожглась на терминологии

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

Автор: newbee 31.1.2012, 19:20
Цитата(mes @  31.1.2012,  14:37 Найти цитируемый пост)
см. RAII
И в этом все С++ники, подобрали решение и делают вид, что проблемы не существует.

Автор: bsa 1.2.2012, 10:06
Это компромисс между надежностью и быстродействием.

Автор: mes 2.2.2012, 11:41
Цитата(newbee @  31.1.2012,  18:20 Найти цитируемый пост)
И в этом все С++ники, подобрали решение и делают вид, что проблемы не существует. 

касательного данного случая проблема в том, что в С++ можно работать с голыми указателями ? ведь проблема не в выделении ресурсов, а именно в наготе последних.. В С проблемы нет, потому что все так и так возложено на программиста (как на ломовую лошадь).. а раз используешь автоматизацию С++, так следуй ей до конца..




Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)