Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Как понимается конструкция С++ по правилам этого языка
Надо С увеличть на единицу и вернуть СТАРОЕ (т.е. не увеличенное) значение
Впервые с языком С++ я столкнулся почти 15 лет назад (почуствуйте какой стаж :))) начав работать на Turbo C++. Тогда из доступной литературы по объектно-ориентированному программированию у меня было очень хорошее (как мне тогда казалсь) руководство по ООП на Turbo Pascal 5.5.
В определенном смысле это было очень правильное руководство, на нескольких простых примерах показывалось, как использование наследования и инкапсуляции позволяет легко работать с разнообразными данными придавать им удобную структуру.
И в то время это казалось очень правильным и полезным (правда та реализация С++ отличается от современного стандарта как небо и земля). Какие-то сомнения пришли гораздо позже.
В частности, первые из них стали приходить при попытках использовать С++ для написания пользовательсвого интерфейса в форточках. Причем аналогичные проблемы в то время были почти у всех. Достаточно просто посмотреть сколько лет прошло между распространением С++ и форточек и появлением сколько-нибудь нормальных библотек классов для С++.
То решение (MFC), которые было предожено, выглядело явным уродством и не зря его в шутку называли Micro$oft Frustration Classes. То, что получалось уже не было нормальным текстом на С++, а представляло из себя уродливое нагромождение непонятных макросов.
Со временем пришло понимание, что на С++ иначе просто нельзя !!! Средства, изначально заложенные в сам язык, настолько негибки и жестки, что реализовывать системы, требующие гибкости на С++ было крайне тяжело и постояннно приводило к кривым способам (т.е. это бага а фича :)).
Давайте посмотрим на истоки возникновения С++ и попытаемся проследить мотивацию г-на Страуструпа. Как известно он для своей диссертации написал на языке Симула программу расчета данных.
Причем программа была написана очень быстро и легко, что весьма порадовало Б.С. Но вот дальше обнаружилось, что скорость работы этой программы крайне мала. Как говорят, ее не хватало даже для того, чтобы насчитать необходимые данные к моменту защиты дисертации.
Эффект, который произвел на (неокрепшие :)) мозги г-на Страуструпа был ужасен. Но вместо визита к психотерапевту он решил создать свой язык, который бы совмещал в себе высокую скорость разработки (от Симулы) и высокую скорость выполнения (от С).
Т.е. была поставлена задача написания языка, во-первых, объектно-ориентированного (в понимании Б.С.), а во-вторых, очень эффективного (в плане скорости выполнения скомпилированного кода).
Исходя из последнего было сразу заявлено "Вы не платите за то, чем не пользуетесь".
Кроме этих требований, также была поставлена задача облегчить переход для программистов на С к новому языку. Также хотелось сделать язык таким, чтобы компилятор мог сразу же находить все возможные ошибки (а для языка С позвать функцию с неправильными параметрами - это легко и просто).
Итак были сформцулированы следующие цели:
Итак, давайте посмотрим, а что же получилось в результате.
Только вот как понимать ОО - я не думаю, что программисты, пишущие на Smalltalk'е, сочтут С++ объектно- ориентированным языком. Даже по сравнению с Java С++ явно в проигрыше - нет интерфейсов (зато есть множественное наследование со всеми его проблемами), объектная модель просто отсутствует.
Понятие метаинформации (а также reflection, introspection и т.п.) в С++ отсутствует полностью (а вся среда Smalltalk построена на ней).
Много ли Вы знаете простых и удобных persistence-библиотек для С++, которые бы сами могли сериализовать произвольные объекты ? Максимум, что есть, это сложные библиотек с кучей ограничений и необходимостью вручную (зачастую при помощи макросов) задавать фактически часть метаинформации.
А вот в Smalltalk'е или в Python'е таких библиотек полно.
А как дела у С++ с интеграцией со скриптовыми языками - Perl, Python, Tcl, Ruby и др ? Опять же очень плохо - надо ручками задавать всю необходимую метаинформацию. Есть правда генераторы интерфейсов, которые разбирают Ваш исходный код на С++ и по нему строят необходимые описания.
Только это не всегда работает и почему это не может делать сам компилятор.
А вот Python умеет легко работать даже с такой кривой объектной моделью как СОМ, умеет сериализовать произвольные объекты, умеет на ходу добавлять объекту новые методы.
А как с программироваинем GUI - одна из распространенных кроссплатформенных библиотек Qt использует свой препроцессор языка С++ для выделения необходимой метаинформации.
Кроме того, С++ вообще не различает два существенно РАЗНЫХ понятия - абстрактнывй тип данных (АТД) и объект.
Классическим примером АТД яволяется класс Vector3D - фактически простая (зачастую inline-овая) обертка вокруг данных. Просто структура, но более удобная в обращении.
Возможно наследование (хотя редко), но только как структур. Никакого переопределения чего-нибудь вообще быть не может. Основной смысл - удобство работы (чтобы не надо было все векторные операции расписывать покомпонентно или наполнять код вызовами addVector, subVector и т.п.).
Естественно, что никаких таблиц виртуальных методов не нужно, зато очень полезной может оказаться возможность переопределения операторов.
Поскольку типы довольно простые, то крайне важна эффективность компиляции (в противном случае зачем вообще нужны такие классы).
Объект, с другой стороны, это то, что с самого начала замышляется как нечто динамичное, расширяемое. Мы всегда можем получить вместо объекта одного класса объект унаследовнного класса (или просто объект, поддерживающий нужный интерфейс).
Для объекта важна максмальная гибкость, как в возможности наследования и переопределения, так и в возможности композиции.
Причем с легкой руки авторов книг по С++ объектную ориентированность обычно понимают только как наследование объектов. А теперь откройте Design Patterns и посмотрите на что там большинство паттернов. А они почти все на композицию объектов - возможность получать новую функциональность путем соединения объектов правильным образом между собой.
Причем это соединение может происходить прямо во время выполнения программы (и ее почему-то не надо перезагружать для этого :)) в отличии от широко известной операционной системы.
В этом плане очень интересна книга Андрея Александреску "Современное проектирование на С++". В ней автор просто чудеса творит, красиво реализуя паттерны и многие полезные вещи. Но все его чудеса относятся почему-то исключительно ко времени компиляции. И требуют кстати весьма немало этого самого времени.
А вот в Objective-C (другой вариант построения объектно-ориентированного языка на основе С, только в нем используется объектная модель языка Smalltalk) можно прямо на ходу узнать какие интерфейсы поддерживает объект, какие у него есть методы. Можно даже прямо на ходу добавить новый метод. И это компилируемый язык, весь графический интерфйес таких операционных систем как NeXTSTEP и Mac OS X построен именно на этом языке.
Сколько шума была из-за "острого" С (C#) в связи с делегированием - еще одним крайне важным элементом объектно-ориентированного программирования.
Делегирование - это возможность передать запрос другому объекту (делегату). Послушаешь сейчас мелкомягких о крутости делегирования в "остром" С - ну прямо они его и придумали и подарили миру :)).
А вот оказывается 15 лет назад делегирование было одни из краеугольных камней графического интерфейса NeXTSTEP'а. И было там крайне легко и красиво сделано. И была там 32-разрядная ОС, вся графика через Display PostScript (в частности, полная поддержка альфа-канала, появившаяся только в Windows XP), возможность вкладывания звуковых сообщений в любой документ и многое другое.
А в это самое время micro$soft только-только выпустил 16-битовый Windows 3.0.
С другной стороны, посмотрев на С++, становится понятно, почему делегирование замалчивалось. А как его можно реализовать на С++, как можно попросить какой-то объект вызвать какой-то метод (даже, если список входных параметров у него точно такой же) ?
В С++ это практически невозможно сделать. Именно поэтому сперва в Delphi и C++ Builder, а затем и в остром С появилось возможность делегирования как расширение языка.
Около года назад подобная возможность наконец-то вошла в стандарт С++. Правда в очень кривом виде. Осталось теперь только дождаться компилятора, который ее правильно реализует - кстати посмотрите в книге Александреску на каких компиляторах он весь код свой проверял, скорее всего вы ни одного из них просто не знаете.
Итак, с объектной ориентированнностью получается плохо - есть какая-то странная смесь АТД и объектов (половина от одного, половина от другого), объектной модели нет вообще, основная гибкость относится ко времени компиляции. Наиболее гибким элементом языка являются шаблоны. И именно на них А. Александреску и творит свои чудеса.
Вспомним "Вы не платите за то, чем не пользуетесь".
А как же исключения (exceptions), а как же RTII ?. За них то я всегда плачу всегда !!!
Крайне интересной особенностью С++ является то, что компилятор в ряде случаев сам генерирует код и тихо вставляет его в вашу программу.
Так если функция описана следующим образом:
void f ( const std::string& s );
А если Вы ее вызвали как f ( "Hello, world!" ), то скрыто от Вас будет создан объект класса std::string (а Вы знаете что в английском языке сокращение STD расшифровывается как Sexually Transmitted Deseases, т.е. заболевания, передающиеся половым путем), он передается функции, а потом уничтожается.
Т.е. вполне корректный и правильный код несет в себе скрытые затраты на создание и уничтожение объекта.
Кроме того, компилятор может также скрыто вызывать оператор преобразования типа, что также ведет к затратам.
Еще одной интересной особенностью языка являются ссылки (references). Давайте сравним два описания функции:
void func1 ( A& a );
void func2 ( A a );
В чем разница между этими описаниями (кроме того, что возможно программист просто пропустил символ '&') ?
А в том, что во втором случае внутрь функции передается не то, что попросил передать программист, а лишь его копия. Т.е сначала вызывается copy-constructor, а по возвращении еще и деструктор.
Т.е. всего один символ в описании параметра может привести к очень большим затратам на создание и уничтожение вот этих временных объектов. А если это достаточно "тяжеловесный" объект или данная операция проводится в цикле, то затраты могут оказаться очень большими.
Не говоря при этом о том, что если программист надеется через переданный в функцию объект получить какой-то результат (т.е. изменение в объекте), то он надеется совершенно напрасно - будет изменена лишь копия объекта, которая сразу же после этого будет разрушена. Здорово, а ?
Т.о. вполне правильный (вроде бы) код может работать очень медленно (и неправильно). Особенно это относится к STL, где копирование объектов весьма распространенная вещь.
Причем для понимания таких мест нужно довольно хорошее знание языка. Конечно проффессионалы с многолетним опытом написания программ на С++ таких ошибок не делают (ну или почти не делают).
А вот те, кто только начинают использовать С++, насколько сильно эффективность их программ страдает от подобных особенностей языка ?
Есть примеры на STL, когда эффективность на простых строковых операциях падала в десятки раз.
Т.е. на С++ МОЖНО писать очень эффективно. И НЕКОТОРЫЕ даже пишут. Вопрос только - относитесь ли Вы к их числу ? Вполне возможно, что и на Симуле можно было эффективно писать, просто г-н Страуструп не знал как (т.е. знал Симулу также как большинство сейчас знает С++).
Вывод по этому пункту - можно писать эффективно, но пройдет несколько лет, прежде чем Вы начнете это делать. А пока, скорее всего, об эффективности можете забыть.
Итого - данное требование выполнено только для профессионалов, которые и так пишут эффективный код. А для всех остальных - это скорее игра случая.
Ага, Щас прямо :))))
Посмотрите на пример выше, легко будет программисту на С понять в чем ошибка ?
А понять чем же отличается ссылка от указателя ?
А почему во втором случае он не получает того эффекта от вызова функции, на который рассчитывал ?
А почему вдруг у него в программе течет память, если она течет в тех вызовах, которые за него сделал сам компилятор ?
Простейшим примером ситуации, когда у в Вашей программе на С++ может потечь память является автоматическое создание (и их вызов) операторов присваивания и copy-конструкторов.
Если Вы их не написали, то компилятор заботлиыво сделает их за Вас. Только скопирует он все поэлементно, в том числе и указатели на динамически выделенные объекты и ресурсы.
При этом внешне все в порядке - нигде явно операции копирования не вызываются. Просто объекты на время помещаются в контейнерный класс.
Мне как-то пришлось искать почему программа, написанная аспирантом ф-та ВМиК МГУ падает через пару недель работы (она должна была работать на сервере месяцами). Именно по этой самой причине - копирование и уничтожение объектов при хранении объектов в контейнерных классах.
Конечно это лечится :))) - нужно написать правильные copy-конструктор, оператор присваивания и декструктор. Но оказалось, что выпускник ВМиК МГУ (работающий в программистской фирме) этого не сделал. И на клинического дебила при этом не похож.
Если это вызывает у Вас улыбку, подумайте, пишете ли Вы для своих классов корректные copy-конструкторы, операторы присваивания и деструкторы.
Просто маленький такой подарочек от автора языка.
Откройте почти любую серьезную книгу (а не С++ за 24 часа) по С++ - о чем там написано ?
Как НЕ НАДО делать.
И это характерно для очень многих серьезных книг по С++. Причина в том, что язык С++ крайне сложен и запутан, несет в себе множество мест, где люди совершают ошибки.
Прямо такое море граблей, заботливо прикрытах травкой - идешь себе, идешь и вдруг - БАЦ ! Прямо по лбу !.
Сравните объем описания языка С и С++ - разница в объеме во много раз.
Книгой самого Страуструпа вообще убить можно :))(А вот прочитать до конца практически нельзя :))
Известных консультант по ООП, Алистер Коберн в своей книге "Surviving object-oriented projects" считает, что при отсутствии значительного опыта "С++ представляет собой наиболее серьезный технический риск для выживания проекта". И призывает не надеятся, что программисты на С смогут легко перейти на С++.
Ему принадлежит следующее высказывание - "Легче научить программиста на С Smalltalk'у, чем С++". Подумайте об этом, этот человек видел и консультировал очень много проектов.
Мы уже упоминали о "легкости" понимания разницы между ссылкой и указателем (фактически ссылка это третий тип передачи параметра, причем выглядящий как передача по значению, а работающий как передача по ссылке, так ведь удобнее, правда :(().
Кроме уже упомянутых "удобств" С++ можно вспомнить и такую
list <set<string>> a;
В этом объявлении есть ошибка - последнии два символа '>' компилятор сочтет за оператор битового сдвига и выдаст ошибку. Правда удобно и понятно ?
А упомянутые тонкости с копированием, присваиванием и разрушеним объектов ?. Для программиста на С это все ново и на этих местах он, скорее всего, будет ошибаться.
За несколько лет конечно можно научиться обходить основные проблемные места, но для только осваивающего С++ программиста язык полон граблей и скрытых ловушек.
Т.е. с этим требованием полный пролет.
Во имя этой "благой" цели из языка всеми способами выкидывали любые намеки на гибкость - ведь в программе с гибкой структурой искать ошибки гораздо тяжелее. Гораздо проще будет, если наложить побольше ограничений и свести гибкость к нулю.
Ведь сейчас в С++ шаблоны (а что такое шаблон - фактически тот же макрос, но со слабенькой проверкой типов - слабенькой, поскольку средств получить информацию о типе-параметре в языке просто нет) обладают гораздо большей гибкостью, чем объектно-ориентированные средства (из которых осталось фактически только наследование).
Т.е. С++ скорее называть языком макропрограммирования, чем ОО языком.
И при всем этом компилятор зачастую не ловит даже простейших ошибок.
class A { string str; public: A ( const string& s ) : str ( str ) {} };
Боле того, оказывается, что компилятор в ряде случаев не ловит даже такую ошибку, как лишние параметры. Известен случай, когда в библиотеке векторов для обозначения скалярного произведения переопределили оператор ','.
А потом другой разработчик месяц искал почему программа работает неправильно. А все оказалось крайне просто - просто одной функции передавался лишний параметр. Слишком умный компилятор сперва сам перевел этот параметр в вектор, а потом соединил получившийся вектор с другим параметром и посчитал их скалярное произведение.
Как Вам такая ошибка - компилятор не просто не нашел тривиальнейшей ошибки, а сгенерировал по ошибочному тексту программы работающий (правда неправильно) код. И эта милая ошибочка обошлась в месяц работы.
Причем, что самое главное, компилятор в принципе может обнаруживать лишь ошибки в структуре языка. В то время как набор тестов способен определять ВСЕ ошибки.
В литературе по экстремальному программированию Вы найдете много полезной информации об организации тестирования.
Конечно, писать и поддерживать тесты гораздо сложнее, чем просто впихивать свой код компилятору и смотреть, что получится (а обычно получается GIGO - Garbage In, Garbage Out).
Однако система тестов намного надежнее и писать серьезный проект без системы тестов просто глупо - Вы будете постоянно повторять одни и те же ошибки, причем натыкаться на них будете, скорее всего, в совершенно других местах, чем те, где ошибки были совершены. И, скорее всего, гораздо позже по времени, чем когда ошибки были сделаны.
А система тестов (в комбинации с системой контроля версий) позволит Вам практически сразу же определять появление ошибок, причем с довольно точной привязкой к месту и времени их совершения.
Недавно я наткнулся на статью о том, какие языки скоре всего будут использоваться в будущем. И автор выдвигал мнение, что в будущем гораздо более распространенными станут гибкие и простые языки типа Python, Ruby и т.п.
Одна из причин отказа от С++ как раз и заключается в его жесткости, которая, как тогда надеялись, позволит бороться с ошибками. И здесь, по мнению автора тестирование оказывается гораздо выгоднее.
А за жесткость и негибкость С++ приходится расплачиваться большим временнем компиляции. В системах из многих миллионов строк кода небольшое изменение одного из классов (особенно при активном использовании шаблонов и STL) приводит к перекомпиляции огромного объема исходного кода, что может занимать многие часы.
Причем эта перекомпиляция связана именно с жесткостью языка, с тем, что почти все решения о структуре классов принимаются исключительно на этапе компиляции и узнать о них во время выполнения просто нвозможно. Т.е. небольшое изменение (например добавление нового метода в класс) приводит к изменению структуры класса и необходимости перекомпиляции даже тех классов, которые данный метод вообще не используют.
Краткий итог - даже простейшие ошибки могут не обнаруживаться компилятором. И выгоды от переваливания проверок на компилятор крайне незначительны. Тестирование гораздо надежнее и удобнее.
Давайте посмотрим, какие же требования удалось выполнить в С++.
А получается что НИКАКИЕ требования полностью не выполнены. Но мы имеем огромный и очень сложный язык с массой ловушек.
Язык, который приходится вручную интегрировать со скриптовыми язками, где для получения persistence приходится самому программисту вручную задавать необходимую информацию о членах класса (вместо того, чтобы использовать для этой цели компилятор).
Язык, на котором с огромным трудом реализуется делегирование, с трудом рализуются основные паттерны проектирования, который своей убогостью заставляет программистов решать такие задачаи, которые в ОО языках просто не возникают.
Простейшим примером такого рода является так называемый виртуальный конструктор, т.е. создание объекта, класс которого на момент компиляции не известен. Для С++ это серьезная задача, на эту тему написано много статей, есть различные подходы.
А вот в Smalltalk'e и Objective-C таких проблем вообще нет. Там для этого используется метаинформация (в виде объекта класса) - достаточно просто передать метаинформацию для требуемого объекта и по ней объект легко создается.
Еще несколько интересных моментов в языке С++.
Язык сильно закладывается на использование шаблонов, т.е. создание параметризованных функций и классов, где в качестве параметра выступает имя типа.
Это конечно здорово, но вот как узнать является ли этот тип каким-либо из элементарных типов, указателем или ссылкой ? Является ли он const или нет ?
Конечно есть библиотеки, предоставляющие подобную функциональность (например boost), но посмотрите на их исходный код, а еше лучше попробуйте его понять.
Почему этих средств нет в самом языке, где это было легко и просто сделать ?
Для программистов на С++ понятие инкапсуляции как-то незаметно перешло в "data hiding" - прятание данных. Только это разные понятия. Одно дело собрать вместе связанные элементы, а совсем другое превратить это вместе в черный ящик.
Зачем прятать данные от библиотеки, реализующей persistence ?
Чтобы потом всю эту информацию набрать ручками, вместо компилятора ?
Мне недавно очень уважаемый мной разработчик сказал, что метаинформация вообще противоречит идеям объектно-ориентированного программирования. Если инкапсуляцию понимать как data hiding, то конечно противоречит.
А вот с точки зрения инкапсуляции все нормально - объект предоставляет информацию о себе в стандартизованном формате. Это позволит тем, что умеет (или кому она нужна) реализовать свою задачу (это может быть persistence, делегирование функций или связывание со скриптовым языком).
А насчет "опасности открывать структуру" - так ведь всегда можно сделать так -
memset ( &myObject, '\0', sizeof ( myObject ) );
Вполне законная конструкция, но сами понимаете, что она делает с объектом. Поэтому способов нарушить целостность объекта всегда хватает.
Реальною угрозу предоставляет именно попытка что-то сделать с объектом, не учитывая его внутреннюю структуру. Проще всего декларировать интерфейс, тогда он показывает соотвествие этой структуре, хотя явное приведение типа (Object2 *)&object1 еще никто не отменял.
А использование метаинформации как раз и позволяет учитывать эту самую внутренню структуру. Причем именно на этапе выполнения. Хотите делегировать функцию mouseClick другому объекту - просто проверьте, есть ли у него функция mouseClick с правильным списком параметров и типом возвращаемого значения.
И это ничуть не опаснее жесткой типизации, но зато это гораздо более гибкий механизм. И в языке Objective-C есть возможность как узнавать поддерживает ли данный объект определенный интерфейс, так и проверять есть ли у объекта заданная функция.
Причем все функции всегда виртуальны, и не по номеру в таблице виртуальных методов (который может меняться при изменении класса, переходе на другой компилятор или платформу), а по имени метода.
Сразу возникает вопрос об эффективности - как это по имени искать метод, а если у меня цикл ?
Вот тут С++ действительно в пролете - каждый вызов будет нести в себе цену вызова виртуальной функции. А в Objective-C можно один раз по имени получить адрес метода, как обычной функции, после чего вызывать ее в цикле как обычную функцию (т.е. overhead виртуальности просто нулевой).
Сразу отвечу на вопрос почему же я не пишу на том же Objective-C.
К сожалению для данного языка (кроме как на Маках) нет интегрированной среды, язык практически не поддерживает АТД (а для графики возможность использовать такие АТД как вектора и матрицы очень удобна).
Многие ли купят книгу даже по интересной теме, если весь код там на незнакомом и малораспространенном (хотя и очень простым, перейти на Objective-C с С действительно вопрос нескольких дней) ?
Многие ли заказчики захотят получить продукт на таком языке ?
Просто смотря на этом язык очень легко видны многие слабости и дыры (в дизайне) С++. Когда видишь, как на компилируемом языке (основанном на С) легко и просто реализуются сложные вещи, реализация которых на С++ очень сложна (если возможна), начинаешь видеть в С++ то, о чем обычно просто не задумываешься (хотя бы потому, что просто считаешь это невозможным или слишком сложным).
"Any road followed precisely to it's end leads precisely nowhere"
Dune, Bene Gesserit saying
Не так давно я узнал, когда именно г-ну Степанову пришла идея библиотеки STL - когда он находился в отделении интенсивной терапии (после отравления рыбой) в, как он сам написал, "delirium state", по-просту - в состоянии БРЕДА.
Этот малеьнкий фактик очень удачно вписался в мои собственные представления об STL и самом языка С++. В предельно короткой форме: STL - это просто болезненный бред.
Бред относится к авторам библиотеки, а болезненный - это состояние ее пользователей.
Уже с самого начала STL производит впечатление чего-то монстроидального и совершенно непонятного. Это нагромождение template'd классов (яязык не поворачивается называть их объектами, это просто АТД-переростки) с огромным количество параметров и морем методов с весьма непонятными названиями.
Например в классе std::list есть метод insert, позволяющий по итератору вставить значение в список. Только вот не понятно куда именно, поскольку можно вставить непосредственно перед тем местом, на которое указывает итератор, можно после него, а межно и непосредственно на него.
Из названия мочное место вставки никак не следует.
В том же классе методы begin и end возвращают указатели (точнее итераторы) на начало и конец списка). Казалось бы в названии методов для вставки объекта в начало и конец списка было бы естественно употребить те же слова (равно как и само слово insert для обозначения операции вставки).
Щас прямо ! Используемые методы называются push_front и push_back. При этом мало того, что для обозначения операции вставки в список было использовано слово, обычно обозначающее помещение элемента на вершину СТЕКА, так еще для обозначения начала и конца списка были использованы термины, отражающие обычно положение по отношению к наблюдателю (перед и назад) !
Как уже упоминалось, милейшим свойством STL является постоянное копирование объектов, что оказывается веьсма неприятным сюрпризом при хранении сложных объектов с контейнерных классах.
Крайне незначительные отличия хранимых классов от того, что по мнению авторов STL, должно быть, легко приводит к огромной неэффективности или даже утеканию ресурсов.
Еще одним весьма неприятным сюрпризом является то, что в класса std::string (пол крайней мере во многих реализациях) быблиотека пытясь минимизировать выделение/освобождние/компирования памяти в многих случаях использует общий бефер для строк с одинаковым значеним.
Рассмотрим пример:
std::string a = "abc"; std::string b = a;
Для многих реализаций обе переменные a и b будут оказывать на один и тот же буфер в памяти, по-возможности, отслеживая моменты, когда необходимо "расщепить" общий буфер, дав каждой переменной по своей копии.
Однако в случае многонитевых приложений подобная политика запросто приводит к возникновению т.н. raise condition, например, одна нить может уничтожить одну переменную, а другая - что-то записать в нее. При этом сущесвтует вероятность того, что произойдет обращение к уже освобожденному блоку памяти и падению программы.
Можно, конечно возразить, что никто не обещал, что STL будет успешно работать в многонитевых приложениях. Однако в ряде реализаций не выдается даже предупреждения на стадии компиляции о возможных проблемах.
И если STL нельзя использовать в многонитевых приложениях (а многие реальные приложения являются многонитевыми, много Вы видели общаний что callback выдет вызван на той же нити, на которой Вы его поставили ?
В данном случае попытка оптимизации приводит к скрытым и тяжело обнаруживаемым проблемам.
Еще одной милой чертой STL является заметное увеличение времени компиляции и объема выполнимого кода. Правда насчет последнего уверяют, что в правильном этого можно избежать. Ну и много Вы видели правильных (хотя бы полностью соответствующего стандарту языка) компиляторов ?
А как правило, каждый раз, когда Вы используете класс map, для него заново генерируется код и включатся в выполнимый файл.
А как Вам очень распространенное предупреждение компилятора в VC 6 о том, что длина идентификатора превысила 255 символов ?
Большинство книг по STL посвящего тому же, чему и большинство книг по С++ - чего и как не надо делать. Как на первый взгляд вполне нормальные конструкции либо не работают вообще, либо делают это очень медленно.
При этом самое интересное, что в большинстве случаев подобные проблемы вызываны желанием авторов библиотеки сделать получающийся код БОЛЕЕ ЭФФЕКТИВНЫМ (а вот про эффективность программиста при этом забыли напроч)!
А попробуйте как-нибудь пройтись отладчиком по коду, использующему STL.
Глядя на монстроидальную сложность STL возникает ощущение, что его авторы до сих пор находятся в состоянии бреда !.
Просто песней являются алгоритмы в STL - где еще (кроме давно забытого языка APL) можно всего в одну-две строчки записать такое, что требует многих часов для понимания того, что же в этом месте делается (про понимание того, как это делается я уже и не говорю).
Про эффективность подобных двух строк можно только гадать и молиться на компилятор.
Достаточно полное описание STL занимает свыше 400 страниц (к примеру, The C++ Standart Library. A tutorial and reference занимает почти 800 (!) страниц).
Фактически STL представляет собой безумных размеров черный ящик (причем макросов!), совершенно непонятно как работающий и с огромных количеством скрытых ловушек.
Сравните описание STL и работу с ним с работой в языке типа Python - крфйне небольшое число простых встроенных классов с понятными методами и операциями над ними.
При этом следует иметь в виду, что все эти встроенные классы и методы для работы с ними были написаны на чистом С и сильно соптимизированы, поэтому скорость работы может в ряде случаев оказаться даже выше чем у эквивалентной С++ программы (при несравненно меньшем объеме кода).
Анаолгичный код с использованием STL, как правило, гораздо менее понятен, отладка его крайне затруднительна. И при всем этом у Вас легко может получиться ограмных размеров выполнимый файл, крайне медленно работающий.
В определенном смысле STL можно воспринимать как доведенную до абсурда попытку дать пользователям С++ то, что сразу же доступно пользователям языком Python, Perl, Smalltalk, Ruby и т.п.
В результате получился монстр, пользоватьеся которым без руководства под рукой практически невозможно из-за его объема и отсутствия единой, простой и понятной модели, наличия большого числа подводных камней и соверешенной неопределенности во чито же именно выльется та или иная конструкция (в отличии от того же С где это было всегда ясно).
Одной из причин этого, скорее всего, является отсутствие хоть какой-нибудь целостной объектной модели в языке С++.
Лучше всего STL подходит для работы с атомарными типами (типа int, float и т.п.), где все операции создания/уничтожения/копирования крайне просты и эффективны.
Однако если же Вы хотите использовать STL для работы со своим классом, то необходимо привести его интерфейс к интефейсу атомарных типов (т.е. корректно, не полагаясь на компилятор, самому написать copy-конструктор, default-конструктор, деструктор, оператор присваивания). Причем все эти операции доложны быть реализовано максимально чисто и корректно.
Однако для достаточно сложных классов (хотя бы просто выделающих для себя какие-то ресурсы) перечисленные операции обычно оказываются довольно дорогостоящими.
Вполне возможно, что именно этим и объясняется странный факт использования экземплярами класса std::string общего буфера - это заметно снижает расходы на создание/копирование/уничтожение строк.
В результате для строк пришли к классическому reference counting'у (правда использовав его только для класса string и спрятав его глубоко внутрь класса).
Фактически STL неявно проповедует свою объектную модель (т.е. эквивлентность атомарным типам), но поскольку большинство реальных объектов намного сложнее атомарных и в эту модель укладываются с трудом (и большими накладными расходами).
Одно время в качестве панацеи считали (это легко прослеживается по годам написания книг) auto_ptr. Прошло немного времени и стало ясно, что от него проблем гораздо больше, чем пользы. Теперь панацеей объявляется уже reference-counted-ptr.
Весьма вероятно, что в конце концов удастся построить набор классов для "заворачивания" указателей и ссылок на объекты/память/ресурсы, чтобы реальные объекты можно было использовать в STL без слишком больших проблем.
Вот только какова будет сложность такого заворачивания, учитываю предыдущий опыт, на простое и понятное решение надеятся не приходится :((
А возврщаеясь к скриптовым яызкам - там почему-то все очень легко и просто, и внутри всего лежит именно reference counting.
А если взять библиотеку Foundation для платформ NeXTSTEP или Mac OS X, то она целиком написана на Objective-C и нет там подобных проблем. Наличие единой, простой и гибкой объектной модели позволило легко и просто реализовать работы с контейнерными классами почти так же легко и просто, как на скриптовых языках.
Возможно для работы с атомарными классами подход, использоанный в STL и окажется быстрее и экономнее (в Foundation как и в Java для хранения в контейнерах атомарные типы заворачиваются в объекты), но зато при работе с реальными классами (например, работа с распределенными объектами, которая там была реализована красиво и просто и за много лет до мелкомягких, или же работа с GUI-классами) проблем не возникает.
Статья, объсняющая почему С++ плохо подходит для написания графических пользовательских интерфейсов.