Модераторы: bsa

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Не зная брода, не лезь в воду. Часть первая. (Си++) 
:(
    Опции темы
Thunderbolt
Дата 30.1.2012, 19:12 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 6
Всего: 16



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

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

Например, программистам бывает лень создавать единую функцию инициализации в классе, чтобы затем вызывать её из разных конструкторов. Программист думает: "Зачем мне лишняя функция? Я лучше вызову один конструктор из другого". К сожалению, даже эту простую задачу программисту удается решить не всегда. Для выявлений таких неудачных попыток я как раз сейчас реализовываю в 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) {}
};


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

Рассмотрим другой код, где явный вызов конструктора приводит к ошибке (пример взят из дискуссии на сайте 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") {}
};


--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
boostcoder
Дата 30.1.2012, 20:11 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



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 
PM WWW   Вверх
bsa
Дата 30.1.2012, 21:47 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Модератор
Сообщений: 9185
Регистрация: 6.4.2006
Где: Москва, Россия

Репутация: 85
Всего: 196



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

Как раз этот момент абсолютно очевиден - любой конструктор класса полностью инициализирует объект и вызов для него деструктора необходим. Это свойство значительно увеличивает безопасность исключений кода по сравнению с общей функцией инициализации.
PM   Вверх
boostcoder
Дата 30.1.2012, 22:23 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



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

два вопроса.
1. разве обязательно?
2. что происходит с деструктором если в конструкторе выбрасывается исключение?
PM WWW   Вверх
disputant
  Дата 30.1.2012, 22:31 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Бывалый
*


Профиль
Группа: Участник
Сообщений: 210
Регистрация: 28.11.2011

Репутация: нет
Всего: 3



Код

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


А вот с чисто теоретической точки зрения - внутри конструктора формально объект еще не создан, так имеем ли мы формальное право вызывать в нем функцию-член?...
PM MAIL   Вверх
boostcoder
Дата 30.1.2012, 23:03 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



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

объект создан. а инициализирован ли он - это уже другой вопрос.
PM WWW   Вверх
disputant
Дата 30.1.2012, 23:30 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Бывалый
*


Профиль
Группа: Участник
Сообщений: 210
Регистрация: 28.11.2011

Репутация: нет
Всего: 3



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

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

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

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

Это сообщение отредактировал(а) disputant - 30.1.2012, 23:31
PM MAIL   Вверх
newbee
Дата 30.1.2012, 23:35 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Бревно
**


Профиль
Группа: Участник
Сообщений: 703
Регистрация: 24.8.2011

Репутация: 3
Всего: 19



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



--------------------
You're face to face
With man who sold the world
PM   Вверх
volatile
Дата 31.1.2012, 00:09 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Завсегдатай
Сообщений: 2107
Регистрация: 7.1.2011

Репутация: 16
Всего: 85



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


PM MAIL   Вверх
boostcoder
Дата 31.1.2012, 02:18 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



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

именно так.

все еще надеюсь, bsa пояснит свой ответ. а то жутко интересно.
PM WWW   Вверх
Result
Дата 31.1.2012, 06:39 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 52
Регистрация: 15.5.2011

Репутация: 3
Всего: 5



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

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

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

В примере Бусткодера один конструктор полностью отрабатывает, поэтому, как мне видится, объект и считается полностью 
сконструированным. И дело не в списке инициализации. И в конструкторе также может быть просто выделение памяти, скажем размещающим оператором new, а не только инициализация.
PM   Вверх
Thunderbolt
Дата 31.1.2012, 08:50 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 6
Всего: 16



По поводу делегирующих конструкторов:

Следует заметить, что если в C++03 объект считается до конца созданным когда его конструктор завершает выполнение, то в C++11 после выполнения хотя бы одного делегирующего конструктора остальные конструкторы будут работать уже над полностью сконструированным объектом. Несмотря на это объекты производного класса начнут конструироваться только после выполнения всех конструкторов базовых классов.

Взято из Wikipedia. Улучшение конструкторов объектов..
--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
bsa
Дата 31.1.2012, 10:21 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Модератор
Сообщений: 9185
Регистрация: 6.4.2006
Где: Москва, Россия

Репутация: 85
Всего: 196



boostcoder, за меня уже полностью отдулись Result и Thunderbolt. Именно это я и имел в виду.
PM   Вверх
boostcoder
Дата 31.1.2012, 11:36 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


pattern`щик
****


Профиль
Группа: Завсегдатай
Сообщений: 5458
Регистрация: 1.4.2010

Репутация: 20
Всего: 110



bsa, необычность в том, что делегирующий конструктор - это тоже конструктор, а не функция init

Добавлено через 4 минуты и 44 секунды
Цитата(Result @  31.1.2012,  06:39 Найти цитируемый пост)
поделишься  ли источником информации ?

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


PM WWW   Вверх
newbee
Дата 31.1.2012, 11:45 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Бревно
**


Профиль
Группа: Участник
Сообщений: 703
Регистрация: 24.8.2011

Репутация: 3
Всего: 19



Цитата(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.

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


--------------------
You're face to face
With man who sold the world
PM   Вверх
bems
Дата 31.1.2012, 12:20 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 3400
Регистрация: 5.1.2006

Репутация: нет
Всего: 88



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

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



--------------------
Обижено школьников: 8
PM MAIL   Вверх
mes
Дата 31.1.2012, 13:37 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


любитель
****


Профиль
Группа: Участник Клуба
Сообщений: 7954
Регистрация: 14.1.2006

Репутация: 79
Всего: 250



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

см. RAII

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

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



--------------------
PM MAIL WWW   Вверх
newbee
Дата 31.1.2012, 19:20 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Бревно
**


Профиль
Группа: Участник
Сообщений: 703
Регистрация: 24.8.2011

Репутация: 3
Всего: 19



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


--------------------
You're face to face
With man who sold the world
PM   Вверх
bsa
Дата 1.2.2012, 10:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Модератор
Сообщений: 9185
Регистрация: 6.4.2006
Где: Москва, Россия

Репутация: 85
Всего: 196



Это компромисс между надежностью и быстродействием.
PM   Вверх
mes
Дата 2.2.2012, 11:41 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


любитель
****


Профиль
Группа: Участник Клуба
Сообщений: 7954
Регистрация: 14.1.2006

Репутация: 79
Всего: 250



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

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





Это сообщение отредактировал(а) mes - 2.2.2012, 11:42


--------------------
PM MAIL WWW   Вверх
Страницы: (2) [Все] 1 2 
Ответ в темуСоздание новой темы Создание опроса
Правила форума "C/C++: Для новичков"
JackYF
bsa

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, JackYF, bsa.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Для новичков | Следующая тема »


 




[ Время генерации скрипта: 0.1148 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.