Привет, Хабр! Меня зовут Георгий Осипов. Я работаю в МГУ и компании Яндекс, а также в команде курса «Разработчик С++» Яндекс Практикума. В этой статье я поделюсь своими мыслями о том, почему немолодой язык С++ до сих пор не теряет актуальности. Кажется, что первое доказательство — новость 2022 года, когда компания Google анонсировала новый язык Carbon. Он должен стать альтернативой C++. Первая версия Carbon выйдет только через 2-3 года, но уже сейчас понятно — если C++ языку ищут замену, значит, её нет. Разберёмся, что же делает язык с 40-летней историей таким популярным и почему сегодня он только укрепляет позиции: в 2022 году C++ занял первое место среди быстрорастущих языков по версии TIOBE.  C++ проделал немалый путь. Родившись надстройкой над более простым языком C, он пережил несколько крупных обновлений, которые изменили его до неузнаваемости. Эти обновления сделали C++ современным языком, учитывающим новейшие тенденции программирования. Новый Стандарт языка выходит каждые три года. Особенность в том, как именно принимаются изменения. Каждое нововведение проходит через обсуждения и голосования в международном комитете. В итоге в стандарт попадают только тщательно выверенные изменения. Следующее крупное обновление запланировано уже на конец текущего года. Можно сказать, что C++ действительно отставал от некоторых современных языков в плане возможностей, но верно нагоняет их. Многие претензии, которые высказывали к C++, потеряли актуальность. Отчасти эта претензия правомерна. Но ситуация улучшается. Чтобы показать это, обратимся к другому популярному языку — Python. Рассмотрим одну из его замечательных возможностей — генератор списка (англ. list comprehension). Он позволяет одним выражением выбрать из списка все четные элементы и поделить их на два. Делается это так: Ещё несколько лет назад в C++ ничего подобного не было. Но сейчас можно использовать std::ranges: Немного сложнее, но смысл передаётся так же хорошо. Эта возможность была добавлена в стандартную библиотеку в 2020 году. Как правило, Python не рассматривают в качестве конкурента C++, эти языки используются для разных целей. Но пример показывает, как растёт C++, впитывая лучшее из разных языков. Также в стандартной библиотеке появились средства для синхронизации потоков, работы с регулярными выражениями, календарём и часами, файловой системой, многопоточными алгоритмами. Одна из самых ожидаемых возможностей C++ — работа с сетью. Сетевые приложения в C++ можно написать, только используя сторонние библиотеки. Комитет по стандартизации упорно работает, но пока не удаётся преодолеть все проблемы, чтобы построить идеальный сетевой фреймворк. Объекты в программировании часто передаются, сохраняются, возвращаются.  Как сохраняет объект C++: копирует объект. Как сохраняют объекты другие языки: сохраняют не сам объект, а ссылку на объект. Для больших объектов операция копирования может быть очень затратной.  Такое поведение в C++ имеет свои причины. Главные из которых — использование стека и принципиальное отсутствие сборщика мусора. В C++ есть целый ряд техник, способных решить эту проблему. Это специальные оптимизации, ссылки, умные указатели, и самая интересная — перемещение. Перемещение — это лёгкая операция, появившаяся в C++ в 2011 году, которая часто может заменить копирование. Перемещение применимо в случаях, когда нужна копия объекта, а оригинал больше не понадобится. Оказывается, это подавляющее большинство случаев. Перемещение в сочетании с умными указателями позволяет решить проблему копирований даже для объектов, не поддерживающих перемещение, делая претензию к C++ неактуальной. Претензия состоит из двух частей: Современный программист на C++ имеет множество инструментов, позволяющих контролировать память автоматически. Среди них контейнеры, автоматические переменные и умные указатели. Они, конечно, имеют некоторые недостатки, но недостатки потенциального сборщика мусора сильно бы перевесили. Например, сборщик мусора иногда хочет «остановить мир» — заблокировать выполнение сразу всех потоков программы, чтобы безопасно собрать неиспользуемые объекты. Со сборщиком мусора была бы невозможна популярная идиома RAII — благодаря ей автоматически и в нужный момент выполняется освобождение ресурсов.
Например, рассмотрим логирующий класс для записи информации в файл на Python: А теперь рассмотрим похожий класс на C++: Главное отличие в методе close — в C++ он не нужен. В Python вы обязаны самостоятельно позаботиться о закрытии файла. Иначе он будет занимать системный идентификатор даже после того, как лог перестал использоваться, и до тех пор, пока сборщик мусора не решит удалить этот объект. А это может произойти относительно нескоро. В C++ закрытие произойдёт автоматически, потому что момент удаления объекта строго определён. Это достигается благодаря отсутствию сборщика мусора. Действительно, в Python вы можете написать так: В C++ ничего подобного нет. Но если вы не в курсе последних новостей, то, вероятно, не знаете, что 38-летие язык отмечает с добавлением стандартной функции print, имеющей замечательные возможности: Шаблон будет разобран на этапе компиляции. Ещё одно приятное дополнение — эта функция корректно работает с кодировкой UTF-8, а значит, консольные приложения наконец-то смогут выводить текст на русском или любом другом языке. Вероятно, это одна из самых серьёзных и обоснованных претензий. Нельзя написать что-то подобное команде pip install opencv-python и затем свободно использовать пакет opencv в любом проекте, просто импортировав его. Традиционный способ установки пакетов в C++: Такой способ не просто сложен. Главный недостаток — его неудобно автоматизировать. Придётся повторять все эти шаги при обновлении библиотеки. Но и тут время не стоит на месте. В C++ появляются менеджеры пакетов. Например, conan. Он позволяет автоматизировать эти шаги. Но сам по себе требует определённой сноровки и в удобстве пока уступает pip. Однако даже в этой области ситуация постепенно улучшается. И снова претензия из двух частей: Опыт Яндекс Практикума показывает, что стать C++-разработчиком может каждый. Но стоит признать: порог входа в язык достаточно высок. C++ растёт и усложняется, и у этого процесса есть две стороны. Язык становится больше, и его труднее учить. С другой стороны, он становится выразительнее, многие основные операции описываются более наглядно и понятно. Однако даже в сложности языка есть преимущество. Освоив C++ вам будет проще перейти на другой язык. Вывод: претензии к С++ теряют актуальность, потому что язык всё время обновляется. Рассмотрим несколько особенностей C++, выделяющих его на фоне большинства других языков. C++ славится высокой производительностью. Проверим, так ли он хорош. В качестве конкурента рассмотрим популярный язык Java. Но вместо того, чтобы сравнивать C++ и Java друг с другом, сравним эти два языка с самими собой. Для примера возьмём популярную конструкцию — цикл по диапазону. Он позволяет обработать массив из множества элементов. Такой вид циклов есть во всех современных языках. Не стали исключением C++ и Java. Рассмотрим цикл, который суммирует элементы массива. В C++ и Java он записывается одинаково: Если отказаться от синтаксического сахара, то цикл можно переписать, используя более примитивные конструкции: Такой вариант сложнее писать и проще допустить ошибку. Но что с производительностью? Сравним производительность этих вариантов в каждом языке. Для Java возьмём следующий пример. Запустив его, увидим подобную картину: Цикл по диапазону — 46 ms
Простой счётчик — 7 ms Простой счётчик работает в 6 раз быстрее. Это говорит о том, что в Java за удобство нужно платить. Программисты в таких случаях говорят, что цикл по диапазону в Java имеет оверхед. Теперь рассмотрим C++. Запустим бенчмарк на специальном сервисе. В нём мы добавили ещё два варианта и получили такой результат: Все варианты цикла работают одинаково быстро! Вот описание версий, которые мы сравнили: C++ смог свести весь оверхед к нулю и уравнять производительность самой сложной и самой простой версий. Удивительно, что сложные операции создания и перемещения итераторов ничего не стоят. Секрет в мощном оптимизаторе. C++ выполняет огромное количество вычислений на этапе компиляции. При этом он разворачивает функции, убирает лишние переменные и вычисления.  В результате сложные и элегантные языковые конструкции при компиляции превращаются в простой и эффективный машинный код. В этом сила C++. Сильные стороны C++ — вычисления времени компиляции и шаблонное программирование. Чем больше вычислит компилятор, тем меньше придётся вычислять у пользователя, тем быстрее будет работать программа.   Шаблонное программирование позволяет использовать общий код — шаблон — который может настраиваться на разные параметры. Это не только удобно, но и крайне эффективно: будет генерироваться очень быстрый машинный код, учитывающий специфику конкретного случая. Один и тот же шаблон может сгенерировать несколько вариантов кода, оптимально работающих в разных ситуациях Настоящие чудеса начинаются, когда мы используем функциональный объект как параметр шаблона. Рассмотрим пример. Два варианта одного и того же цикла на Python: В первом варианте мы используем обычный цикл. Во втором list comprehension в сочетании с лямбда-функциями. Python-программист, скорее всего, сразу скажет: разумеется, второй вариант будет работать дольше, ведь вызов функции — дорогая операция! И окажется прав. Запустим этот код и увидим подобный результат: Loop time: 0.119847 seconds
Comprehension time: 0.179655 seconds C++ не исключение. В нём тоже вызов функции требует накладных расходов. Но попробуем запустить бенчмарк, аналогичный примеру из Python’а. Удивительно, но версия, миллион раз вызывающая функции, не уступает варианту без них. Дело в том, что C++ полностью исключил вызовы функций при оптимизации. И это стало возможно именно благодаря шаблонной магии. Компилятор выполнял настройку автоматически выведенных типов в таком выражении: Каждая из этих функций возвращает сложный обобщённый объект из библиотеки ranges. Затем эти объекты комбинируются операцией “|”. При этом компилятор знает, какие три действия выполняются, и даже то, что именно делают вызываемые лямбда-функции. Благодаря этому стала возможной комплексная оптимизация. Таким образом, из трёх отдельных операций и двух лямбда-функций компилятор чудесным образом построил код, вообще не имеющий вызовов функций. Мы не преследовали цель показать, что C++ лучше Python или C++ лучше Java. Каждый из языков имеет свою нишу и своё назначение. C++ пока что не превзойдён во многих отраслях, особенно требующих высокопроизводительных вычислений. У него появляются конкуренты: такие языки, как Go, Rust, тоже способны показывать высокую производительность. Но C++ растёт высокими темпами и осваивает новые рубежи. Например, компания Яндекс активно развивает фреймворк userver и переводит на C++ свои сервисы. Всё это позволяет сделать вывод: C++ хоть и старый язык, но вовсе не устаревший.