Привет, Хабр! Меня зовут Георгий Осипов. Я работаю в МГУ и компании Яндекс, а также в команде курса «Разработчик С++» Яндекс Практикума. В этой статье я поделюсь своими мыслями о том, почему немолодой язык С++ до сих пор не теряет актуальности.
Кажется, что первое доказательство — новость 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++ хоть и старый язык, но вовсе не устаревший.