17 November 2011

C++11: революция или работа над ошибками?

"... и много много радости детишкам принесла!"
из детской песенки

Иногда мне кажется, что я люблю C++.
Это не похоже на крышесносяший страстный роман с роковой красавицей, это скорее теплые чувства к жене, с которой вы разменяли под одной крышей не один десяток лет. Да, у каждого из вас есть какие-то недостатки, да, бывало всякое, но, в конечном счете, вы близкие люди и не тратите лишних слов, понимая друг друга уже невербально.


Кликабельно до размера настольных обоев

У языка C++ сложная судьба. Страуструп поступил очень мудро, обеспечив практически полную совместимость своего детища со сверх популярным и тогда, и что более удивительно -- сейчас, Си. Если бы не этот шаг, вряд ли бы плюсы стали бы тем, чем они стали. Но это достоинство языка с каждым днем становится все большим и большим его недостатком, добавляя множество дыр и низкоуровневых проблем в язык вроде как высокоуровневый и современный. Многие до сих пор так толком и не понимаю, что С и C++ это два совершенно разных языка, на которых нужно писать совершенно по-разному. И расхожее сочетание "C/C++" это просто какой-то root of all evil. Мне сплошь и рядом попадаются люди, которые худо-бедно программировали на Си, потом за день выучили ключевое слово "class" и стали считать себя большими гуру в плюсах (к примеру, такой случай). "Специалист" подобного рода узнается слету -- у него в коде вы обязательно найдете любимый printf... Плюсы очень мощный и одновременно довольно сложный инструмент, на овладение и глубокое понимание которого требуется много времени. Без полной перестройки мозгов начать программировать на C++ после С никак не получится, и те, кто этого не понимает, серьезно портят репутацию языка. Когда Линус Торвальдс, ни черта на смыслящий в C++, начинает поучать других на тему, почему плюсы это зло, выглядит это чертовски комично.

Другая историческая проблема языка -- очень позднее принятие стандарта и мучительно долгое подтягивание компиляторов под его требования. Плюсы начали очень активно использоваться в индустрии с начала 90-х. Стандарт был принят только в 98-м. Более менее приличные компиляторы появились только в 2000-х.
В середине 90-х излишне консервативные товарищи имели все основания говорить о том, что исключения в C++ это какое-то баловство и новомодная ересь, и именно из той эпохи тянется взгляд на язык как на "си с классами", которое, к сожалению, широко используется и до сих пор.

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

Лично я не считаю, в отличии от многих, что новый стандарт это революция, и C++11 это совершенно новый язык. Дело в том, что C++98 несет в себе просто нереальное количество недодуманностей, ограничений и откровенных недоработок, поэтому я бы сказал, что новый стандарт разрабатывал Капитан Очевидность и главная задача, которая стояла перед ним -- латание старых дыр и огрех, затянувшееся на непростительные 10+ лет.
Я отношу себя к той категории людей, которым не нужно читать умные толстые книжки о том, чем может быть вам полезно то или иное новшество в языке. Я из тех, кто много много лет в нетерпении ерзал на стуле, ожидая, когда же наконец комитет закроет все эти бесчисленные проблемы в дизайне моего любимого языка...

***

Давайте попробуем рассмотреть нововведения с точки зрения "работы над ошибками". Поехали.

#1. Опускание типа там, где оно может быть определено компилятором -- auto. Затыкание неудобства, известного буквально каждому плюсовому кодеру. Вот у вас есть какой-то развесистый тип на основе std::map. А вот тут у него нужно взять и определить итератор. В итоге, или порождаем развесистый копипаст и длинное некрасивое имя типа "std::map<бла бла бла>::iterator". Либо делаем typedef для нашего типа map и используем его. Теперь же можно просто писать auto и не морочить себе голову.
Меня, кстати, всегда возмущал тот факт, что я не могу написать что-то типа cont_var::iterator, т.е. взять тип через имя переменной. По-прежнему остается туманным момент из нового стандарта, будет ли работать decltype(cont_var)::iterator, ибо в 2010-й студии это не работает, а что-то найти и понять из текста стандарта невозможно в принципе, ибо этот документ создан для сильных духом самураев, работающих на самым сложным проектом в мире -- компилятором C++.
У многих уже сейчас есть нехорошие предчувствия на тему того, что индусы и всякие другие нехорошие геи будут этим самым auto чрезмерно злоупотреблять. Ежу понятно, что программист интеллигентный будет использовать это дело аккуратно и к месту, только так, как оно и задумывалось -- итераторы, развесистые умные указатели, лямбды, результат bind выражения и проч. А говнокодеры пусть на си с классами пишут, не стоять же из-за этим придурков много лет на месте.
Кстати, по мнению Саттера, auto это самый серьезный удар по совместимости со старым кодом в новом стандарте.

#2. Знаменитые лямбды, которыми нам все уши прожужжали. Вещь, несомненно, хорошая, не спорю, но у меня есть взгляд на это нововведение под другим углом.

Дизайн STL, основанный на триединстве б-га в лице итераторов, контейнеров и алгоритмов, тема для отдельного большого разговора. Вне всяких сомнений, в таком решении есть определенная красота и определенные достоинства (вроде возможности использовать алгоритмы для обычных указателей). Но и недостатков -- выше крыши.
Это сильно громоздко. Потому что сильно проще написать как в Qt или .NET "cont_var.contains(val)" чем "std::find(cont_var.begin(), cont_var.end(), val) != cont_var.end()". И мне плевать на рассуждения высоколобых дядек о том, что универсальные, абстрагированные от контейнеров, алгоритмы это круто, и вообще, если у тебя есть возможность реализовать метод класса без использования его protected/private членов, то этот метод надо выносить из определения класса... Мужики! Это не нормально, когда для выполнения одной простой операции мне надо три (!) раза обратиться в выражении к контейнеру. Это не только громоздко, это еще и опасно, т.к. за тем, чтобы имя контейнера везде было одинаковым, должен следить программист, который есть породы человеков, котором, увы, свойственно ошибаться. Опасность и громоздкость итераторов -- проблема, которую начали обсуждать отнюдь не вчера, смотри, к примеру, Iterators Must Go от человека-бизона -- Александреску...

Кстати (отложим пока лямбды), в области итераторов мы тоже получили несколько сверхочевидных правок. Например, begin() и end() принято теперь писать как вызов функции для контейнера, а не вызов его метода. Потому что это много более гибкий способ, позволяющий "извлечь" итераторы из любой сущности. Например, из сишного массива. Или чужого класса, который изначально такими методами не обладает.
Еще одно улучшение в области итераторов -- возможность вызвать у контейнера erase() передав туда константный итератор... Аргх! Тот, кто хоть раз докапывался до сути этой проблемы, знает какая она дурная по своей природе. У вас есть некий метод у объекта с названием Find(), который возвращает итератор. Поиск, он константный по своей сути, сам метод ничего не меняет, поэтому пишем const для метода, и нас вынуждают возвращать const_iterator. Через какое-то время нам нужно воспользоваться этим константным методом Find() в неконстантном методе для того, чтобы удалить найденный им элемент. Все, приехали! Возвращаем iterator, снимаем const с Find() и по цепочке вынуждены снимать const со всех константных методов, которые вызывали наш Find(). В случае, если ваш контейнер имеет random iterators, то можно не париться, и возвращать индекс, но вот если ваш контейнер, к примеру, std::list, то у вас большие неприятности. Потому что в языке нет O(1) способа породить из const_iterator просто iterator.

Ладно, итераторы я вспомнил к слову, лямбды -- эта вся история об алгоритмах. Смотрим на std::for_each. Скажите мне, хоть один человек в здравом уме и трезвой памяти его использовал?...
Другая классическая проблема -- std::find_if, когда контейнер содержит какие-то объекты, и поиск нужно сделать для значения в каком-то из его полей. Стандартная библиотека предлагает нам городить уродливые выражения из всяких там not_equal_to + mem_fun + bind1st и прочего гавна, признанного в новом стандарте depricated. Ну или лепить целый отдельный тип-функтор, с конструктором, замыкающим нужные переменные, глобальный, потому что в шаблон нельзя подставлять локальные типы... В общем, кто хоть раз писал, знает о каком зоопарке идет речь. Много много никому ненужных букв, почему многие программисты просто предпочитают написать явный for(), который выйдет компактнее и проще.
Да, есть boost, в котором есть псевдо лямбда, смахивающая на резиновую женщину. Есть boost::bind(), который очень лихо умеет замыкать переменную из правой части для операторов сравнения. Но все это совсем не то, все это костыли и полумеры... Так вот, в язык, наконец-то, добавили механизм, позволяющий нормально использовать алгоритмы! Мужики, мы таки дожили до того дня, когда даже std::for_each стал полезным!

#3. Перемещение (move) для rvalues. Есть ни что иное, как решение всем давно известной проблемы, вокруг которой был создан специальный хак, под названием RVO.
Интересный сайд эффект от новой фичи, появление в языке не только копируемых (copiable) но и перемещаемых (movable) типов. Теперь вы можете хранить в векторе не копируемые типы! Здравствуй, unique_ptr.

Кстати, implicit sharing, который активно используется в Qt, есть более фундаментальное решение проблемы. Строка, которую не западло передать в функцию по значению, давняя мечта любого плюсового программиста.

#4. Шаблоны с переменным числом параметров. Собственно говоря, а почему бы и нет?

#5. Типобезопасный enum. Возможность делать forward объявление для enum.
Блин, без комментариев!

#6. Нормальная работа с виртуальными методами. Ключевое слово override и проч.
Без комментариев, хотя некоторые программисты с диагнозом "Java головного мозга" недоумевают.
Кстати, по-моему, на одном из собеседований какой-то толстый тролль спросил у меня "при перекрытии виртуального метода в потомке нужно писать virtual или нет?". Надавил, гад, на мозоль. Хотелось дать ему в морду.

#7. Несмешное -- nullptr.
Это какой-то разрыв шаблона (с переменным числом аргументов). Это как в школе, когда мы пол-урока хором кричали "делить на ноль нельзя", а потом в институте на лекциях по высшей математике я наконец-то узнал, откуда на самом деле берутся дети.
Дядя Страуструп, ну зачем ты нас обманывал, рассказывая, что NULL это пережиток прошлого, и теперь мы можем везде писать 0 и не париться?

#8. Снятие идиотского ограничения на использование локальных типов в шаблонах.
Без комментариев.
Кста, студия, по-моему, забивает на это дело. Но потом приходит GCC и ты, громко матюкаясь, что снова наступил на эти грабли, переписываешь код.

#9. Инициализация "на месте" для нестатических членов данных класса. Аааа!
Ну почему, почему, это не было сделано сразу?!
У нас сейчас в проекте 99.9% случаев undefined behaviour связанно с bool полем, которое добавили и случайно забыли проинициализировать в конструкторе. Хоть подменяй стандартное управление кучей и возвращай блоки, залитые нулями (понятно, что для стековых переменных не помогает).
Может кто знает статический анализатор, тыкающий в эту проблему?

***

В общем, список можно продолжать и дальше. Raw strings. "Наследование" конструкторов (писали когда-то развесистую иерархию исключений? макросы были, да?). Расширение синтаксиса using, позволяющее в человеческом виде описывать указатель на функцию. И так далее и тому подобное.

Библиотека? Ну о чем тут говорить?
Умные указатели, без которых сегодня даже "hello world" не напишешь. Любимый auto_ptr, наконец-то, отправлен на пенсию. Я бы просто оторвал руки тому, кто сделал копируемым объект с таким вот замечательным побочным эффектом от этого копирования.
Хэш контейнеры. Серьезно.
Регулярные выражения. Да, да.
Поддержка многопоточности.
Продолжать надо?

Вообщем, повторюсь еще раз -- разработка "нового" стандарта, занявшая целых 13 лет, призвана заткнуть самые сверхочевидные и наболевшие проблемы в языке, вроде абсолютно неюзабельных стандартных алгоритмов или умных указателей в лице auto_ptr.

Над чем будет работать комитет следующие лет пятнадцать?... О, вот тут как раз нас и поджидают революции, вроде сборки мусора или reflection, на фоне всякой мелочевки, вроде концептов, так и не попавших в C++11, и макросов с ограниченной областью видимости (йе, бейби!).

Какие главные проблемы у языка существуют на сегодняшний день? На мой взгляд, проблемы есть, но они косвенные по своей природе. К примеру, очень серьезная проблема -- запредельная сложность компилятора. Конкурентоспособный плюсовый компилятор это самый настоящий rocket science. И добавление какой-то фичи из нового стандарта это не только боль головная, но и боль анальная, потому что фичу надо не просто добавить, но и не сильно снизить общую скорость компиляции.
Кстати, про скорость. Для C++ это чертовски важная проблема. И решить ее можно только одним способом -- менять модель импорта. Включение кусков других файлов в один общий мегафайл для раздельной компиляции -- в XXI веке это просто мракобесие, и дело не только в скорости, но и в бесконечном бессмысленном копи-пасте, изматывающем программистов. Intel, в свое время, проводила исследование на тему заголовочных файлов. В начале 90-х в них содержалось всего 10% объема кода. К концу 90-х это значение выросло до 50%. Грустно. Грустно компилировать большой современный C++ проект.


Еще одна проблема -- игнорирование стандартом языка вещей, лежащий вне задач исполнения кода. Я про библиотеки, ABI и примыкающий к ним reflection. Ну не может современный язык абстрагироваться от всех этих реалий. Поэтому и имеем махровую проприетарщину, вроде Windows Runtime в Windows 8 (на эту тему я собираюсь собираюсь написать отдельный, очень основательный пост).

Ладно, нам хотя бы живой C++11 в руки получить.
А то же ведь теперь сидим и ждем, пока напишут мало-мальски приличные компиляторы с поддержкой нового стандарта. Страуструп несколько лет будет мучать четвертую редакцию своей книжки "C++". Потом подтянутся другие авторы. Может даже будет совершен революционный прорыв, вроде открытия метапрограммирования на шаблонах. Новая версия языка более менее начнет входить в мейнстрим, появятся программисты, более менее толково использующие новые фичи. Мало-мальски либеральные фирмы, не сидящие до сих пор на "си с классами", начнут использовать C++11 в своих проектах.
А я к тому времени, похоже, как раз уйду на пенсию, чтобы программировать на питоне в свое удовольствие...

зы. Книги писать уже начали.
Майерс вон подсуетился, уже можно что-то покупать и читать.

Да и с компиляторами все не так плохо. Если не брать Microsoft, которая чисто сапожник без сапог с их печальной историей под названием VC11 и C++11 (обязательно вернусь к этой теме), то остальные игроки внушают определенный оптимизм -- наши внуки точно будут жить при коммунизме писать на C++11. Впереди планеты всей GCC. Не сильно отстает clang, правда они до сих пор почему-то не прикрутили лямбды.
В общем, в области компиляторов работа идет.

No comments:

Post a Comment