Часть 1
Не у каждого объекта может быть один владелец. Нам надо убедиться, что объект уничтожен и освобождён, когда исчезает последняя ссылка на него. Таким образом, нам необходима модель разделённого владения объектом. Допустим, у нас есть синхронная очередь, sync_queue, для общения между задачами. Отправитель и получатель получают по указателю на sync_queue:
Предполагается, что task1, task2, iqueue и oqueue уже где-то были соответствующим образом определены и прошу прощения за то, что thread переживёт область видимости, где они были созданы (посредством detatch()). Вопрос: кто удалит sync_queue, созданные в startup()? Ответ: тот, кто последний будет использовать sync_queue. Это классический случай, когда требуется сборка мусора. Изначально сборка подсчитывала указатели: нужно хранить количество использований объекта, и в тот момент, когда счётчик обнуляется, удалять его. Множество современных языков работают так, а С++11 поддерживает эту идею через shared_ptr. Пример превращается в:
Теперь деструкторы task1 и task2 могут уничтожить их shared_ptr (и в большинстве правильно построенных систем так и сделают), и последнее, что нужно сделать – уничтожить sync_queue. Это просто и довольно эффективно. Никакой сложной системы. Что важно, она не просто возвращает память, связанную с sync_queue. Она возвращает объект синхронизации (мьютекс, блокировку, что угодно), встроенный в sync_queue, чтобы синхронизировать две нити, выполняющие две задачи. Это не просто управление памятью, это управление ресурсами. Этот «скрытый» объект синхронизации обрабатывается так же, как хендлы файлов и потоков в предыдущем примере. Можно попробовать избавиться от использования shared_ptr, введя уникального владельца в какой-либо области видимости, заключающей в себе задачу, но это не всегда просто сделать – поэтому в С++11 есть и unique_ptr (для одиночного владения) and shared_ptr (для разделённого владения).
Я говорил пока о сборке мусора в контексте управления ресурсами. Но есть ещё и типобезопасность. У нас есть операция delete, которую можно применить неправильно. Пример:
Не надо так делать. Непосредственное применение delete опасно и не нужно в обычных случаях. Оставьте удаления классам, управляющим ресурсами — string, ostream, thread, unique_ptr и shared_ptr. Там удаления аккуратно отслеживаются.
С моей точки зрения, сборка мусора – последнее средство для управления ресурсами, а не решение задачи и не идеал.
1. Используйте подходящие абстракции, которые рекурсивно и неявно обслуживают свои ресурсы. Отдавайте предпочтение им, а не переменным в определённой области видимости.
2. Когда вам необходимо использовать указатели и ссылки, используйте умные указатели — unique_ptr и shared_ptr
3. Если ничего не помогает (например, ваш код – часть программы, которая запуталась в указателях, и не использует стратегию, поддерживаемую языком, для управления ресурсами и обработки ошибок), попробуйте обрабатывать ресурсы, не относящиеся к памяти, вручную, и включайте сборку мусора для обработки неизбежных утечек памяти.
Многие верят в то, что эффективный код обязан быть низкоуровневым. Некоторые даже верят, что низкоуровневый код обязательно эффективен. («Если оно такое уродливое, наверняка оно быстрое! Кто-то потратил кучу времени и своего таланта для создания этой штуковины!»). Конечно, можно писать эффективный код на низком уровне, и некоторый код необходимо делать низкоуровневым для работы с машинными ресурсами. Измеряйте, однако, стоит ли это ваших усилий. Современные компиляторы С++ очень эффективные, а архитектура современных машин чрезвычайно сложна. При необходимости такой низкоуровневый код необходимо прятать за интерфейсом для удобства. Часто сокрытие низкоуровневого кода за высокоуровневым интерфейсом способствует оптимизации. Там, где важна эффективность, сначала попробуйте достичь её, выразив идею на высоком уровне, не кидайтесь сразу на биты и указатели.
Простой пример. Если вам надо отсортировать набор чисел с плавающей точкой по убыванию, вы могли бы написать для этого код. Но если у вас нет экстремальных требований (например, чисел больше, чем может уместиться в памяти), это было бы наивно. За десятилетия мы сделали библиотеки с алгоритмами сортировки с приемлемой скоростью работы. Мне меньше всего нравится qsort() из стандартной ISO библиотеки C:
Если вы не программируете на С, или если вы в последнее время не использовали qsort, потребуется кое-что объяснить; qsort принимает 4 аргумента
— указатель на последовательность байтов
— количество элементов
— размер элемента
— функция, сравнивающая два элемента, которые передаются как указатели на их первые байты
Этот интерфейс скрывает информацию. Мы сортируем не байты – мы сортируем double, но qsort этого не знает, поэтому нам надо предоставить информацию о том, как сравнивать double, и сколько байтов в double. Конечно, компилятор знает такие вещи. Но низкоуровневый интерфейс qsort не позволяет компилятору воспользоваться этой информацией. Необходимость указывать такую простую информацию ведёт к ошибкам. Не перепутал ли я два целых аргумента qsort? Если перепутаю, компьютер этого не заметит. Соответствует ли моя compare() соглашениям в C для трёхстороннего сравнения? Если вы посмотрите на промышленную реализацию qsort (рекомендую), вы увидите, сколько усилий приложено для компенсации недостатка информации. К примеру, довольно трудно произвести смену местами элементов, заданных в виде количества байт, чтобы это было так же эффективно, как смена местами пары double. Затратные непрямые вызовы функции сравнения могут быть устранены компилятором только в том случае, если он применит распространение констант для указателей на функции.
Сравним qsort с его эквивалентом sort из С++
Здесь требуется меньше объяснений. Вектору известен его размер, и нам не надо явно указывать количество элементов. Тип элементов не теряется, и не нужно помнить об их размере. По умолчанию, sort сортирует по возрастанию, поэтому пришлось задать критерий сравнения, как и для qsort. Здесь он передан в качестве лямбда-выражения, сравнивающего два double при помощи >. И так получилось, что эта лямбда тривиальным образом инлайнится всеми компиляторами С++, что я знаю, поэтому сравнение превращается в одну машинную операцию «больше, чем» — никаких неэффективных вызовов функции.
Я использовал контейнерную версию sort, чтобы не задавать итераторы явно, то есть, чтобы не писать:
Можно пойти дальше и использовать объект сравнения С++14:
Какая из версий быстрее? Можно скомпилировать версию qsort как С, так и С++ без всякий различий в быстродействии, поэтому это будет скорее сравнением стилей программирования, а не языков. Библиотечные реализации используют один алгоритм для sort и qsort, поэтому это сравнение стилей программирования, а не алгоритмов. Конечно, у разных библиотек и компиляторов будут разные результаты, но для каждой реализации будет видна разумная реакция на разные уровни абстракции.
Я недавно прогнал примеры, и увидел, что sort в 2.5 раза быстрее, чем qsort. Это может меняться от компилятора к компилятору и от компьютера к компьютеру, но ни разу у меня qsort не выиграл у sort. Иногда sort выполнялся в 10 раз быстрее. Почему? В стандартной библиотеке С++ sort явно выше уровнем, чем qsort, при этом более гибкий и общий. Он типобезопасен и параметризован на типе хранения, типе элементов и критерию сортировки. Никаких указателей, размеров, байтов. Библиотека STL, к которой принадлежит sort, старается не выбрасывать никакой информации. Это приводит к превосходному инлайнингу и хорошей оптимизации.
Обобщение и высокоуровневый код могут выигрывать у низкоуровневого. Не всегда, но сравнение sort/qsort – это не единичный пример. Всегда начинайте с высокогоуровневой, точной и типобезопасной версии решения. Оптимизируйте по необходимости.
С++ — объёмный язык. Размер определений схож с С# и Java. Но это не значит, что вам нужно знать каждую деталь, чтобы использовать его, или использовать все функции непосредственно в каждой программе. Вот пример использования основных компонент из стандартной библиотеки:
Предполагаю, что вы знакомы с регулярками. Если нет – самое время ознакомиться. Заметьте, что я полагаюсь на семантику перемещений, чтобы просто и эффективно вернуть потенциально большой набор строк. Все контейнеры стандартной библиотеки обеспечивают конструкторы перемещения, поэтому нет нужды возиться с new.
Для работы примера требуется включить компоненты:
Проверим:
Просто пример. Легко можно поменять get_addresses(), чтобы она принимала регулярку как аргумент, чтобы она могла искать URL или что угодно. Легко поменять get_addresses(), чтоб она распознавала больше одного вхождения шаблона в строке. С++ предназначен для гибкости и обобщений, но не каждая программа обязана быть фреймворком. Суть в том, что задача извлечения емейлов из потока просто выражается и просто проверяется.
На любом языке писать программу только через встроенные возможности языка (if, for, и +) утомительно. И наоборот, при наличии подходящих библиотек (graphics, route planning, database) любую задачу можно выполнить, приложив разумные усилия. Стандартная ISO библиотека С++ относительно небольшая (по сравнению с коммерческими), но помимо неё есть много библиотек как с исходным кодом, так и коммерческих. К примеру, при помощи библиотек Boost, POCO, AMP, TBB, Cinder, vxWidgets, CGAL сложные вещи становятся проще. К примеру, пусть наша программка извлекает URL с веб-страницы. Для начала, мы обобщим get_addresses() для поиска любой строки, совпадающей с шаблоном.
Это упрощённая версия. Теперь надо как-то прочесть файл из веба. В Boost есть библиотека asio для работы с вебом:
Общение с веб-сервером довольно непростое:
При разборе файла www.stroustrup.com/C++.html это даёт:
www-h.eng.cam.ac.uk/help/tpl/languages/C++.html
www.accu.org
www.artima.co/cppsource
www.boost.org
…
Я использовал множество, поэтому URL выводятся по алфавиту.
Я спрятал проверку соединения в connect_to_file():
Я не писал всё с нуля. Работа с HTTP скопирована с документации по asio.
С++ — компилируемый язык, предназначающийся для создания хорошего, обслуживаемого кода, для которого имеет значение быстродействие и надёжность. Он не предназначался для соревнований с интерпретируемыми скриптовыми языками, которые подходят для написания маленьких программ. JavaScript и другие подобные языки часто написаны на С++. Тем не менее, есть много полезных программ на С++, которые занимают всего несколько десятков или сотен строк.
Тут могут помочь авторы библиотек. Вместо того, чтобы концентрироваться на заумных и продвинутых вещах в библиотеках, предоставьте простые примеры “hello, world!”. Сделайте минимальную версию библиотеки, которую легко установить, и пример на одну страничку из того, что она умеет. В тот или иной момент времени мы все оказываемся в роли новичка. Кстати, вот моя версия “hello world” для С++:
Более длинные и сложные версии кажутся мне менее прикольными.
Часто у мифов есть основание. Каждому из них соответствуют моменты и ситуации, когда в них можно верить на разумном основании, основанном на доказательствах. На сегодняшний день я считаю их абсолютно ложными, простыми недоразумениями, хотя и полученными честным путём. Проблема в том, что мифы всегда служат какой-то цели, или они бы уже вымерли. Эти пять мифов служат разным целям:
— они дают комфорт. Не нужно ничего менять, переоценивать и переосмысливать. Знакомое кажется приятным. Перемены вызывают тревогу, поэтому хорошо, если новинка будет нежизнеспособной.
— можно сэкономить время. Если вам кажется, что вы знаете, что из себя представляет С++, вам не надо тратить время на изучение чего-либо нового, экспериментировать с новыми технологиями, измерять код на быстродействие, тренировать новичков.
— можно не учить С++. Если бы эти мифы были правдой, зачем его вообще нужно было бы учить?
— они помогают продвигать другие языки и технологии – в случае их правдивости это было бы необходимо.
Но они ложны, поэтому аргументы за то, чтобы сохранить всё, как есть, искать альтернативы С++ или избегать современного стиля программирования на нём, нельзя основывать на этих мифах. Существовать с устаревшим представлением о С++ в голове может и комфортно, но при работе с софтом необходимо меняться. Можно достичь большего, чем просто использовать С, С с классами, С++98 и т.д.
Приверженцы «старого, доброго» проигрывают. Затраты на поддержку часто больше, чем на написание современного кода. Старые компиляторы и инструменты обеспечивают меньшее быстродействие и проводят худший анализ, чем современные. Хорошие программисты часто отказываются от работы с антикварным кодом.
Современные версии С++ и технологии программирования, которые он поддерживает, отличаются в лучшую сторону от того представления, которое создают «общепризнанные мифы». Если вы верите в какие-то из них – не верьте мне на слово. Попробуйте, проверьте. Измерьте «старый способ» и альтернативы для актуальной проблемы. Попробуйте освоить новые методы, изучить новые возможности и технологии. Не забывайте сравнивать оценочную стоимость поддержки нового и старого способов. Лучший способ опровержения мифа – это представить доказательство. Я представил вам свои примеры и аргументы.
И я не заявляю, что С++ идеален. Он не идеален, он не является наилучшим языком для всего и для всех. Как и любой другой язык. Воспринимайте его таким, какой он сейчас, а не каким он был 20 лет назад, и не таким, как его выставляет кто-то, кто рекламирует альтернативы. Чтобы сделать рациональный выбор, поищите достоверную информацию, и попробуйте сами понять, как современный С++ справляется с вашими задачами.
Не верьте «общепризнанному» знанию о С++, или бездоказательному его использованию. В этой статье рассматриваются пять популярных мнений о С++ и предлагаются аргументы в пользу того, что они – всего лишь мифы:
1. Чтобы понять С++, сначала нужно выучить С
2. С++ — это объектно-ориентированный язык программирования
3. В надёжных программах необходима сборка мусора
4. Для достижения эффективности необходимо писать низкоуровневый код
5. С++ подходит только для больших и сложных программ
Эти мифы вредны.
Остались сомнения? Сообщите мне, почему. Какие ещё мифы вы встречали? Почему они являются мифами, а не правдой? Какие у вас есть доказательства их разоблачения?
1. ISO/IEC 14882:2011 Programming Language C++
2. POCO libraries: pocoproject.org
3. Boost libraries: www.boost.org
4. AMP: C++ Accelerated Massive Parallelism. msdn.microsoft.com/en-us/library/hh265137.aspx
5. TBB: Intel Threading Building Blocks. www.threadingbuildingblocks.org
6. Cinder: A library for professional-quality creative coding. libcinder.org
7. vxWidgets: A Cross-Platform GUI Library. www.wxwidgets.org
8. Cgal — Computational Geometry Algorithms Library. www.cgal.org
9. Christopher Kohlhoff: Boost.Asio documentation. www.boost.org/doc/libs/1_55_0/doc/html/boost_asio.html
10. B. Stroustrup: Software Development for Infrastructure. Computer, vol. 45, no. 1, pp. 47-58, Jan. 2012, doi:10.1109/MC.2011.353.
11. Bjarne Stroustrup: The C++ Programming Language (4th Edition). Addison-Wesley. ISBN 978-0321563842. May 2013.
12. Bjarne Stroustrup: A Tour of C++. Addison Wesley. ISBN 978-0321958310. September 2013.
13. B. Stroustrup: Programming: Principles and Practice using C++ (2nd edition). Addison-Wesley. ISBN 978-0321992789. May 2014.
После публикации статьи на isocpp.org получили разные комментарии. Разрешите мне прокомментировать некоторые из них.
Комментарии подтвердили, что этот материал необходим. Люди повторяют старые аргументы. К сожалению, многие программисты не читают длинные статьи, а короткие отбрасывают за неполноту. Нежелание читать длинные статьи побудило меня написать этот материал и разбить на три части при начальной публикации.
Это не исследовательский материал, который подробно описывает каждую деталь. Как я написал вначале: «Каждому мифу можно посвятить книгу, но я ограничусь простой констатацией и кратким изложением своих аргументов против них».
Тем не менее, многие путают примеры для иллюстрации точки зрения с самой точкой зрения. Некоторые пытались «опровергнуть опровержение», меняя примеры, меняя ограничения примеров, или объявляя примеры тривиальными. Примеры небольшие, т.к. они должны поместиться в небольшую работу. Но они не так уж отличаются от кода, который лежит в основе «настоящих» программ.
Некоторые комментаторы перешли с С++11/С++14, на которых я основывал свою аргументацию, к более старым версиям. С++14 – не С++ из 1980-х. Эти стандарты уже не такие, каким учились большинство людей. И не то, чему учатся на сегодняшних курсах. И не то, что люди видят, разглядывая довольно объёмные тексты существующих программ. Я хочу поменять это представление. Если вам не удаётся работать с моими примерами в какой-либо антикварной версии С++ или со старым компилятором – это плохо, но сегодня есть улучшенные версии всех основных компиляторов (и обычно бесплатные). В моих примерах не было никакого ультрасовременного кода.
Каждый язык программирования, достигающий успеха, сталкивается с проблемами старого кода. Не судите С++ по технологиям программирования 20-летней давности или компиляторам 10-летней давности. Взгляните на современный С++ и попробуйте воспользоваться новыми возможностями, как это уже удалось многим. Сегодня вы почти наверняка пользовались программой, написанной на С++11. Между моим и вашим компьютером очень много шагов, на которых встречаются программы на С++11.
Довольно много комментариев содержат заявления вроде «а в языке Х есть точно такая же возможность» или «библиотека Y в языке X делает именно это». Очевидно, если у вас есть язык, в котором проще, чем в С++, решить вашу задачу, при этом не теряя критично в быстродействии, переносимости и не приобретая ненужных ограничений – используйте его. Но ни один язык или библиотека не подходят идеально для всех и всего.
Я предоставил примеры для общих задачи и общих технологий. Сравнивать что-то с одним примером не особенно нужно. Моя точка зрения относится к общим вещам, а примеры – просто иллюстрации. При использовании достаточно хорошей библиотеки любой язык будет простым и приятным. Для достаточно ограниченной задачи можно сконструировать специальный язык, который будет элегантнее языка общего назначения. К примеру, библиотека asio, которую я использовал в пункте 6.1 – гибкая, эффективная сетевая библиотека общего назначения. Для любой задачи её можно обернуть в простую функцию (или небольшой набор функций), чтобы сделать её более удобной. И мой код был бы реализацией этого. То, что я пытался объяснить в п.6.2 – сообщество программистов С++ могли бы помочь программистам, проведя побольше времени над тем, чтобы делать простые вещи более простыми. Например, в 99% случаев я использую sort(v) вместо sort(v.begin(),v.end()).
Мои комментарии вызвали небольшую бурю. Многие пытались опровергать их простыми возражениями. Я не принимаю аргументы по быстродействию, не подкреплённые данными о тестировании. Мои комментарии были подтверждены реальными измерениями в разных ситуациях на протяжении нескольких лет. Многие из них описаны в книгах. Они правдивы для широкого спектра схожих примеров.
Я имею в виду современную реализацию С++, соответствующую стандартам. К примеру, когда я пишу про быстродействие оптимизации коротких строк, я не имею в виду реализации С++ до С++11. Я не рассматриваю комментарии на тему, что std::sort() или std::string работают медленно без использования оптимизатора. Разумеется – но глупо обсуждать быстродействие неоптимизированного кода. При использовании GCC или Clang используйте –O2; для продуктов от Microsoft используйте release mode.
Я неплохо знаю С и его стандартную библиотеку. Я написал много кода на С ещё до того, как сегодняшние студенты родились, и многое привнёс в язык: прототипирование, константы, инлайнинг, декларации в for, декларации как объявления, и многое другое. Я следил за его развитием и эволюционированием.
Да, C-версия compose() не проверяет значение, возвращаемое malloc(). Я же спрашивал у вас, всё ли я правильно сделал. Я намеренно не предоставил вам код, годный для продакшена. Отсутствие проверки результата – один из основных источников ошибок, поэтому моя «ошибка» специально была сделана для иллюстрации этого. В данном случае часто помогают исключения. Конечно, можно было написать С-версию compose(), используя менее известные функции стандартной библиотеки, и да, можно было избежать свободного хранения, если позволить вызывающему коду передать размещённый в стеке буфер и позволить вызывающему разбираться с проблемой строковых аргументов, которые бы его переполнили. Тем не менее, эти альтернативы не относятся к главному вопросу: такой код писать сложнее, чем в С++, и ещё сложнее писать его правильно. Новички с первого раза пишут версию для С++, но не для С, особенно для тех версий, которые основаны на функциях из стандартной библиотеки, которым новичков не обучают.
С++ использовался в критических и высоконагруженных встраиваемых системах годами – в тех же марсианских Роверах (анализ обстановки и автономная работа), F-35, F-16 (системы управления полётом), и множестве других: www.stroustrup.com/applications.html. И да, космическая капсула Орион запрограммирована при помощи С++.
Да, библиотеки разнятся по качеству, и иногда сложно выбрать нестандартную библиотеку из множества вариантов. Это проблема. Но эти библиотеки существуют, и их исследование часто более продуктивно, чем простое движение вперёд напролом, кончающееся изобретением очередного колеса.
К сожалению, часто библиотеки С++ не разрабатываются с учётом совместной работы с другими. И нет одного места, где можно было бы брать все библиотеки. Я годами наблюдал за процессом обучения студентов по схеме «сначала С», и читал эти программы десятилетиями. Тысячам людей я преподавал С++ в качестве первого языка. Мои заявления о возможности обучению С++ основаны на большом опыте.
С++ обучать легче, чем С из-за более хорошей системы типов и синтаксиса. Необходимо учить меньше трюков и костылей. Представьте, как бы вы стали учить стилю программирования на С, обучая языку С++. Я бы никогда не стал давать новичкам курс С++, который бы:
— не содержал хорошей основы касаемо работы с памятью, указателями, и т.д.
— не давал студентам представления о «чистом С» и о его использовании
— не обосновывал большинство возможностей языка
— пытался бы обучить абсолютно всем техникам С++
Хорошие учителя, преподающие С, не пытаются научить новичков всем техникам.
www.stroustrup.com/programming.html — мой ответ на вопрос «Как бы вы обучали новичков С++?». Эта система работает.
Можно ознакомиться с моей довольно старой работой по некоторым аспектам преподавания С и С++: Learning Standard C++ as a New Language. C/C++ Users Journal. pp 43-54. May 1999 (www.stroustrup.com/papers.html).
Сегодня я бы сделал С-версию курса получше, а С++ — сильно лучше. Примеры отражают стиль программирования того времени (и были рассмотрены экспертами по программированию на С и С++).
Сегодняшний С++ — это стандарт ISO С++14, а не то, что я описывал 30 лет назад, и не то, что ваш преподаватель рассказывал вам 20 лет назад. Изучите C++11/C++14 в том виде, в каком они поддерживаются основными компиляторами, и привыкните к ним. Это гораздо лучший инструмент, нежели ранние версии С++. Сегодняшний С – это стандарт ISO С11, а не K&R C (хотя я не уверен, соответствуют ли сегодняшние компиляторы стандарту С11 так же хорошо, как компиляторы С++ стандарту С++14). Меня шокируют некоторые вещи, которые сегодня преподают под видом «правильного С++».
С++ — это не ООП-язык. Это язык, поддерживающий ООП, другие техники программирования, и их комбинации. Если вы – опытный программист, я рекомендую прочесть A Tour of C++ в качестве быстрого обзора современного языка C++.
4.2 Разделённое владение shared_ptr
Не у каждого объекта может быть один владелец. Нам надо убедиться, что объект уничтожен и освобождён, когда исчезает последняя ссылка на него. Таким образом, нам необходима модель разделённого владения объектом. Допустим, у нас есть синхронная очередь, sync_queue, для общения между задачами. Отправитель и получатель получают по указателю на sync_queue:
void startup()
{
sync_queue* p = new sync_queue{200}; // опасность!
thread t1 {task1,iqueue,p}; // task1 читает из *iqueue и пишет в *p
thread t2 {task2,p,oqueue}; // task2 читает из *p и пишет в *oqueue
t1.detach();
t2.detach();
}
Предполагается, что task1, task2, iqueue и oqueue уже где-то были соответствующим образом определены и прошу прощения за то, что thread переживёт область видимости, где они были созданы (посредством detatch()). Вопрос: кто удалит sync_queue, созданные в startup()? Ответ: тот, кто последний будет использовать sync_queue. Это классический случай, когда требуется сборка мусора. Изначально сборка подсчитывала указатели: нужно хранить количество использований объекта, и в тот момент, когда счётчик обнуляется, удалять его. Множество современных языков работают так, а С++11 поддерживает эту идею через shared_ptr. Пример превращается в:
void startup()
{
auto p = make_shared<sync_queue>(200); // создать sync_queue и вернуть указатель stared_ptr на неё
thread t1 {task1,iqueue,p}; // task1 читает из *iqueue и пишет в *p
thread t2 {task2,p,oqueue}; // task2 читает из *p и пишет в *oqueue
t1.detach();
t2.detach();
}
Теперь деструкторы task1 и task2 могут уничтожить их shared_ptr (и в большинстве правильно построенных систем так и сделают), и последнее, что нужно сделать – уничтожить sync_queue. Это просто и довольно эффективно. Никакой сложной системы. Что важно, она не просто возвращает память, связанную с sync_queue. Она возвращает объект синхронизации (мьютекс, блокировку, что угодно), встроенный в sync_queue, чтобы синхронизировать две нити, выполняющие две задачи. Это не просто управление памятью, это управление ресурсами. Этот «скрытый» объект синхронизации обрабатывается так же, как хендлы файлов и потоков в предыдущем примере. Можно попробовать избавиться от использования shared_ptr, введя уникального владельца в какой-либо области видимости, заключающей в себе задачу, но это не всегда просто сделать – поэтому в С++11 есть и unique_ptr (для одиночного владения) and shared_ptr (для разделённого владения).
4.3 Типобезопасность
Я говорил пока о сборке мусора в контексте управления ресурсами. Но есть ещё и типобезопасность. У нас есть операция delete, которую можно применить неправильно. Пример:
X* p = new X;
X* q = p;
delete p;
// …
q->do_something(); // память, отведённая *p, могла быть перезаписана
Не надо так делать. Непосредственное применение delete опасно и не нужно в обычных случаях. Оставьте удаления классам, управляющим ресурсами — string, ostream, thread, unique_ptr и shared_ptr. Там удаления аккуратно отслеживаются.
4.4 Итог: идеалы управления ресурсами
С моей точки зрения, сборка мусора – последнее средство для управления ресурсами, а не решение задачи и не идеал.
1. Используйте подходящие абстракции, которые рекурсивно и неявно обслуживают свои ресурсы. Отдавайте предпочтение им, а не переменным в определённой области видимости.
2. Когда вам необходимо использовать указатели и ссылки, используйте умные указатели — unique_ptr и shared_ptr
3. Если ничего не помогает (например, ваш код – часть программы, которая запуталась в указателях, и не использует стратегию, поддерживаемую языком, для управления ресурсами и обработки ошибок), попробуйте обрабатывать ресурсы, не относящиеся к памяти, вручную, и включайте сборку мусора для обработки неизбежных утечек памяти.
5. Миф 4: для эффективности необходимо писать низкоуровневый код
Многие верят в то, что эффективный код обязан быть низкоуровневым. Некоторые даже верят, что низкоуровневый код обязательно эффективен. («Если оно такое уродливое, наверняка оно быстрое! Кто-то потратил кучу времени и своего таланта для создания этой штуковины!»). Конечно, можно писать эффективный код на низком уровне, и некоторый код необходимо делать низкоуровневым для работы с машинными ресурсами. Измеряйте, однако, стоит ли это ваших усилий. Современные компиляторы С++ очень эффективные, а архитектура современных машин чрезвычайно сложна. При необходимости такой низкоуровневый код необходимо прятать за интерфейсом для удобства. Часто сокрытие низкоуровневого кода за высокоуровневым интерфейсом способствует оптимизации. Там, где важна эффективность, сначала попробуйте достичь её, выразив идею на высоком уровне, не кидайтесь сразу на биты и указатели.
5.1 qsort() в С
Простой пример. Если вам надо отсортировать набор чисел с плавающей точкой по убыванию, вы могли бы написать для этого код. Но если у вас нет экстремальных требований (например, чисел больше, чем может уместиться в памяти), это было бы наивно. За десятилетия мы сделали библиотеки с алгоритмами сортировки с приемлемой скоростью работы. Мне меньше всего нравится qsort() из стандартной ISO библиотеки C:
int greater(const void* p, const void* q) // трёхстороннее сравнение
{
double x = *(double*)p; // получить значение double с адреса p
double y = *(double*)q;
if (x>y) return 1;
if (x<y) return -1;
return 0;
}
void do_my_sort(double* p, unsigned int n)
{
qsort(p,n,sizeof(*p),greater);
}
int main()
{
double a[500000];
// … fill a …
do_my_sort(a,sizeof(a)/sizeof(*a)); // передать указатель и количество элементов
// …
}
Если вы не программируете на С, или если вы в последнее время не использовали qsort, потребуется кое-что объяснить; qsort принимает 4 аргумента
— указатель на последовательность байтов
— количество элементов
— размер элемента
— функция, сравнивающая два элемента, которые передаются как указатели на их первые байты
Этот интерфейс скрывает информацию. Мы сортируем не байты – мы сортируем double, но qsort этого не знает, поэтому нам надо предоставить информацию о том, как сравнивать double, и сколько байтов в double. Конечно, компилятор знает такие вещи. Но низкоуровневый интерфейс qsort не позволяет компилятору воспользоваться этой информацией. Необходимость указывать такую простую информацию ведёт к ошибкам. Не перепутал ли я два целых аргумента qsort? Если перепутаю, компьютер этого не заметит. Соответствует ли моя compare() соглашениям в C для трёхстороннего сравнения? Если вы посмотрите на промышленную реализацию qsort (рекомендую), вы увидите, сколько усилий приложено для компенсации недостатка информации. К примеру, довольно трудно произвести смену местами элементов, заданных в виде количества байт, чтобы это было так же эффективно, как смена местами пары double. Затратные непрямые вызовы функции сравнения могут быть устранены компилятором только в том случае, если он применит распространение констант для указателей на функции.
5.2 sort() в C++
Сравним qsort с его эквивалентом sort из С++
void do_my_sort(vector<double>& v)
{
sort(v,[](double x, double y) { return x>y; }); // сортировка v по убыванию
}
int main()
{
vector<double> vd;
// … fill vd …
do_my_sort(v);
// …
}
Здесь требуется меньше объяснений. Вектору известен его размер, и нам не надо явно указывать количество элементов. Тип элементов не теряется, и не нужно помнить об их размере. По умолчанию, sort сортирует по возрастанию, поэтому пришлось задать критерий сравнения, как и для qsort. Здесь он передан в качестве лямбда-выражения, сравнивающего два double при помощи >. И так получилось, что эта лямбда тривиальным образом инлайнится всеми компиляторами С++, что я знаю, поэтому сравнение превращается в одну машинную операцию «больше, чем» — никаких неэффективных вызовов функции.
Я использовал контейнерную версию sort, чтобы не задавать итераторы явно, то есть, чтобы не писать:
std::sort(v.begin(),v.end(),[](double x, double y) { return x>y; });
Можно пойти дальше и использовать объект сравнения С++14:
sort(v,greater<>()); // сортировка v по убыванию
Какая из версий быстрее? Можно скомпилировать версию qsort как С, так и С++ без всякий различий в быстродействии, поэтому это будет скорее сравнением стилей программирования, а не языков. Библиотечные реализации используют один алгоритм для sort и qsort, поэтому это сравнение стилей программирования, а не алгоритмов. Конечно, у разных библиотек и компиляторов будут разные результаты, но для каждой реализации будет видна разумная реакция на разные уровни абстракции.
Я недавно прогнал примеры, и увидел, что sort в 2.5 раза быстрее, чем qsort. Это может меняться от компилятора к компилятору и от компьютера к компьютеру, но ни разу у меня qsort не выиграл у sort. Иногда sort выполнялся в 10 раз быстрее. Почему? В стандартной библиотеке С++ sort явно выше уровнем, чем qsort, при этом более гибкий и общий. Он типобезопасен и параметризован на типе хранения, типе элементов и критерию сортировки. Никаких указателей, размеров, байтов. Библиотека STL, к которой принадлежит sort, старается не выбрасывать никакой информации. Это приводит к превосходному инлайнингу и хорошей оптимизации.
Обобщение и высокоуровневый код могут выигрывать у низкоуровневого. Не всегда, но сравнение sort/qsort – это не единичный пример. Всегда начинайте с высокогоуровневой, точной и типобезопасной версии решения. Оптимизируйте по необходимости.
6. Миф 5: С++ предназначен для больших и сложных программ
С++ — объёмный язык. Размер определений схож с С# и Java. Но это не значит, что вам нужно знать каждую деталь, чтобы использовать его, или использовать все функции непосредственно в каждой программе. Вот пример использования основных компонент из стандартной библиотеки:
set<string> get_addresses(istream& is)
{
set<string> addr;
regex pat { R"((\w+([.-]\w+)*)@(\w+([.-]\w+)*))"}; // шаблон е-мейл адреса
smatch m;
for (string s; getline(is,s); ) // прочесть строку
if (regex_search(s, m, pat)) // ищем шаблон
addr.insert(m[0]); // сохраняем адрес в наборе
return addr;
}
Предполагаю, что вы знакомы с регулярками. Если нет – самое время ознакомиться. Заметьте, что я полагаюсь на семантику перемещений, чтобы просто и эффективно вернуть потенциально большой набор строк. Все контейнеры стандартной библиотеки обеспечивают конструкторы перемещения, поэтому нет нужды возиться с new.
Для работы примера требуется включить компоненты:
#include<string>
#include<set>
#include<iostream>
#include<sstream>
#include<regex>
using namespace std;
Проверим:
istringstream test { // инициализируем поток строкой, содержащей адреса
"asasasa\n"
"bs@foo.com\n"
"ms@foo.bar.com$aaa\n"
"ms@foo.bar.com aaa\n"
"asdf bs.ms@x\n"
"$$bs.ms@x$$goo\n"
"cft foo-bar.ff@ss-tt.vv@yy asas"
"qwert\n"
};
int main()
{
auto addr = get_addresses(test); // get the email addresses
for (auto& s : addr) // write out the addresses
cout << s << '\n';
}
Просто пример. Легко можно поменять get_addresses(), чтобы она принимала регулярку как аргумент, чтобы она могла искать URL или что угодно. Легко поменять get_addresses(), чтоб она распознавала больше одного вхождения шаблона в строке. С++ предназначен для гибкости и обобщений, но не каждая программа обязана быть фреймворком. Суть в том, что задача извлечения емейлов из потока просто выражается и просто проверяется.
6.1 Библиотеки
На любом языке писать программу только через встроенные возможности языка (if, for, и +) утомительно. И наоборот, при наличии подходящих библиотек (graphics, route planning, database) любую задачу можно выполнить, приложив разумные усилия. Стандартная ISO библиотека С++ относительно небольшая (по сравнению с коммерческими), но помимо неё есть много библиотек как с исходным кодом, так и коммерческих. К примеру, при помощи библиотек Boost, POCO, AMP, TBB, Cinder, vxWidgets, CGAL сложные вещи становятся проще. К примеру, пусть наша программка извлекает URL с веб-страницы. Для начала, мы обобщим get_addresses() для поиска любой строки, совпадающей с шаблоном.
set<string> get_strings(istream& is, regex pat)
{
set<string> res;
smatch m;
for (string s; getline(is,s); ) // прочесть строку
if (regex_search(s, m, pat))
res.insert(m[0]); // сохранить совпадение в наборе
return res;
}
Это упрощённая версия. Теперь надо как-то прочесть файл из веба. В Boost есть библиотека asio для работы с вебом:
#include <boost/asio.hpp> // подключить boost.asio
Общение с веб-сервером довольно непростое:
int main()
try {
string server = "www.stroustrup.com";
boost::asio::ip::tcp::iostream s {server,"http"}; // установить соединение
connect_to_file(s,server,"C++.html"); // проверить и открыть файл
regex pat {R"((http://)?www([./#\+-]\w*)+)"}; // URL
for (auto x : get_strings(s,pat)) // ищем ссылки
cout << x << '\n';
}
catch (std::exception& e) {
std::cout << "Exception: " << e.what() << "\n";
return 1;
}
При разборе файла www.stroustrup.com/C++.html это даёт:
www-h.eng.cam.ac.uk/help/tpl/languages/C++.html
www.accu.org
www.artima.co/cppsource
www.boost.org
…
Я использовал множество, поэтому URL выводятся по алфавиту.
Я спрятал проверку соединения в connect_to_file():
void connect_to_file(iostream& s, const string& server, const string& file)
// открыть соединение с сервером и открыть файл в s
// пропустить заголовки
{
if (!s)
throw runtime_error{"нет соединения\n"};
// Запросить чтение файла с сервера
s << "GET " << "http://"+server+"/"+file << " HTTP/1.0\r\n";
s << "Host: " << server << "\r\n";
s << "Accept: */*\r\n";
s << "Connection: close\r\n\r\n";
// Проверить ответ:
string http_version;
unsigned int status_code;
s >> http_version >> status_code;
string status_message;
getline(s,status_message);
if (!s || http_version.substr(0, 5) != "HTTP/")
throw runtime_error{ "недопустимый ответ \n" };
if (status_code!=200)
throw runtime_error{ "код статуса в ответе " };
// Выбросить заголовки ответа, которые заканчиваются пустой строкой:
string header;
while (getline(s,header) && header!="\r");
}
Я не писал всё с нуля. Работа с HTTP скопирована с документации по asio.
6.2 Hello, World!
С++ — компилируемый язык, предназначающийся для создания хорошего, обслуживаемого кода, для которого имеет значение быстродействие и надёжность. Он не предназначался для соревнований с интерпретируемыми скриптовыми языками, которые подходят для написания маленьких программ. JavaScript и другие подобные языки часто написаны на С++. Тем не менее, есть много полезных программ на С++, которые занимают всего несколько десятков или сотен строк.
Тут могут помочь авторы библиотек. Вместо того, чтобы концентрироваться на заумных и продвинутых вещах в библиотеках, предоставьте простые примеры “hello, world!”. Сделайте минимальную версию библиотеки, которую легко установить, и пример на одну страничку из того, что она умеет. В тот или иной момент времени мы все оказываемся в роли новичка. Кстати, вот моя версия “hello world” для С++:
#include<iostream>
int main()
{
std::cout << "Hello, World\n";
}
Более длинные и сложные версии кажутся мне менее прикольными.
7 Применения мифов
Часто у мифов есть основание. Каждому из них соответствуют моменты и ситуации, когда в них можно верить на разумном основании, основанном на доказательствах. На сегодняшний день я считаю их абсолютно ложными, простыми недоразумениями, хотя и полученными честным путём. Проблема в том, что мифы всегда служат какой-то цели, или они бы уже вымерли. Эти пять мифов служат разным целям:
— они дают комфорт. Не нужно ничего менять, переоценивать и переосмысливать. Знакомое кажется приятным. Перемены вызывают тревогу, поэтому хорошо, если новинка будет нежизнеспособной.
— можно сэкономить время. Если вам кажется, что вы знаете, что из себя представляет С++, вам не надо тратить время на изучение чего-либо нового, экспериментировать с новыми технологиями, измерять код на быстродействие, тренировать новичков.
— можно не учить С++. Если бы эти мифы были правдой, зачем его вообще нужно было бы учить?
— они помогают продвигать другие языки и технологии – в случае их правдивости это было бы необходимо.
Но они ложны, поэтому аргументы за то, чтобы сохранить всё, как есть, искать альтернативы С++ или избегать современного стиля программирования на нём, нельзя основывать на этих мифах. Существовать с устаревшим представлением о С++ в голове может и комфортно, но при работе с софтом необходимо меняться. Можно достичь большего, чем просто использовать С, С с классами, С++98 и т.д.
Приверженцы «старого, доброго» проигрывают. Затраты на поддержку часто больше, чем на написание современного кода. Старые компиляторы и инструменты обеспечивают меньшее быстродействие и проводят худший анализ, чем современные. Хорошие программисты часто отказываются от работы с антикварным кодом.
Современные версии С++ и технологии программирования, которые он поддерживает, отличаются в лучшую сторону от того представления, которое создают «общепризнанные мифы». Если вы верите в какие-то из них – не верьте мне на слово. Попробуйте, проверьте. Измерьте «старый способ» и альтернативы для актуальной проблемы. Попробуйте освоить новые методы, изучить новые возможности и технологии. Не забывайте сравнивать оценочную стоимость поддержки нового и старого способов. Лучший способ опровержения мифа – это представить доказательство. Я представил вам свои примеры и аргументы.
И я не заявляю, что С++ идеален. Он не идеален, он не является наилучшим языком для всего и для всех. Как и любой другой язык. Воспринимайте его таким, какой он сейчас, а не каким он был 20 лет назад, и не таким, как его выставляет кто-то, кто рекламирует альтернативы. Чтобы сделать рациональный выбор, поищите достоверную информацию, и попробуйте сами понять, как современный С++ справляется с вашими задачами.
8 Итог
Не верьте «общепризнанному» знанию о С++, или бездоказательному его использованию. В этой статье рассматриваются пять популярных мнений о С++ и предлагаются аргументы в пользу того, что они – всего лишь мифы:
1. Чтобы понять С++, сначала нужно выучить С
2. С++ — это объектно-ориентированный язык программирования
3. В надёжных программах необходима сборка мусора
4. Для достижения эффективности необходимо писать низкоуровневый код
5. С++ подходит только для больших и сложных программ
Эти мифы вредны.
9 Обратная связь
Остались сомнения? Сообщите мне, почему. Какие ещё мифы вы встречали? Почему они являются мифами, а не правдой? Какие у вас есть доказательства их разоблачения?
10 Ссылки
1. ISO/IEC 14882:2011 Programming Language C++
2. POCO libraries: pocoproject.org
3. Boost libraries: www.boost.org
4. AMP: C++ Accelerated Massive Parallelism. msdn.microsoft.com/en-us/library/hh265137.aspx
5. TBB: Intel Threading Building Blocks. www.threadingbuildingblocks.org
6. Cinder: A library for professional-quality creative coding. libcinder.org
7. vxWidgets: A Cross-Platform GUI Library. www.wxwidgets.org
8. Cgal — Computational Geometry Algorithms Library. www.cgal.org
9. Christopher Kohlhoff: Boost.Asio documentation. www.boost.org/doc/libs/1_55_0/doc/html/boost_asio.html
10. B. Stroustrup: Software Development for Infrastructure. Computer, vol. 45, no. 1, pp. 47-58, Jan. 2012, doi:10.1109/MC.2011.353.
11. Bjarne Stroustrup: The C++ Programming Language (4th Edition). Addison-Wesley. ISBN 978-0321563842. May 2013.
12. Bjarne Stroustrup: A Tour of C++. Addison Wesley. ISBN 978-0321958310. September 2013.
13. B. Stroustrup: Programming: Principles and Practice using C++ (2nd edition). Addison-Wesley. ISBN 978-0321992789. May 2014.
Послесловие
После публикации статьи на isocpp.org получили разные комментарии. Разрешите мне прокомментировать некоторые из них.
Комментарии подтвердили, что этот материал необходим. Люди повторяют старые аргументы. К сожалению, многие программисты не читают длинные статьи, а короткие отбрасывают за неполноту. Нежелание читать длинные статьи побудило меня написать этот материал и разбить на три части при начальной публикации.
Это не исследовательский материал, который подробно описывает каждую деталь. Как я написал вначале: «Каждому мифу можно посвятить книгу, но я ограничусь простой констатацией и кратким изложением своих аргументов против них».
Тем не менее, многие путают примеры для иллюстрации точки зрения с самой точкой зрения. Некоторые пытались «опровергнуть опровержение», меняя примеры, меняя ограничения примеров, или объявляя примеры тривиальными. Примеры небольшие, т.к. они должны поместиться в небольшую работу. Но они не так уж отличаются от кода, который лежит в основе «настоящих» программ.
Некоторые комментаторы перешли с С++11/С++14, на которых я основывал свою аргументацию, к более старым версиям. С++14 – не С++ из 1980-х. Эти стандарты уже не такие, каким учились большинство людей. И не то, чему учатся на сегодняшних курсах. И не то, что люди видят, разглядывая довольно объёмные тексты существующих программ. Я хочу поменять это представление. Если вам не удаётся работать с моими примерами в какой-либо антикварной версии С++ или со старым компилятором – это плохо, но сегодня есть улучшенные версии всех основных компиляторов (и обычно бесплатные). В моих примерах не было никакого ультрасовременного кода.
Каждый язык программирования, достигающий успеха, сталкивается с проблемами старого кода. Не судите С++ по технологиям программирования 20-летней давности или компиляторам 10-летней давности. Взгляните на современный С++ и попробуйте воспользоваться новыми возможностями, как это уже удалось многим. Сегодня вы почти наверняка пользовались программой, написанной на С++11. Между моим и вашим компьютером очень много шагов, на которых встречаются программы на С++11.
Довольно много комментариев содержат заявления вроде «а в языке Х есть точно такая же возможность» или «библиотека Y в языке X делает именно это». Очевидно, если у вас есть язык, в котором проще, чем в С++, решить вашу задачу, при этом не теряя критично в быстродействии, переносимости и не приобретая ненужных ограничений – используйте его. Но ни один язык или библиотека не подходят идеально для всех и всего.
Я предоставил примеры для общих задачи и общих технологий. Сравнивать что-то с одним примером не особенно нужно. Моя точка зрения относится к общим вещам, а примеры – просто иллюстрации. При использовании достаточно хорошей библиотеки любой язык будет простым и приятным. Для достаточно ограниченной задачи можно сконструировать специальный язык, который будет элегантнее языка общего назначения. К примеру, библиотека asio, которую я использовал в пункте 6.1 – гибкая, эффективная сетевая библиотека общего назначения. Для любой задачи её можно обернуть в простую функцию (или небольшой набор функций), чтобы сделать её более удобной. И мой код был бы реализацией этого. То, что я пытался объяснить в п.6.2 – сообщество программистов С++ могли бы помочь программистам, проведя побольше времени над тем, чтобы делать простые вещи более простыми. Например, в 99% случаев я использую sort(v) вместо sort(v.begin(),v.end()).
Быстродействие
Мои комментарии вызвали небольшую бурю. Многие пытались опровергать их простыми возражениями. Я не принимаю аргументы по быстродействию, не подкреплённые данными о тестировании. Мои комментарии были подтверждены реальными измерениями в разных ситуациях на протяжении нескольких лет. Многие из них описаны в книгах. Они правдивы для широкого спектра схожих примеров.
Я имею в виду современную реализацию С++, соответствующую стандартам. К примеру, когда я пишу про быстродействие оптимизации коротких строк, я не имею в виду реализации С++ до С++11. Я не рассматриваю комментарии на тему, что std::sort() или std::string работают медленно без использования оптимизатора. Разумеется – но глупо обсуждать быстродействие неоптимизированного кода. При использовании GCC или Clang используйте –O2; для продуктов от Microsoft используйте release mode.
Я неплохо знаю С и его стандартную библиотеку. Я написал много кода на С ещё до того, как сегодняшние студенты родились, и многое привнёс в язык: прототипирование, константы, инлайнинг, декларации в for, декларации как объявления, и многое другое. Я следил за его развитием и эволюционированием.
Да, C-версия compose() не проверяет значение, возвращаемое malloc(). Я же спрашивал у вас, всё ли я правильно сделал. Я намеренно не предоставил вам код, годный для продакшена. Отсутствие проверки результата – один из основных источников ошибок, поэтому моя «ошибка» специально была сделана для иллюстрации этого. В данном случае часто помогают исключения. Конечно, можно было написать С-версию compose(), используя менее известные функции стандартной библиотеки, и да, можно было избежать свободного хранения, если позволить вызывающему коду передать размещённый в стеке буфер и позволить вызывающему разбираться с проблемой строковых аргументов, которые бы его переполнили. Тем не менее, эти альтернативы не относятся к главному вопросу: такой код писать сложнее, чем в С++, и ещё сложнее писать его правильно. Новички с первого раза пишут версию для С++, но не для С, особенно для тех версий, которые основаны на функциях из стандартной библиотеки, которым новичков не обучают.
С++ использовался в критических и высоконагруженных встраиваемых системах годами – в тех же марсианских Роверах (анализ обстановки и автономная работа), F-35, F-16 (системы управления полётом), и множестве других: www.stroustrup.com/applications.html. И да, космическая капсула Орион запрограммирована при помощи С++.
Библиотеки
Да, библиотеки разнятся по качеству, и иногда сложно выбрать нестандартную библиотеку из множества вариантов. Это проблема. Но эти библиотеки существуют, и их исследование часто более продуктивно, чем простое движение вперёд напролом, кончающееся изобретением очередного колеса.
К сожалению, часто библиотеки С++ не разрабатываются с учётом совместной работы с другими. И нет одного места, где можно было бы брать все библиотеки. Я годами наблюдал за процессом обучения студентов по схеме «сначала С», и читал эти программы десятилетиями. Тысячам людей я преподавал С++ в качестве первого языка. Мои заявления о возможности обучению С++ основаны на большом опыте.
С++ обучать легче, чем С из-за более хорошей системы типов и синтаксиса. Необходимо учить меньше трюков и костылей. Представьте, как бы вы стали учить стилю программирования на С, обучая языку С++. Я бы никогда не стал давать новичкам курс С++, который бы:
— не содержал хорошей основы касаемо работы с памятью, указателями, и т.д.
— не давал студентам представления о «чистом С» и о его использовании
— не обосновывал большинство возможностей языка
— пытался бы обучить абсолютно всем техникам С++
Хорошие учителя, преподающие С, не пытаются научить новичков всем техникам.
www.stroustrup.com/programming.html — мой ответ на вопрос «Как бы вы обучали новичков С++?». Эта система работает.
Можно ознакомиться с моей довольно старой работой по некоторым аспектам преподавания С и С++: Learning Standard C++ as a New Language. C/C++ Users Journal. pp 43-54. May 1999 (www.stroustrup.com/papers.html).
Сегодня я бы сделал С-версию курса получше, а С++ — сильно лучше. Примеры отражают стиль программирования того времени (и были рассмотрены экспертами по программированию на С и С++).
Сегодняшний С++ — это стандарт ISO С++14, а не то, что я описывал 30 лет назад, и не то, что ваш преподаватель рассказывал вам 20 лет назад. Изучите C++11/C++14 в том виде, в каком они поддерживаются основными компиляторами, и привыкните к ним. Это гораздо лучший инструмент, нежели ранние версии С++. Сегодняшний С – это стандарт ISO С11, а не K&R C (хотя я не уверен, соответствуют ли сегодняшние компиляторы стандарту С11 так же хорошо, как компиляторы С++ стандарту С++14). Меня шокируют некоторые вещи, которые сегодня преподают под видом «правильного С++».
С++ — это не ООП-язык. Это язык, поддерживающий ООП, другие техники программирования, и их комбинации. Если вы – опытный программист, я рекомендую прочесть A Tour of C++ в качестве быстрого обзора современного языка C++.