Перейти к основному содержимому

C++ — углублённые темы

Разработчику Архитектору

Продолжение обзора C++ — синтаксис, стандартная библиотека, инструменты и смежные темы.


Как использовать этот материал на практике

Эта статья полезнее всего в формате "теория -> маленький эксперимент -> фиксация вывода":

  1. Берёте один раздел (например, модули или модель памяти).
  2. Пишете минимальный пример на 30-50 строк.
  3. Проверяете поведение в отладке и профилировщике.
  4. Формулируете правило для своего проекта ("в каких случаях применяем").

Так знание превращается в инженерную привычку, а не остаётся академическим списком терминов.


Синтаксис

Хотя лексика C++ унаследована от C (точки с запятой, фигурные скобки, операторы +, -, *, /), семантика многих конструкций принципиально иная. Рассмотрим ключевые отличия на уровне восприятия кода.


Пространства имён

Пространство имён (namespace) — это лексическая область видимости, вводимая для избежания коллизий имён. В отличие от Java, где иерархия пакетов отражается в файловой структуре и имени класса (com.example.Foo), в C++ пространство имён не влияет на ABI и линковку: std::vector<int> и mylib::vector<int> — это совершенно разные типы, даже если реализации идентичны.

Критически важно: пространства имён можно расширять. В одном заголовке можно написать:

namespace graphics {
class Point { /* ... */ };
}

Разбор:

  • Фрагмент объявляет пространство имён graphics, которое изолирует тип Point от конфликтов имён в других модулях.
  • class Point внутри namespace получает полное имя graphics::Point, и это имя используют при обращении извне.
  • Такой приём упорядочивает крупный код: типы группируются по подсистемам, а не по "глобальному" пространству.
  • В контексте C++ это основа масштабируемой структуры API библиотек и внутренних модулей.
  • Код также подготавливает почву к расширению пространства имён в других файлах.

а в другом —

namespace graphics {
class Color { /* ... */ };
}

Разбор:

  • Второй блок расширяет уже существующее пространство имён graphics, не создавая новое.
  • Point и Color оказываются в одном логическом модуле и доступны через единый префикс graphics::.
  • Этот механизм позволяет распределять объявления по разным заголовкам без потери целостности API.
  • Подход особенно полезен при разделении проекта по функциональным областям.
  • Важно держать согласованную структуру имён, чтобы пользователю библиотеки было проще навигировать по типам.

и оба объявления отнесутся к одному и тому же graphics. Это позволяет разделять интерфейсы по функциональности, не привязываясь к файловой структуре.


Перегрузка операторов

В C++ операторы — это функции-члены или свободные функции с особым именем (operator+, operator<< и т.д.). Это означает:

  • вы можете определить смысл + для своих типов;
  • << и >> для std::ostream/std::istream — это обычные функции, перегруженные в <iostream>;
  • компилятор разрешает, какую версию оператора вызывать, на основе типов операндов (перегрузка по типам).

Это даёт гибкость, но требует дисциплины — нельзя перегружать операторы так, чтобы нарушалась их "естественная" семантика (например, a + b не должно изменять a).


Классы

Самая важная роль класса в C++ — инкапсуляция ресурсов через RAII (Resource Acquisition Is Initialization). Рассмотрим:

class FileHandle {
FILE* fp;
public:
FileHandle(const char* name) : fp(fopen(name, "r")) {}
~FileHandle() { if (fp) fclose(fp); }
// запрещаем копирование по умолчанию
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};

Разбор:

  • Класс FileHandle реализует RAII-обёртку вокруг FILE* и инкапсулирует работу с файловым ресурсом.
  • Конструктор открывает файл через fopen, а деструктор гарантированно закрывает его через fclose.
  • Запрет копирования (= delete) исключает ситуацию двух владельцев одного дескриптора.
  • Такой дизайн делает управление ресурсом детерминированным и защищает от утечек на ошибках.
  • Это классический шаблон проектирования для внешних ресурсов в C++.

Здесь деструктор гарантирует освобождение ресурса автоматически, при выходе из области видимости. Это работает независимо от исключений — если в функции, использующей FileHandle, произойдёт throw, деструктор вызовется в процессе раскрутки стека. Именно RAII — основа безопасности C++ в условиях отсутствия сборщика мусора.

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • Пример демонстрирует жизненный цикл объекта DatabaseConnection и автоматический cleanup при исключениях.
  • Конструктор сообщает об открытии подключения, деструктор — о закрытии; это наглядная иллюстрация RAII.
  • В process_data() выбрасывается исключение после создания conn, но деструктор всё равно вызывается при раскрутке стека.
  • Блок try/catch в main показывает, где ошибка перехватывается и преобразуется в контролируемый вывод.
  • Фрагмент подтверждает, что корректный деструктор обеспечивает устойчивость к аварийному выходу из функции.

Результат выполнения: Выведет сообщения об установке и обязательно об закрытии подключения, несмотря на выброс исключения. Это демонстрирует детерминированное уничтожение объектов.


Шаблоны

Шаблоны (template) — это механизм метапрограммирования первого класса. При инстанцировании шаблона (например, std::vector<int>) компилятор генерирует новый код — отдельную версию функций и методов для каждого набора параметров. Это позволяет:

  • добиваться zero-cost abstractions (как в std::vector);
  • писать обобщённый код без приведения типов;
  • вычислять значения и типы во время компиляции (через constexpr, if constexpr, template specialization).

Например, std::enable_if или std::conditional — это языковые конструкции, позволяющие ветвить логику компиляции в зависимости от свойств типов.


Стандарты и эволюция

C++ живёт. Стандарт обновляется примерно раз в три года — C++11 (революция), C++14 (уточнения), C++17 (практические улучшения), C++20 (концепции, модули, корутины), C++23 (текущий опубликованный стандарт), C++26 (в работе). Например:

  • C++11 принёс — auto, range-based for, nullptr, move semantics, лямбды, std::unique_ptr/std::shared_ptr, constexpr;
  • C++20 добавил — Concepts (ограничения на шаблоны), Modules (альтернатива #include), Coroutines (асинхронность без callback hell), std::span, std::format.

Модули, в частности, решают фундаментальную проблему C: O(N²) зависимостей при #include. Вместо текстового включения заголовков, модули экспортируют интерфейсные декларации, что ускоряет компиляцию в десятки раз и исключает проблемы с include guards.


RAII

Resource Acquisition Is Initialization — это не просто способ избежать утечек памяти. Это модель управления временем жизни любых внешних ресурсов — файлов, сокетов, блокировок, GPU-буферов, транзакций в СУБД. В C++ ресурс считается приобретённым в момент конструирования объекта и освобождённым в момент его уничтожения. Важно: деструктор вызывается детерминированно, при выходе из области видимости — даже если выброшено исключение.

Пример: работа с мьютексом.

void critical_section(std::mutex& m) {
std::lock_guard<std::mutex> lock(m); // захват мьютекса в конструкторе
// ... критический код ...
// деструктор lock автоматически вызовет m.unlock()
}

Разбор:

  • Функция использует std::lock_guard<std::mutex> как RAII-объект блокировки критической секции.
  • Захват мьютекса выполняется в конструкторе lock_guard, освобождение — в его деструкторе.
  • Даже если внутри критического участка произойдёт исключение, unlock будет вызван автоматически.
  • Такой паттерн минимизирует риск дедлоков из-за пропущенного ручного unlock.
  • Это стандартный и рекомендуемый способ синхронизации в современном C++.

Здесь невозможно забыть разблокировать мьютекс — если критический код бросит исключение, стек раскрутится, и lock уничтожится до того, как исключение покинет функцию. Это называется exception safety — и оно встроено в язык, а не реализовано надстройками.

Сравните с C:

void critical_section(mutex_t *m) {
mutex_lock(m);
// ... если здесь будет ошибка, mutex_unlock() не вызовется ...
mutex_unlock(m);
}

Разбор:

  • C-версия показывает ручное управление блокировкой через mutex_lock и mutex_unlock.
  • Корректность здесь полностью зависит от того, что все пути выхода из функции вызовут mutex_unlock.
  • При добавлении новых веток ошибок легко пропустить разблокировку и получить зависание.
  • Этот контраст объясняет, почему в C++ предпочитают RAII-обёртки над прямыми парами lock/unlock.
  • Пример полезен как граница ответственности между языком без деструкторов и языком с автоматическим cleanup.

В C приходится вручную дублировать unlock в каждом goto error, что приводит к ошибкам. В C++ — нет.

RAII лежит в основе всех "умных указателей":

  • std::unique_ptr<T> — единоличное владение, move-only, нулевой overhead;
  • std::shared_ptr<T> — разделяемое владение со счётчиком ссылок; std::make_shared объединяет аллокацию объекта и control block (меньше обращений к куче). Потокобезопасны только операции со счётчиком; сам объект по-прежнему нужно защищать, если к нему обращаются из нескольких потоков.
  • std::weak_ptr<T> — наблюдатель, не продлевает жизнь объекта.

Заметьте: никакие из них не являются "заменой сборщику мусора". Они реализуют разные модели владения. unique_ptr часто компилируется в тот же код, что и T* вручную — просто с гарантией вызова delete.


Стандартная библиотека C++

STL (Standard Template Library), принятая в стандарт C++98, — это не просто набор контейнеров (vector, map, set). Это единая система абстракций, включающая:

  • Контейнеры — структуры данных (vector, deque, list, map, unordered_map, array, span);
  • Итераторы — обобщённые "указатели", позволяющие отделить алгоритм от структуры данных;
  • Алгоритмы — функции вроде std::sort, std::find, std::transform, работающие через итераторы;
  • Функторы и адаптерыstd::less, std::greater, std::bind, std::function.

Ключевой принцип: алгоритмы не знают о контейнерах. std::sort(v.begin(), v.end()) работает одинаково для std::vector<int> и int arr[100] — потому что оба предоставляют рандом-доступные итераторы. Это — истинная сила обобщённого программирования.

Но STL — лишь часть стандартной библиотеки. Современный <iostream>, <filesystem>, <thread>, <chrono>, <regex>, <format> (C++20) — это полноценные фреймворки, спроектированные под те же принципы:

  • эффективность по умолчанию;
  • совместимость с RAII;
  • поддержка пользовательских типов через перегрузку.

Например, std::filesystem::path не хранит строки напрямую — он инкапсулирует логику нормализации, разделителей, кодировок ОС. И при этом:

std::filesystem::path p = "/home/user/file.txt";
std::cout << p.filename() << std::endl; // file.txt

Разбор:

  • Создаётся объект std::filesystem::path, который абстрагирует путь с учётом особенностей ОС.
  • Вызов p.filename() извлекает последний компонент пути, то есть имя файла без каталогов.
  • Потоковый вывод в std::cout показывает результат в человекочитаемом виде.
  • Пример подчёркивает, что path — тип с семантикой файловой системы.
  • Такой API уменьшает число ошибок при ручном парсинге строк путей.

— работает без динамических аллокаций в простых случаях (SSO — Small String Optimization применяется и здесь).

Пример:

Код ITЗагрузка примера кода…

Разбор:

  • Код демонстрирует связку контейнера std::vector и алгоритмов STL из <algorithm>/<numeric>.
  • std::sort упорядочивает элементы диапазона, std::accumulate считает сумму, а std::transform модифицирует значения по лямбде.
  • Лямбда [](int x){ return x * x; } реализует локальную операцию возведения в квадрат.
  • Цикл for (int n : numbers) печатает уже преобразованные данные, подтверждая эффект алгоритмов.
  • Пример наглядно показывает принцип "алгоритмы работают с итераторами, а не с конкретным типом контейнера".

Алгоритм std::sort не знает, что он сортирует именно std::vector. Он получает два итератора и упорядочивает элементы между ними. То же самое можно сделать с массивом int arr[5], просто передав arr и arr + 5.


C++ и C#

Сравнение C++ и C# часто сводят к синтаксису: "оба используют фигурные скобки". Но суть — в модели исполнения.

КритерийC++C# (.NET)
Модель памятиНативная: стек, куча, статическая память — программа управляет всем.Управляемая: garbage-collected heap, стек для значимых типов, pinned objects для межъязыкового взаимодействия.
Время связыванияСтатическое (link-time), частично динамическое (DLL/so).Загрузка сборок JIT’ом (или AOT в .NET Native / NativeAOT), reflection.
ABI (Application Binary Interface)Стабилен на уровне компилятора (MSVC, GCC), но не между ними.Стабилен через IL (Intermediate Language) и CLR — двоичная совместимость между версиями .NET.
Совместимость с CПолная: можно линковать .o-файлы из C напрямую.Только через P/Invoke или C++/CLI — с overhead’ом и ограничениями.
Предсказуемость latencyДа: можно доказать отсутствие пауз, использовать lock-free структуры, избегать аллокаций.Нет: GC паузы (даже в режиме low-latency) не гарантированы.
ПортативностьТребует перекомпиляции, но работает на любой архитектуре с компилятором.Требует runtime (CLR/CoreCLR), но IL-код переносится без изменений.

C# — это платформа (язык + библиотеки + runtime), C++ — язык системного проектирования. Выбирая C#, вы вступаете в договор: "я отказываюсь от контроля над памятью и временем выполнения в обмен на безопасность и скорость разработки". Выбирая C++, вы берёте на себя ответственность — но получаете полную власть.

Именно поэтому C++ не "устарел": он решает другие задачи. Невозможно написать ядро ОС на C#, потому что .NET требует ОС для запуска. Невозможно написать GPU-шейдерный компилятор на C# без огромных прослоек — потому что он должен генерировать код до загрузки runtime’а.


Современный инструментарий

C++ давно перестал быть языком "просто .cpp и g++". Индустрия стандартизировала инструменты:


Сборка

cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(myapp main.cpp utils.cpp)
target_link_libraries(myapp PRIVATE Threads::Threads)
target_compile_options(myapp PRIVATE -Wall -Wextra -Wpedantic)

Разбор:

  • Конфигурация CMake задаёт проект и фиксирует стандарт C++20 для единообразной компиляции.
  • add_executable объявляет целевой бинарник myapp из перечисленных исходников.
  • target_link_libraries(... Threads::Threads) подключает библиотеку потоков в переносимой форме.
  • target_compile_options добавляет строгие предупреждения компилятора для раннего поиска проблем в коде.
  • Такой шаблон CMake создаёт хорошую базу для CI и масштабирования сборки.

CMake генерирует нативные проекты — Makefile, Ninja, Visual Studio .sln, Xcode — в зависимости от окружения. Он абстрагирует платформенные различия, но не скрывает их.


Управление зависимостями

  • Conan — централизованный пакетный менеджер (как Maven для C++):
[requires]
boost/1.84.0
nlohmann_json/3.11.3

[generators]
CMakeDeps
  • vcpkg — от Microsoft, интегрируется в Visual Studio, поддерживает triplets (x64-windows, arm64-linux и т.д.).

Оба позволяют собирать зависимости из исходников с вашими флагами — критично для embedded и high-performance.


Санитайзеры

  • AddressSanitizer (ASan) — ловит use-after-free, buffer overflows, double-free;
  • UndefinedBehaviorSanitizer (UBSan) — переполнение знаковых целых, деление на ноль, выравнивание;
  • ThreadSanitizer (TSan) — data races;
  • MemorySanitizer (MSan) — чтение неинициализированной памяти.

Запуск:

g++ -fsanitize=address,undefined -g main.cpp -o app
./app # при ошибке — стек-трейс с точным местом

Это — обязательный этап в CI/CD профессиональных проектов.


Модули (C++20)

// math.ixx (интерфейсный модуль)
export module math;

export int add(int a, int b) { return a + b; }
export double pi = 3.1415926535;
// main.cpp

import math;

int main() {
std::cout << add(2, 3) << " and " << pi << "\n";
}

Модули:

  • устраняют O(N²) зависимостей;
  • ускоряют компиляцию в 10–100×;
  • изолируют препроцессорные макросы (они не "просачиваются");
  • позволяют экспортировать только интерфейс, скрывая реализацию.

Это — наиболее значимое изменение в экосистеме C++ за 25 лет.

Пример - geometry.ixx:

// geometry.ixx (Интерфейсный модуль)
export module geometry;

export struct Point {
double x;
double y;
};

export double distance(Point p1, Point p2) {
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
return std::sqrt(dx * dx + dy * dy);
}

main.cpp:

// main.cpp (Основной файл)

import geometry;
import <iostream>;

int main() {
Point p1{0.0, 0.0};
Point p2{3.0, 4.0};

double dist = distance(p1, p2);
std::cout << "Расстояние: " << dist << "\n"; // Выведет 5

return 0;
}

Модуль geometry компилируется один раз и сохраняется в бинарном виде. Файл main.cpp импортирует только объявленные символы. Макросы из стандартных библиотек не влияют на пространство имен модуля, что устраняет конфликты имен.


Перспективы

Текущий стандарт — C++23 (принят в 2024 г.), работа над C++26 идёт активно. Ключевые направления:

  1. Исправление исторических ошибок:

    • унификация std::span и std::mdspan (многомерные массивы);
    • std::expected<T, E> как стандартный способ возврата ошибок (альтернатива исключениям);
    • упрощение синтаксиса корутин (C++20 ввёл их, но API неудобен).
  2. Контракты (Contracts):

int sqrt(int x) [[expects: x >= 0]] [[ensures r: r * r == x]];

Позволят формально специфицировать пред- и постусловия, проверяемые на этапе компиляции или выполнения.

  1. Reflection (метаинформация времени компиляции): Возможность анализировать структуру классов, членов, атрибутов — без макросов и boilerplate’а. Это откроет путь к автоматической сериализации, binding’ам, генерации интерфейсов.

  2. Безопасность памяти без overhead’а: Исследования в рамках C++ Safety Profiles (например, от Microsoft и Google) — как запретить "опасные" практики (сырые указатели, reinterpret_cast) на уровне статического анализа, не теряя производительности.


Мини-чеклист перед применением "продвинутой" возможности

Перед внедрением новой техники в проект задайте 5 вопросов:

  • Задача действительно требует этой сложности?
  • Команда сможет сопровождать такой код через 6 месяцев?
  • Есть тесты, покрывающие краевые случаи?
  • Есть измерения до/после (скорость, память, размер бинарника)?
  • Есть fallback-путь, если инструмент/фича нестабильна на целевой платформе?

Если хотя бы на 2 вопроса ответ "нет", сначала закройте эти риски.


Частые ошибки архитектурного уровня

ОшибкаПоследствиеКак исправить
Смешение нескольких стилей обработки ошибокхаотичный код и сложная диагностикавыбрать единые правила: исключения или expected по слоям
Ранний переход к lock-free структурамтрудноуловимые баги и регресс читаемостиначинать с mutex и переходить по профилированию
Переизбыток шаблонной магии в публичном APIтяжёлые ошибки компиляции у пользователейconcepts + документация + простые адаптеры
Игнорирование ABI-границпадения при деплое плагинов/SDKC-границы, PIMPL, строгая версия toolchain

Связанные статьи


Единица трансляции

Как уже упоминалось, C++ компилируется по единицам трансляции (translation units), каждая из которых — это результат обработки одного .cpp-файла препроцессором. Однако важно понимать, что именно изолируется и что приводит к связыванию.

Каждая единица трансляции — это **автономный контекст для:

  • разрешения имён (через using, namespace, ADL — Argument-Dependent Lookup);
  • инстанцирования шаблонов (при этом одно и то же шаблонное определение может быть инстанциировано по-разному в разных единицах, если параметры шаблона зависят от локальных typedef или using);
  • инициализации статических объектов (глобальных и static в пространствах имён).

Последнее особенно важно: порядок инициализации одноимённых статических объектов в разных единицах не определён. Это — главная причина "static initialization order fiasco", которую невозможно исключить полностью, но можно смягчить:

// utils.cpp
const std::string& get_config_path() {
static const std::string path = load_config_path(); // локальный static — инициализация при первом вызове
return path;
}

Здесь path инициализируется лениво и ровно один раз — даже в многопоточной среде (гарантия C++11+).


Совместимость с C

C++ сохраняет двустороннюю совместимость с C на уровне объектного кода. Это достигается через механизм языковой связи (language linkage):

// В C++ заголовке, предназначенном для C-кода:
#ifdef __cplusplus
extern "C" {
#endif

int legacy_c_function(int x);
void* allocate_buffer(size_t n);

#ifdef __cplusplus
}
#endif

Ключевое:

  • extern "C" подавляет манглинг имён — компилятор генерирует символы вида _legacy_c_function, а не _Z18legacy_c_functioni;
  • функции, объявленные в extern "C", не могут быть перегружены и не могут быть членами классов;
  • можно включать C-заголовки внутрь extern "C" блоков — это стандартная практика в смешанных проектах.

Это позволяет:

  • использовать C-библиотеки (POSIX, OpenGL, zlib, SQLite) напрямую;
  • писать интерфейсы для других языков (Python через ctypes, Rust через extern "C"), поскольку C — де-факто ABI-интерфейс для межъязыкового взаимодействия;
  • сохранять стабильность двоичного интерфейса: изменение реализации на C++ не ломает существующие .so/.dll, если заголовки не менялись.

⚠️ Важно: extern "C" не означает, что функция будет выполняться как C-код — она компилируется тем же компилятором, но с другим соглашением о вызовах и именовании.


Мономорфизация, code bloat и концепты

В отличие от generics в C# или Java, шаблоны C++ — это генерация кода на этапе компиляции (мономорфизация). При каждом уникальном наборе аргументов шаблона компилятор создаёт отдельную копию функции или класса.

Пример:

template<typename T>
T add(T a, T b) { return a + b; }

int main() {
add<int>(1, 2); // генерируется add<int>
add<double>(1.0, 2.0); // генерируется add<double>
}

Компилятор выдаст два независимых символа: _Z3addIiET_S0_S0_ и _Z3addIdET_S0_S0_ — с разным машинным кодом, оптимизированным под int и double.

Преимущества:

  • add<int> компилируется в lea eax, [rdi + rsi] — без вызовов, без проверок;
  • std::vector<bool> может иметь специализацию, хранящую биты, в то время как std::vector<int> — 32-битные слова;
  • if constexpr (C++17) позволяет условно исключать код из инстанцирования:
template<typename T>
auto process(T x) {
if constexpr (std::is_integral_v<T>) {
return x * 2;
} else {
return x.length();
}
}

Недостатки:

  • Code bloat: если шаблон используется с 100 типами — 100 копий кода;
  • сложные сообщения об ошибках (до C++20 — "template instantiation depth exceeds");
  • невозможность отделить интерфейс от реализации (шаблон должен быть виден целиком в заголовке).

Концепты (C++20)

Концепты — это ограничения на параметры шаблонов на уровне компиляции, позволяющие:

  • писать понятные условия: template<std::integral T> T add(T a, T b);
  • получать осмысленные ошибки: "std::string не удовлетворяет концепту std::integral" вместо "оператор + не определён для basic_string";
  • перегружать шаблоны по концептам:
template<std::integral T>
void sort(T* arr, size_t n); // быстрая сортировка для POD

template<std::sortable U>
void sort(std::vector<U>& v); // интроспективная сортировка для контейнеров

Это — переход от duck typing ("если крякает, как утка") к structural typing ("если имеет операции <, =, swap — то Sortable").

Пример:

#include <iostream>
#include <string>

template<typename T>
T multiply(T a, T b) {
return a * b;
}

int main() {
int int_result = multiply(5, 6); // Генерируется код для int
double double_result = multiply(2.5, 4.0); // Генерируется код для double
std::string str_result = multiply(std::string("A"), std::string("B")); // Генерируется код для string

std::cout << "Int: " << int_result << "\n";
std::cout << "Double: " << double_result << "\n";
std::cout << "String: " << str_result << "\n";

return 0;
}

Компилятор создаст три независимых машинных кода внутри исполняемого файла. Для int это будет простая инструкция умножения процессора, для double — FPU-инструкция, для std::string — вызов метода конкатенации. Никаких проверок типов во время выполнения не происходит.


Исключения

Распространённое утверждение: "исключения в C++ бесплатны, если не выбрасываются". На практике — это table-driven zero-cost exception handling.

Как это работает (вкратце):

  1. При компиляции генерируются таблицы раскрутки стека (LSDA — Language-Specific Data Area), описывающие, какие деструкторы вызывать при раскрутке для каждой точки программы.
  2. При выбросе исключения (throw) runtime ищет в этих таблицах соответствие типа исключения и блока catch.
  3. Если найдено — стек раскручивается, вызываются деструкторы, управление передаётся в catch.
  4. Если не найдено — вызывается std::terminate.

Накладные расходы:

  • В нормальном потоке: ни одного дополнительного if, ни одного сравнения — ноль overhead’а.
  • При выбросе: поиск в таблицах O(1) в среднем, но с затратами на обход фреймов стека и вызов деструкторов.
  • Размер кода: таблицы увеличивают размер бинарника (на 5–15%, в зависимости от объёма try/catch).

Поэтому в real-time системах (авионика, embedded) исключения часто отключаются (-fno-exceptions), а ошибки передаются через std::expected<T, E> (C++23) или коды возврата.


Философия "pay for what you use"

Этот принцип — основа стандартизации C++. Он означает:

Если вы не используете некую функцию языка или библиотеки, она не должна:

  • увеличивать размер исполняемого файла;
  • замедлять выполнение;
  • усложнять модель памяти;
  • вводить неопределённое поведение.

Примеры:

ФичаКак обеспечивается "pay for what you use"
Виртуальные функцииТаблица виртуальных функций (vtable) создаётся только если в классе есть хотя бы одна virtual функция. Чистые данные (struct Point { int x, y; }) — без overhead’а.
RTTI (typeid, dynamic_cast)Активируется только при использовании; без -frtti — исключается из бинарника полностью.
ИсключенияПри -fno-exceptions компилятор удаляет LSDA и заменяет throw на abort().
std::vectorНет виртуальных вызовов, нет проверок границ в operator[], аллокатор можно заменить.
std::functionИспользует малый буфер (SSO) для лямбд без захвата — без аллокаций.

Это — не "оптимизация компилятором". Это — спецификационное требование. Например, стандарт гарантирует, что std::unique_ptr<T> имеет тот же размер, что и T*, и что его move — тривиален.


Header Units и Modules

До C++20 единственный способ компоновки — #include, что приводит к:

  • O(N²) зависимостей — каждый #include <vector> тянет за собой <type_traits>, <memory>, <initializer_list> и т.д.;
  • Дублирование парсинга: один и тот же <iostream> парсится в каждом .cpp, где он нужен;
  • Макросные конфликты: #define max(a,b) ((a)>(b)?(a):(b)) в Windows’овском windows.h ломает std::max.

Header Units (C++20) — промежуточное решение:

g++ -fmodules-ts -xc++-system-header iostream

Превращает <iostream> в бинарный модуль, который компилируется один раз и импортируется быстро.

Именованные модули — окончательное:

// math.mpp
export module math;
export import <cmath>;

export double deg2rad(double deg) { return deg * std::numbers::pi / 180.0; }
// main.cpp

import math;
import <iostream>;

int main() {
std::cout << deg2rad(180.0) << "\n"; // 3.14159...
}

Преимущества:

  • компиляция ускоряется в 10–100×;
  • макросы не "просачиваются";
  • интерфейс (export) явно отделён от реализации;
  • IDE получает точную информацию без парсинга тысяч строк.

Модель памяти и многопоточность

С появлением C++11 стандарт впервые зафиксировал модель памяти, совместимую с аппаратными архитектурами (x86, ARM, POWER). До этого поведение многопоточных программ зависело от компилятора и CPU — и было неопределённым.

Ключевое понятие — happens-before ("происходит до"). Это частичный порядок на операциях в программе, который гарантирует, что эффекты одной операции будут видны в другой. В C++ он строится из трёх компонентов:

  1. Program order — порядок в рамках одного потока;
  2. Synchronizes-with — связи через синхронизирующие операции (например, mutex.lock()mutex.unlock() в другом потоке);
  3. Transitive closure — если A happens-before B и B happens-before C, то A happens-before C.

Пример — data race и его устранение

// НЕПРАВИЛЬНО: data race
int counter = 0;
std::thread t1([]{ for (int i = 0; i < 1000; ++i) ++counter; });
std::thread t2([]{ for (int i = 0; i < 1000; ++i) ++counter; });
t1.join(); t2.join();
// counter может быть < 2000 — неопределённое поведение!

Решение — синхронизация:

std::mutex mtx;
int counter = 0;

auto inc = [&]{
for (int i = 0; i < 1000; ++i) {
std::lock_guard lock(mtx);
++counter;
}
};

Здесь lock() в одном потоке synchronizes-with unlock() в другом, и ++counter внутри критической секции упорядочено.

Пример публикации значения между потоками (корректная пара release/acquire для атомарных полей):

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> value{0};
std::atomic<bool> is_ready{false};

void producer() {
value.store(42, std::memory_order_relaxed);
is_ready.store(true, std::memory_order_release);
}

void consumer() {
while (!is_ready.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
std::cout << "Получены данные: " << value.load(std::memory_order_relaxed) << '\n';
}

int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
Важно

Пара release/acquire на одном атомарном флаге не делает безопасными обычные (неатомарные) поля структуры — для нескольких полей используйте std::mutex, все поля — атомарные, или указатель, опубликованный через std::atomic<T*>.


Атомарные операции

std::atomic<int> counter{0};

// без блокировок, но с гарантией целостности
counter.fetch_add(1, std::memory_order_relaxed); // если порядок не важен

Модификаторы memory_order позволяют выбрать компромисс:

  • relaxed — только атомарность, без упорядочения (счётчики);
  • acquire/release — однонаправленный барьер (например, "публикация" указателя);
  • seq_cst — полная последовательная согласованность (по умолчанию, но дорого на ARM).

Для нескольких полей на практике чаще берут мьютекс или публикуют указатель:

struct Payload { int x, y; };
Payload payload{};
std::mutex mtx;
bool ready = false;

// producer: std::lock_guard lock(mtx); payload = {42, 73}; ready = true;
// consumer: std::lock_guard lock(mtx); if (ready) use payload.x, payload.y;

Lock-free варианты (std::atomic<T*>, ring buffer) требуют отдельного проектирования и не сводятся к одному флагу ready над обычными int.


Современные паттерны

Policy-Based Проектирование (А. Александреску)

Разделение поведения через параметры шаблонов:

template<typename T, typename ThreadingModel = SingleThreaded>
class SmartPtr {
T* ptr;
// ThreadingModel::lock(), unlock() вызываются при доступе
};

Преимущество: выбор стратегии на этапе компиляцииSingleThreaded сводится к пустым inline-функциям.


CRTP (Curiously Recurring Template Pattern)

Статическое полиморфное наследование без виртуальных таблиц:

template<typename Derived>
struct Base {
void interface() {
static_cast<Derived*>(this)->implementation();
}
};

struct MyType : Base<MyType> {
void implementation() { std::cout << "MyType\n"; }
};

Используется в std::enable_shared_from_this, Eigen, Boost.Iterator.


Type Erasure (стирание типов)

Реализация интерфейса без виртуальных функций в пользовательском коде:

class Any {
struct Concept { virtual ~Concept() = default; };
template<typename T> struct Model : Concept { T data; };
std::unique_ptr<Concept> ptr;
};

Это — основа std::function, std::any, std::variant (частично).


Инструменты анализа

ИнструментНазначениеОсобенность
Clang Static AnalyzerАнализ потока данных, утечки, null-dereferenceИнтеграция в Xcode, open-source
PVS-StudioКоммерческий анализатор, ориентированный на промышленные стандарты (MISRA, AUTOSAR)Поддержка C++20, межъединичный анализ
CppcheckБесплатный статический анализаторХорош для CI (лёгкий, быстрый)
MISRA C++:2008/2023Стандарт безопасности для embedded (автомобильная, авиационная промышленность)Запрещает динамические аллокации, исключения, рекурсию в критических системах
Clang-TidyЛинтер + рефакторингПравила вроде modernize-use-nullptr, performance-unnecessary-copy-initialization

Пример правила Clang-Tidy:

clang-tidy main.cpp -- -std=c++20 -Iinclude

→ предупредит, если std::vector инициализируется через копию вместо std::move.


Freestanding Implementation

Стандарт C++ делит реализации на две категории:

  • Hosted — полная поддержка стандартной библиотеки (<iostream>, <vector>, исключения);
  • Freestanding — только ядро языка + заголовки — <cstddef>, <new>, <type_traits>, <atomic>, <coroutine> (частично).

Для embedded, ядер ОС, bootloaders используется freestanding mode:

// no #include <iostream>, no main() with args
extern "C" void _start() {
// инициализация сегментов .data, .bss вручную
// вызов глобальных конструкторов (если разрешено)
kernel_main();
__builtin_unreachable();
}

void kernel_main() {
// можно использовать:
// - размещение объектов (placement new)
// - шаблоны (std::array, std::span)
// - constexpr
// - atomics
// но не: std::cout, malloc, exceptions
}

Ключевые ограничения:

  • нет main() — точка входа задаётся линкером;
  • нет динамической памяти (если не реализована вручную);
  • исключения и RTTI обычно отключены (-fno-exceptions -fno-rtti);
  • глобальные объекты инициализируются статически (через .init_array или вручную).

Пример: ядро seL4 (C и небольшой долей C++) использует freestanding C++11+ с -fno-exceptions, -fno-rtti, -fno-threadsafe-statics.


C++ в контексте системных языков — Rust, Zig, Carbon

КритерийC++RustZigCarbon (experimental)
Безопасность памяти по умолчанию❌ (требует дисциплины)✅ (borrow checker)❌ (но есть анализ в runtime/compile-time)✅ (planned)
Производительность✅ (нулевой overhead)✅ (но иногда нужно unsafe)✅ (C ABI, no hidden cost)?
Совместимость с C✅ (полная)✅ (через extern "C")✅ (прямая, без wrapper’ов)✅ (goal)
Модель владенияRAII (ручная)ownership + borrow checkeroptional explicit ownership?
Компиляциямногоступенчатая (CPP → TU → link)единая единица (crate)единая (но с композицией)модульная (planned)
СтандартISO (раз в 3 года)RFC-driven communityсамодостаточный компиляторGoogle-led experiment
Сложность языка⚠️ (огромная)⚠️ (сложная модель заимствований)✅ (минималистичный синтаксис)?

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

  • unsafe блоки всё равно нужны для драйверов, FFI, lock-free;
  • компиляция медленнее;
  • Стабильный межъязыковый контракт обычно через extern "C"; "Rust ABI" для типов Rust между версиями компилятора не гарантируется. У C++ ABI тоже не единый между MSVC, GCC и Clang — стабильность только в рамках выбранного toolset и runtime.

Zig — "C, но лучше" — фокус на простоте, отладке, совместимости. Позволяет:

  • импортировать заголовки C напрямую (@cImport);
  • писать comptime-логику (как constexpr, но мощнее);
  • использовать defer (как RAII, но без классов).

Carbon (эксперимент от Google) — попытка модернизировать C++ без breaking changes, через новый язык с двусторонней совместимостью. Пока — research project.


Совместимость версий

C++ сталкивается с уникальной проблемой — миллиарды строк legacy-кода, написанных под C++98/03, должны продолжать компилироваться и линковаться с новым кодом — без пересборки. Это достигается через:


1. Стабильность ABI на уровне реализации

  • MSVC гарантирует стабильность ABI в пределах одного major-релиза (например, VS 2019 — v142 toolset). Изменения ABI происходят при смене toolset’а (v142 → v143).
  • GCC стабилен в пределах major-версии (GCC 11.x), но может менять ABI между ними (например, std::string — COW до GCC 5, SSO после).
  • Clang+libc++ — стабильность на уровне версии libc++ (ABI tags: _LIBCPP_ABI_VERSION=2 и т.д.).

2. Symbol versioning (Linux)

В .so-библиотеках символы могут иметь версионные метки:

_ZNSt6vectorIiSaIiEE5clearEv@GLIBCXX_3.4
_ZNSt6vectorIiSaIiEE5clearEv@@GLIBCXX_3.4.21

Позволяет одномоментно поддерживать несколько ABI в одной библиотеке.


3. Inline namespaces (C++11+)

Механизм плавного обновления интерфейсов без изменения имени пространства имён:

namespace std {
inline namespace v2 {
void new_algorithm();
}
namespace v1 {
void old_algorithm();
}
} // end namespace std

Пользователь пишет std::new_algorithm(), но линковка идёт в std::v2::new_algorithm. Это — основа literals:

using namespace std::literals;
auto s = "hello"s; // std::string, а не const char*

4. Dual ABI (GCC 5+)

Для std::string и std::list GCC ввёл двойной ABI: старый (COW) и новый (SSO). Переключается флагом _GLIBCXX_USE_CXX11_ABI=0/1.

💡 Практическое правило: линкуйте все компоненты проекта одним компилятором и одной версией стандартной библиотеки. Смешивание MSVC+Clang или GCC 10+GCC 13 — почти всегда приведёт к UB.


Нестандартные расширения

Стандарт C++ — это минимум, который обязан поддерживать компилятор. Реальные компиляторы добавляют расширения для:

  • совместимости с ОС;
  • отладки;
  • низкоуровневого контроля;
  • межъязыкового взаимодействия.

GCC/Clang

РасширениеНазначение
__attribute__((packed))отключает выравнивание полей в структуре (для работы с сетевыми/дискретными протоколами)
__attribute__((noreturn))функция не возвращает управление (например, abort())
__builtin_expect(cond, likely)подсказка ветвлению (if (__builtin_expect(x == 0, 0)))
__thread / thread_localTLS (thread-local storage) до C++11
__VA_OPT__условное расширение в variadic macros

MSVC

РасширениеНазначение
__declspec(dllexport) / dllimportэкспорт/импорт символов в DLL
#pragma comment(lib, "libname")автоматическая линковка библиотеки
__forceinlineпринудительный инлайн (сильнее inline)
__uuidof(T)получение GUID COM-интерфейса
#pragma onceнестандартная, но широко поддерживаемая защита от повторного включения

Специализированные подмножества

  • CUDA C++ — расширение для GPU — __global__, __device__, __host__, unified memory.
  • C++/CLI — управляемый C++ для .NET — ref class, gcnew, ^ (handle), cli::array<T>^. Не является ISO C++ — отдельный язык.
  • OpenMP#pragma omp parallel for — простая параллелизация циклов.

⚠️ Важно: расширения не переносимы. Используйте их только при явной необходимости и изолируйте через макросы:

#ifdef _MSC_VER
__declspec(noinline) void f();
#elif defined(__GNUC__)
__attribute__((noinline)) void f();
#else
void f(); // fallback
#endif

Инструменты профилирования

Производительность C++-кода нельзя оценивать "на глаз". Требуются инструменты:

ИнструментПлатформаОсобенность
perf (Linux)Linuxsampling-based, hardware counters (cycles, cache misses, branches), perf record -g ./app → flame graph
VTune Profiler (Intel)Cross-platformtop-down microarchitecture analysis, memory access patterns, thread contention
TracyCross-platform (open-source)instrumentation-based, real-time timeline, lock profiling, allocation tracing — встраивается в код
heaptrackLinuxтрекинг аллокаций: кто выделяет, сколько, и не освобождает
gprofLegacyустарел: не работает с оптимизированным кодом, не поддерживает многопоточность

Пример использования Tracy:

#define TRACY_ENABLE
#include <Tracy.hpp>

void heavy_computation() {
ZoneScoped; // автоматически измеряет время выполнения
for (int i = 0; i < 1000000; ++i) {
// ...
}
}

→ Запуск ./app + Tracy-client → интерактивный timeline с call stack’ами, allocation heatmap’ом, lock waits.


C++ в научных вычислениях и HPC

Высокопроизводительные вычисления (HPC), машинное обучение, физика, биоинформатика — всё чаще используют C++ как хост-язык для ядер вычислений.


Почему C++?

  • Нулевой overhead при вызове CUDA/HIP/SYCL ядер;
  • Возможность писать векторизуемый код (#pragma omp simd, __builtin_assume_aligned);
  • Интеграция с BLAS/LAPACK через интерфейсы на C;
  • Выразительность шаблонов для generic linear algebra.

Ключевые библиотеки

БиблиотекаНазначениеОсобенность
Eigenлинейная алгебра (матрицы, векторы, разложения)header-only, expression templates, SIMD auto-vectorization
ArmadilloMATLAB-подобный API, лёгкая интеграция с LAPACKmat A = randu<mat>(5,5);
xTensorтензорные вычисления (релятивистская физика)символьные выражения, code generation
oneAPI DPC++ / SYCLединый код для CPU/GPU/FPGAстандарт Khronos, поддержка USM (Unified Shared Memory)

Пример: Eigen + OpenMP

#include <Eigen/Dense>
#include <omp.h>

Eigen::MatrixXd A = Eigen::MatrixXd::Random(1000, 1000);
Eigen::MatrixXd B = Eigen::MatrixXd::Random(1000, 1000);

// Eigen автоматически использует BLAS, но можно контролировать:
#pragma omp parallel for
for (int i = 0; i < A.rows(); ++i) {
A.row(i) *= 2.0;
}

Eigen::MatrixXd C = A * B; // вызовет ?GEMM из BLAS

C++ здесь — движок, на котором работает PyTorch (libtorch), TensorFlow (TFRT), ROOT (CERN).


Безопасность как приоритет

C++ не может игнорировать проблему memory safety — особенно на фоне успеха Rust. Ответ — безопасные подмножества и статический контроль.


CppCoreGuidelines (Bjarne Stroustrup & Herb Sutter)

https://isocpp.github.io/CppCoreGuidelines
— рекомендации по написанию безопасного, эффективного кода.

Ключевые правила:

  • I.4: "Make interfaces precisely and strongly typed"
    → избегайте void*, int для идентификаторов — используйте strong typedef’ы:
struct UserId { int value; };
void send_email(UserId u); // не send_email(int id)
  • R.2: "Use smart pointers, not raw pointers"
    unique_ptr, shared_ptr, span вместо T*.
  • ES.49: "If you must use a cast, use a named cast"
    static_cast, dynamic_cast, reinterpret_cast, const_cast — не C-style (T)x.

Guidelines Support Library (GSL)

Реализация ключевых идиом из CppCoreGuidelines:

#include <gsl/gsl>

gsl::span<int> process(gsl::span<const int> data) {
// span — view на [ptr, size), проверяет выход за границы в debug
Expects(!data.empty()); // Contract checking
return data.subspan(0, 1);
}
  • gsl::owner<T*> — маркер: "этот указатель владеет памятью";
  • gsl::not_null<T*> — гарантия ненулевого указателя;
  • gsl::span<T> — безопасная замена "указателю + длине".

MISRA C++ и AUTOSAR C++

Промышленные стандарты для автомобильной и аэрокосмической отраслей:

  • запрет исключений;
  • запрет рекурсии;
  • ограничение глубины вложенности;
  • запрет динамических аллокаций после старта.

Пример правила AUTOSAR:

Rule A13-5-1: A function shall not have more than 5 levels of nesting.


Microsoft GSL, clang-tidy, PVS-Studio — автоматизация

  • clang-tidy --checks=cppcoreguidelines-*,-performance-*
  • PVS-Studio: поддержка MISRA C++:2008 и AUTOSAR C++14.