Почему C++ не устаревает

—

от автора

Привет, Хабр! Меня зовут Георгий Осипов. Я работаю в МГУ и компании Яндекс, а также в команде курса «Разработчик С++» Яндекс Практикума. В этой статье я поделюсь своими мыслями о том, почему немолодой язык С++ до сих пор не теряет актуальности.

Кажется, что первое доказательство — новость 2022 года, когда компания Google анонсировала новый язык Carbon. Он должен стать альтернативой C++. Первая версия Carbon выйдет только через 2-3 года, но уже сейчас понятно — если C++ языку ищут замену, значит, её нет.

Разберёмся, что же делает язык с 40-летней историей таким популярным и почему сегодня он только укрепляет позиции: в 2022 году C++ занял первое место среди быстрорастущих языков по версии TIOBE.

C++ и его стандарты

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

Новый Стандарт языка выходит каждые три года. Особенность в том, как именно принимаются изменения. Каждое нововведение проходит через обсуждения и голосования в международном комитете. В итоге в стандарт попадают только тщательно выверенные изменения.

Следующее крупное обновление запланировано уже на конец текущего года. Можно сказать, что C++ действительно отставал от некоторых современных языков в плане возможностей, но верно нагоняет их. Многие претензии, которые высказывали к C++, потеряли актуальность.

Рассмотрим некоторые претензии, которые часто предъявляются к C++

Претензия 1: C++ имеет слабую стандартную библиотеку

Отчасти эта претензия правомерна. Но ситуация улучшается.

Чтобы показать это, обратимся к другому популярному языку — Python. Рассмотрим одну из его замечательных возможностей — генератор списка (англ. list comprehension). Он позволяет одним выражением выбрать из списка все четные элементы и поделить их на два. Делается это так:

# смысл — положить в новый список x // 2 (половина x) # для всех x из списка list, если x делится на 2 [x // 2 for x in list if x % 2 == 0]

Ещё несколько лет назад в C++ ничего подобного не было. Но сейчас можно использовать std::ranges:

namespace view = std::views; auto even = [](int i) { return i % 2 == 0; }; auto half = [](int i) { return i / 2; }; auto range = view::all(list) |               view::filter(even) |              view::transform(half);

Немного сложнее, но смысл передаётся так же хорошо. Эта возможность была добавлена в стандартную библиотеку в 2020 году.

Как правило, Python не рассматривают в качестве конкурента C++, эти языки используются для разных целей. Но пример показывает, как растёт C++, впитывая лучшее из разных языков. Также в стандартной библиотеке появились средства для синхронизации потоков, работы с регулярными выражениями, календарём и часами, файловой системой, многопоточными алгоритмами.

Одна из самых ожидаемых возможностей C++ — работа с сетью. Сетевые приложения в C++ можно написать, только используя сторонние библиотеки. Комитет по стандартизации упорно работает, но пока не удаётся преодолеть все проблемы, чтобы построить идеальный сетевой фреймворк.

Претензия 2: в C++ много избыточных копирований

Объекты в программировании часто передаются, сохраняются, возвращаются.

Как сохраняет объект C++: копирует объект.

Как сохраняют объекты другие языки: сохраняют не сам объект, а ссылку на объект.

Для больших объектов операция копирования может быть очень затратной.

Такое поведение в C++ имеет свои причины. Главные из которых — использование стека и принципиальное отсутствие сборщика мусора. В C++ есть целый ряд техник, способных решить эту проблему. Это специальные оптимизации, ссылки, умные указатели, и самая интересная — перемещение.

Перемещение — это лёгкая операция, появившаяся в C++ в 2011 году, которая часто может заменить копирование. Перемещение применимо в случаях, когда нужна копия объекта, а оригинал больше не понадобится. Оказывается, это подавляющее большинство случаев.

class Schoolmates { public:     Schoolmates(std::vector<Students> students)         // перемещаем вектор, вместо того, чтобы копировать его         : students_(std::move(students)) {}  private:     std::vector<Students> students_; }; 

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

Претензия 3: в C++ отсутствует сборщик мусора, приходится вручную управлять памятью

Претензия состоит из двух частей:

  • В C++ отсутствует сборщик мусора — это правда.
  • Приходится вручную управлять памятью — это неправда.

Современный программист на C++ имеет множество инструментов, позволяющих контролировать память автоматически. Среди них контейнеры, автоматические переменные и умные указатели. Они, конечно, имеют некоторые недостатки, но недостатки потенциального сборщика мусора сильно бы перевесили. Например, сборщик мусора иногда хочет «остановить мир» — заблокировать выполнение сразу всех потоков программы, чтобы безопасно собрать неиспользуемые объекты. Со сборщиком мусора была бы невозможна популярная идиома RAII — благодаря ей автоматически и в нужный момент выполняется освобождение ресурсов.
Например, рассмотрим логирующий класс для записи информации в файл на Python:

class Logger:     def __init__(self, file):         self.file_obj = open(file, "w")      def log(self, message):         current_time = datetime.datetime.now()         formatted_timestamp = current_time.strftime("%Y-%m-%d %H:%M:%S")         self.file_obj.write(f"{formatted_timestamp}: {message}\n")      def close(self):         self.file_obj.close() 

А теперь рассмотрим похожий класс на C++:

class Logger { public:     Logger(std::filesystem::path file_name) : file_obj(file_name) {     }      void log(std::string_view message) {         auto current_time = std::chrono::system_clock::now();         file_obj << std::format("{:%Y-%m-%d %H:%M:%S} {}\n", current_time, message); }  private:     std::ofstream file_obj; }; 

Главное отличие в методе close — в C++ он не нужен. В Python вы обязаны самостоятельно позаботиться о закрытии файла. Иначе он будет занимать системный идентификатор даже после того, как лог перестал использоваться, и до тех пор, пока сборщик мусора не решит удалить этот объект. А это может произойти относительно нескоро.

В C++ закрытие произойдёт автоматически, потому что момент удаления объекта строго определён. Это достигается благодаря отсутствию сборщика мусора.

Претензия 4: в C++ ужасный ввод-вывод

Действительно, в Python вы можете написать так:

print(f"x = {x}, y = {y}, x + y = {x + y}"); 

В C++ ничего подобного нет. Но если вы не в курсе последних новостей, то, вероятно, не знаете, что 38-летие язык отмечает с добавлением стандартной функции print, имеющей замечательные возможности:

std::print("x = {}, y = {}, x + y = {}", x, y, x + y); 

Шаблон будет разобран на этапе компиляции. Ещё одно приятное дополнение — эта функция корректно работает с кодировкой UTF-8, а значит, консольные приложения наконец-то смогут выводить текст на русском или любом другом языке.

Претензия 5: в C++ вы потратите часы на поиск, подключение и сборку нужных зависимостей

Вероятно, это одна из самых серьёзных и обоснованных претензий. Нельзя написать что-то подобное команде pip install opencv-python и затем свободно использовать пакет opencv в любом проекте, просто импортировав его. Традиционный способ установки пакетов в C++:

  • найти репозиторий,
  • подобрать нужные настройки и конфигурировать пакет,
  • собрать библиотеку,
  • сделать библиотеку видимой в нужном проекте.

Такой способ не просто сложен. Главный недостаток — его неудобно автоматизировать. Придётся повторять все эти шаги при обновлении библиотеки.

Но и тут время не стоит на месте. В C++ появляются менеджеры пакетов. Например, conan. Он позволяет автоматизировать эти шаги. Но сам по себе требует определённой сноровки и в удобстве пока уступает pip. Однако даже в этой области ситуация постепенно улучшается.

Претензия 6: C++ сложен в изучении. Даже не беритесь за него, если у вас нет диплома в области математики или программирования

И снова претензия из двух частей:

  • C++ сложен в изучении — это правда.
  • Доступен только дипломированным специалистам — это неправда.

Опыт Яндекс Практикума показывает, что стать C++-разработчиком может каждый. Но стоит признать: порог входа в язык достаточно высок. C++ растёт и усложняется, и у этого процесса есть две стороны. Язык становится больше, и его труднее учить. С другой стороны, он становится выразительнее, многие основные операции описываются более наглядно и понятно.

Однако даже в сложности языка есть преимущество. Освоив C++ вам будет проще перейти на другой язык.

Вывод: претензии к С++ теряют актуальность, потому что язык всё время обновляется.

Сильные стороны С++:

Рассмотрим несколько особенностей C++, выделяющих его на фоне большинства других языков.

Нулевой оверхед

C++ славится высокой производительностью. Проверим, так ли он хорош. В качестве конкурента рассмотрим популярный язык Java. Но вместо того, чтобы сравнивать C++ и Java друг с другом, сравним эти два языка с самими собой.

Для примера возьмём популярную конструкцию — цикл по диапазону. Он позволяет обработать массив из множества элементов. Такой вид циклов есть во всех современных языках. Не стали исключением C++ и Java.

Рассмотрим цикл, который суммирует элементы массива. В C++ и Java он записывается одинаково:

// C++ #1 // Java #1 for(int i : array) {     sum += i; }

Если отказаться от синтаксического сахара, то цикл можно переписать, используя более примитивные конструкции:

// C++ #2 for (int j = 0; j < array.size(); j++) {     sum += array[j]; } 

// Java #2 for (int j = 0; j < array.size(); j++) {     sum += array.get(j); } 

Такой вариант сложнее писать и проще допустить ошибку. Но что с производительностью? Сравним производительность этих вариантов в каждом языке. Для Java возьмём следующий пример. Запустив его, увидим подобную картину:

Цикл по диапазону — 46 ms
Простой счётчик — 7 ms

Простой счётчик работает в 6 раз быстрее. Это говорит о том, что в Java за удобство нужно платить. Программисты в таких случаях говорят, что цикл по диапазону в Java имеет оверхед.

Теперь рассмотрим C++. Запустим бенчмарк на специальном сервисе. В нём мы добавили ещё два варианта и получили такой результат:

Все варианты цикла работают одинаково быстро!

Вот описание версий, которые мы сравнили:

  • ForEach — цикл по диапазону. Аналог цикла, работающего медленно в Java.
  • WithSize — простой цикл со счётчиком.
  • WithIterators — сложный цикл, в котором вместо счётчика используются итераторы. Это то, как работает цикл по диапазону, но без синтаксического сахара.
  • RawMemory — цикл, в котором используется прямой доступ к памяти. Ничего лишнего, он должен быть самым быстрым.

C++ смог свести весь оверхед к нулю и уравнять производительность самой сложной и самой простой версий. Удивительно, что сложные операции создания и перемещения итераторов ничего не стоят.

Секрет в мощном оптимизаторе. C++ выполняет огромное количество вычислений на этапе компиляции. При этом он разворачивает функции, убирает лишние переменные и вычисления.

В результате сложные и элегантные языковые конструкции при компиляции превращаются в простой и эффективный машинный код. В этом сила C++.

Шаблонное программирование

Сильные стороны C++ — вычисления времени компиляции и шаблонное программирование. Чем больше вычислит компилятор, тем меньше придётся вычислять у пользователя, тем быстрее будет работать программа.

Шаблонное программирование позволяет использовать общий код — шаблон — который может настраиваться на разные параметры. Это не только удобно, но и крайне эффективно: будет генерироваться очень быстрый машинный код, учитывающий специфику конкретного случая.

Один и тот же шаблон может сгенерировать несколько вариантов кода, оптимально работающих в разных ситуациях

Настоящие чудеса начинаются, когда мы используем функциональный объект как параметр шаблона. Рассмотрим пример. Два варианта одного и того же цикла на Python:

import time  # запускаем обычный цикл million = range(1000000) target = [] start_time = time.time() for x in million:     if x % 2 == 0:         target.append(x // 2) elapsed_time = time.time() - start_time print(f"Loop time: {elapsed_time:.6f} seconds")  # используем list comprehension с функциями million = range(1000000) start_time = time.time() even = lambda x: x % 2 == 0 half = lambda x: x // 2 target = [half(x) for x in million if even(x)] elapsed_time = time.time() - start_time print(f"Comprehension time: {elapsed_time:.6f} seconds") 

В первом варианте мы используем обычный цикл. Во втором list comprehension в сочетании с лямбда-функциями. Python-программист, скорее всего, сразу скажет: разумеется, второй вариант будет работать дольше, ведь вызов функции — дорогая операция! И окажется прав. Запустим этот код и увидим подобный результат:

Loop time: 0.119847 seconds
Comprehension time: 0.179655 seconds

C++ не исключение. В нём тоже вызов функции требует накладных расходов. Но попробуем запустить бенчмарк, аналогичный примеру из Python’а. Удивительно, но версия, миллион раз вызывающая функции, не уступает варианту без них. Дело в том, что C++ полностью исключил вызовы функций при оптимизации. И это стало возможно именно благодаря шаблонной магии. Компилятор выполнял настройку автоматически выведенных типов в таком выражении:

   view::filter(even) |    view::transform(half)

Каждая из этих функций возвращает сложный обобщённый объект из библиотеки ranges. Затем эти объекты комбинируются операцией “|”. При этом компилятор знает, какие три действия выполняются, и даже то, что именно делают вызываемые лямбда-функции. Благодаря этому стала возможной комплексная оптимизация. Таким образом, из трёх отдельных операций и двух лямбда-функций компилятор чудесным образом построил код, вообще не имеющий вызовов функций.

Заключение

Мы не преследовали цель показать, что C++ лучше Python или C++ лучше Java. Каждый из языков имеет свою нишу и своё назначение. C++ пока что не превзойдён во многих отраслях, особенно требующих высокопроизводительных вычислений. У него появляются конкуренты: такие языки, как Go, Rust, тоже способны показывать высокую производительность. Но C++ растёт высокими темпами и осваивает новые рубежи. Например, компания Яндекс активно развивает фреймворк userver и переводит на C++ свои сервисы. Всё это позволяет сделать вывод: C++ хоть и старый язык, но вовсе не устаревший.


ссылка на оригинал статьи https://habr.com/ru/articles/758744/