Урок информатики для мелкософта (пояснение к залепе №8)

Давайте поговорим о С++ и его синтаксисе. В частности о "сокращенной" записи некоторых операций. Слово "сокращенной" я взял в кавычки неспроста. Дело в том, что это сделано не только (и не столько) для того чтобы избавить пользователя от написания длинных строк. Хотя, учитывая майкрософтовскую маниакальную тягу к длинным идентификатором, эти сокращенные формы действительно дают прирост производительности программиста. :)

Ладно, шутки в сторону. Давайте о серьезном.

Язык С (да и его потомок - С++) спроектированы таким образом, чтобы (при использовании НОРМАЛЬНОГО компилятора) программист мог еще на этапе разработки программы управлять полученным машинным кодом. Удивлены? Я тоже не сразу поверил.

Язык С разрабатывался как альтернатива ассемблеру (по этому поводу заранее попрошу не флудить в комментах), т.к. серьезные программы писать на асме в то время стало уже слишком сложно. Другие языки высокого уровня генерировали слишком медленный и прожорливый машинный код и не позволяли программисту управлять этой самой генерацией. Выходило, что чтобы получить оптимальное по скорости и памяти решение (например драйвер устройства или часть ядра ОС) необходимо использовать только ассемблер, напороть ошибок в котором проще простого.

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

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

Именно исходя из этих требований, в языке С есть, например:

  • команды инкремента/декремента (а++, а--), как аналоги соответствующих команд ЦП;
  • возможность указать, что переменная должна храниться в регистре процессора, а не в памяти;
  • работа с указателями;
  • возможность управления размещением переменных и полей структур в памяти;
  • команды выделения/освобождения памяти;
  • команды поразрядных сдвигов;
  • типы, сходные с аппаратными типами (поддерживаемыми процессорами);
  • типы unsigned;
  • возможность использования любого простого типа в качестве логического (bool);
  • отсутствие встроенного строкового типа (т.к. такой тип не поддерживался ни одной платформой);
  • и т.д и т.п.

Я слышу, как самые нетерпеливые читатели уже кричат, что многие языки поддерживают все это. Верно, поддерживают! Но это сейчас. А в то время таких возможностей не было ни в одном языке высокого уровня. Возьмите документацию по FORTRAN, COBOL, ALGOL...

Короче, С разрабатывался как полноценная, достаточно высокоуровневая (!) замена ассемблеру. Точка. Естественно и логично, что С++ унаследовал все эти возможности плюс добавил к ним ПОЛНОЦЕННУЮ поддержку ООП. Далее речь пойдет именно о С++.

Экскурс в историю окончен. Пришло, наконец, время объяснить, в чем же различия между полной и сокращенной записью команд и почему одно вовсе не соответствует второму.

Итак, рассмотрим механизм работы выражения c = a + b (естественно, что все три переменные имеют один и тот же тип и для данного типа определена операция сложения). Значение переменной a складывается с значением переменной b и результат заносится в переменную c. Вроде бы все просто. Но эта простота существует только до тех пор, пока наши переменные имеют простой тип. Давайте заменим их объектами и посмотрим, что получится.

А получится следующее:

  1. для объекта a будет вызван метод T operator+(const T&) const, которому в качестве параметра будет "скормлен" объект b;
  2. этот метод что-то сделает (например прибавит одно к другому) и создаст временный объект типа Т для хранения результата;
  3. затем метод вернет ссылку на созданный временный объект (назовем его "объектом D");
  4. будет вызван метод объекта с: T& operator=(const T&), т.е. ни что иное, как оператор присваивания;
  5. этот метод и приведет объект с в соответствие с состоянием объекта D, переданного в качестве аргумента.
  6. Временный объект D будет уничтожен.

Вот именно так работает вся эта, казалось бы простая, кухня. На что нам нужно обратить внимание в первую очередь?

Первоочередную важность тут имеют не вызовы методов и не махинации с объектами, а тот факт, что наше простейшее и казалось бы безобидное выражение c = a + b приводит к автоматическому (скрытому от наших глаз) созданию и уничтожению объекта со всеми вытекающими отсюда накладками!

Может показаться, что создание и уничтожение временного объекта-пустышки хоть и связано с лишней тратой системных ресурсов, но все же не так уж и страшно, особенно при теперешних-то мощностях. Но тут я хочу напомнить, что создание объекта - это не только выделение памяти под сам объект, но и создание и инициализация всех его членов-полей (которые, в свою очередь, тоже могут быть объектами), а также выполнение конструктора этого объекта и всех конструкторов всех его предков. Соответственно уничтожение объекта - это вызов деструкторов объекта и всех деструкторов всех его предков плюс уничтожение всех его членов-полей, которые тоже могут быть объектами. Получается не так уж и мало.

Ну, а если этот объект представляет собой, например, таблицу из базы данных, который, вдобавок ко всему сказанному, при инициализации будет устанавливать соединение с этой самой БД и загрузку данных из нее, отъедая не только память и процессорное время, но еще и канал связи? Подумайте, такими ли скромными могут оказаться на практике эти "накладные расходы".

Ладно, давайте перейдем к "сокращенной" записи описанной выше операции и подумаем вот над чем: теоретически методы operator+() и operator+=() должны делать одно и тоже, различие лишь в том, куда направится результат. Получается, что метод operator+=() можно "вывести" из метода operator+() (как и сделано в C#), но зачем же тогда создатели языка С++ сделали возможность перегрузки обоих методов для каждой операции? Только лишь для того, чтобы программист мог написать различные варианты, оптимизированные для обоих случаев? Это вряд ли.

Уверен, что дальше можно уже ничего не говорить, т.к. настоящие кодеры уже поняли к чему весь этот длинный пост и, вставляя трясущейся рукой сигарету в рот, судорожно прокручивают в мозгу все ужасные последствия применения этого "выведения сокращенного оператора из полного" по схеме "а'ля Шарп". Но все же я разжую ситуацию, дабы и не сильно понятливые осознали всю дикость такого "выведения" и поняли, что при словах "Шарп" и "Майкрософт" рукам есть отчего затрястись.

Итак, по образцу и подобию предыдущих абзацев, разложу все по полочкам. Берем выражение a += b и препарируем его:

  1. для объекта a будет вызван метод T& operator+=(const T&), которому в качестве параметра будет "скормлен" объект b;
  2. этот метод что-то сделает с объектом а (изменит его состояние ) и вернет ссылку на объект а.

Всё! Никаких созданий лишних объектов, никаких удалений, никаких присваиваний, ни-че-го! Надеюсь теперь понятно, почему не стоит в С++ делать такие "заглушки":

T& operator+=(const T& b)
{ return this->operator=(this->operator+(b)); };

или

T& operator+=(const T& b) { return *this = *this + b; };

что одно и тоже.

Разница в потреблении ресурсов, конечно, впечатляющая, но вроде не такая сильная, чтобы заставить руки так сильно колотиться. Наверное я сгустил краски. Что ж, это действительно так... но только для С++. Теперь давайте рассмотрим, к чему приведет такая же (автоматически генерируемая компилятором) заглушка в шарпе.

Отличие шарпа от С++ (в данном случае) в том, что наши переменные a и b являются на самом деле не "хранителями" объекта, а всего лишь ссылками на него. Вроде бы разницы в результатах быть не должно, но если бы так было на самом деле, то в С++ создатели не вводили бы лишний тип - ссылки. Ладно, смотрим!

Для пояснения ситуации приведу кусок кода из "Залепы №8" (далее все написано на C#):

Пример кода:
MyCls a = new MyCls();
MyCls b = new MyCls();
MyCls c = a; // (1)
a.x = 4; b.x = 6; // (2)
Console.WriteLine("Before: A={0}, B={1}, C={2}", a.x, b.x, c.x);
a += b; // (3)
Console.WriteLine("After: A={0}, B={1}, C={2}", a.x, b.x, c.x);

Итак, по-пунктам:

  • в строке (1) переменная c становится "дублем" переменной a, т.к. теперь ссылается на тот же объект в памяти, на который ссылается a;
  • после инициализации в строке (2), значение c.x так же как и значение a.x становится равным 4 (они же ссылаются на один и тот же объект)
  • после выполнения сгенерированного компилятором оператора operator+=() происходит что-то очень странное - связь переменных a и c внезапно разрывается, ибо теперь они указывают на совершенно разные объекты! Это подтверждается вторым выводом значений переменных. Разве этого мы добивались командой сложения? Разве наше сложение не должно просто изменить объект, на который ссылается переменная a и всё???

А происходит такое именно из-за созданной компилятором описанной выше заглушки. Получается, что оператор сложения создал временный объект с результатом (помните "объект D"? ), ссылка на который и записалась в нашу переменную a. А переменная c об этом создании до сих пор ничего не знает и (что самое страшное) НИКОГДА не узнает, ибо для исправления этого "улучшительно-упрощающего" косяка никаких средств в языке нет.

Вот так и будет у нас болтаться в памяти фантом (на который ссылается переменная c), который должен быть давным-давно уничтожен, а правильнее - просто откорректирован еще в методе operator+=(), создание которого майкрософт возложило на компилятор и (внимание!) запретило программистам. Поэтому, если вам захочется самостоятельно перегрузить метод operator+=() или его аналоги для других операций, то просто попейте воды, сделайте несколько глубоких вдохов и расслабьтесь - вам перегрузить эти методы не удастся.

Ну что, Вам все еще хочется "самой перспективной технологии будущего"? Мне уже нет. :(



Адрес заметки: http://fit-media.com/post_1198760520.html


Если вы не можете отправить комментарий, то прочтите как это исправить здесь

Обязательные для заполнения поля помечены карандашом.


Ваш комментарий к статье:
cod

email при указании не будет опубликован.
Адреса с http:// преобразуются в ссылки автоматически.
Для этого отделяйте их от текста ПРОБЕЛАМИ с обеих концов.
Теги запрещены.

Этот сайт полностью окупает себя, хотя его ТИЦ=10, а PR=2. Хотите знать, как он это делает? Хотите чтобы Ваш сайт чарез пол-часа тоже начал на полном автопилоте приносить деньги?
Регистрируйся здесь и здесь и начинай получать деньги со своего сайта!