Skip to content

C++ Шпаргалка

Язык общего назначения с ООП, обобщённым программированием и низкоуровневым контролем.

01

Начало работы

Hello World

Каждая программа на C++ начинается в main(). <iostream> предоставляет std::cout (стандартный вывод) и std::cin (стандартный ввод). Префикс std:: относится к стандартному пространству имён; using namespace std; может избежать его, но не рекомендуется в заголовках, так как загрязняет глобальное пространство имён.

cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Переменные и auto

auto выводит тип из инициализатора (C++11). Используйте auto, когда тип очевиден или многословен (итераторы). const делает значение неизменяемым; constexpr вычисляется во время компиляции для истинных констант, встроенных в бинарник.

cpp
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42;          // int
auto y = 3.14;        // double
const double TAX = 0.08;

Ввод и вывод

std::getline читает всю строку вместе с пробелами, тогда как std::cin >> останавливается на пробеле. Смешивание их оставляет перевод строки в буфере; вызывайте std::cin.ignore() между getline и >>, чтобы отбросить его.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name: ";
    std::getline(std::cin, name);
    std::cout << "Age: ";
    std::cin >> age;
    std::cout << "Hi " << name << ", " << age << "\n";
}

Ссылки

Ссылки — это алиасы, которые должны быть инициализированы и не могут быть перепривязаны к другому объекту. Передача по ссылке избегает копирования и позволяет изменять переменную вызывающего. Используйте const T& для параметров только для чтения, чтобы избежать дорогостоящих копий.

cpp
int x = 10;
int &ref = x;   // alias for x
ref = 20;
std::cout << x;   // 20

void increment(int &n) { n++; }
int a = 5;
increment(a);   // a is now 6

Преобразование типов

Предпочитайте static_cast вместо приведения в стиле C, так как оно явно и проверяется во время компиляции, делая намерение ясным. std::stoi, std::stod преобразуют строки в числа; std::to_string делает обратное. Остерегайтесь std::out_of_range при некорректном вводе.

cpp
double d = 3.99;
int i = (int)d;               // C-style: 3
int j = static_cast<int>(d);  // C++: 3

int n = 65;
char c = static_cast<char>(n);  // 'A'

// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);
02

Строки

Основы std::string

std::string управляет собственной памятью и растёт по мере необходимости. В отличие от C char-массивов, вам не нужно вручную управлять длиной. .find() возвращает std::string::npos (огромное значение), если подстрока не найдена, поэтому всегда сравнивайте с npos.

cpp
#include <string>
std::string s = "Hello";
std::cout << s.length();      // 5
std::cout << s[0];            // H
s += ", World!";
std::cout << s;               // Hello, World!
std::cout << s.substr(0, 3);  // Hel
std::cout << s.find("World"); // 7

Сравнение и поиск

Сравнение лексикографическое (словарный порядок). .find() ищет вперёд, .rfind() — назад. Оба возвращают std::string::npos, если не найдено, поэтому всегда сравнивайте с npos, а не трактовайте результат как булево.

cpp
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
    std::cout << "found\n";
}
size_t pos = a.rfind("p");  // last occurrence

Stringstream

stringstream связывает строки и типизированные значения, полезен для построения форматированных строк (как буфер) или разбора токенов, разделённых пробелами. Медленнее прямых операций, но очень гибок для сериализации и десериализации.

cpp
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();

// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;   // a=10, b=20, c=30

Сырые строки и многострочность

Сырые строковые литералы R"(...)" трактуют обратные слэши и кавычки буквально, идеальны для regex-паттернов, путей к файлам Windows и шаблонов JSON/XML. Разделители внутри скобок произвольны, например R"x(...)x", чтобы разрешить ) внутри.

cpp
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes

std::string json = R"({
  "name": "Alice",
  "age": 30
})";

Массивы char vs std::string

C-массивы char требуют ручного управления размером и подвержены ошибкам (переполнения буфера). Предпочитайте std::string; используйте .c_str() при взаимодействии с C-API, ожидающими const char*. Учтите, что c_str() действителен только пока строка жива и не изменена.

cpp
char cstr[] = "Hello";   // null-terminated, size 6
std::string cppstr = "Hello";

#include <cstring>
std::cout << strlen(cstr);      // 5
std::cout << cppstr.length();   // 5

std::string from_c = cstr;          // C -> C++
const char* to_c = cppstr.c_str();  // C++ -> C
03

Числа и математика

Целые и плавающие типы

Используйте фиксированной ширины типы <cstdint> (int32_t, int64_t), когда точный размер важен на разных платформах. Разделитель ' (C++14) улучшает читаемость больших чисел. double — плавающий тип по умолчанию, предпочтительнее float для точности.

cpp
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a);   // 4

float f = 1.5f;          // 4 bytes
double d = 3.14159265;   // 8 bytes (preferred)

Числовые лимиты

<limits> предоставляет type-traits для числовых свойств. Используйте их вместо жёстко заданных макросов INT_MAX. epsilon() даёт наименьшую различимую разность для плавающей точки, полезно для сравнения double с допуском.

cpp
#include <limits>
std::cout << std::numeric_limits<int>::max();    // 2147483647
std::cout << std::numeric_limits<int>::min();    // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();

Математические функции

<cmath> предоставляет стандартные математические функции. Переполнение целого — неопределённое поведение в C++; используйте int64_t или проверяйте границы. Для финансового кода помните, что плавающая точка неточна — рассмотрите целочисленные копейки или десятичную библиотеку.

cpp
#include <cmath>
double x = 2.5;
std::pow(x, 3);     // 15.625
std::sqrt(x);       // 1.581
std::abs(-5);       // 5
std::floor(3.7);    // 3.0
std::ceil(3.2);     // 4.0
std::round(3.5);    // 4.0
std::fmod(10.5, 3); // 1.5

Случайные числа

Современный C++ использует библиотеку <random> вместо rand(). mt19937 — высококачественный PRNG. Распределения (uniform_int, uniform_real, normal) отображают сырые биты в нужный диапазон без смещения по модулю, которое преследует rand() % N.

cpp
#include <random>
std::random_device rd;
std::mt19937 gen(rd());  // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);

for (int i = 0; i < 5; i++) {
    std::cout << dist(gen) << " ";
}

Переполнение целых и приведения

Переполнение знакового целого — неопределённое поведение в C++ (компилятор может оптимизировать, предполагая, что оно никогда не происходит). Всегда приводите к более широкому типу перед умножением или проверяйте границы. Переполнение беззнакового оборачивается по модулю 2^n и хорошо определено.

cpp
int a = 100'000;
int b = a * a;   // overflow! undefined behavior
long long safe = (long long)a * a;  // OK

// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
    // would overflow, handle it
}
04

Поток управления

If / Else

C++17 вводит if с инициализатором: if (auto it = m.find(k); it != m.end()) { ... }. Это ограничивает переменную блоком if/else, сохраняя окружающую область чистой и избегая случайного повторного использования.

cpp
int score = 85;
if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 80) {
    std::cout << "B\n";
} else {
    std::cout << "C\n";
}

Switch

Всегда включайте break для предотвращения непреднамеренного проваливания. Атрибут [[fallthrough]] в C++17 документирует намеренное проваливание для подавления предупреждений. Switch работает с целыми и enum-типами, не со строками или float.

cpp
int day = 3;
switch (day) {
    case 1: std::cout << "Mon"; break;
    case 2: std::cout << "Tue"; break;
    case 3: std::cout << "Wed"; break;
    default: std::cout << "Other";
}

Циклы for

Range-based for чисто итерирует контейнеры. Используйте const auto&, чтобы избежать копирования элементов (важно для строк и крупных объектов). Для изменения элементов на месте используйте auto& (неконстантная ссылка).

cpp
// Classic for
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n;  // no copy

While и do-while

while проверяет условие перед выполнением; do-while выполняет тело хотя бы один раз перед проверкой. do-while полезен для валидации ввода и циклов меню, где тело должно выполняться до того, как условие можно оценить.

cpp
int n = 5;
while (n > 0) {
    std::cout << n-- << " ";
}

int x;
do {
    std::cin >> x;
} while (x < 0);  // runs at least once

Break, continue и вложенные циклы

break выходит из ближайшего охватывающего цикла; continue переходит к следующей итерации. В C++ нет помеченного break как в Java; используйте переменную-флаг или извлеките цикл в функцию и используйте return для выхода из вложенных циклов.

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    std::cout << i << " ";  // 0 1 2 4 5 6
}

// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
    for (int j = 0; j < m; j++) {
        if (matrix[i][j] == target) { found = true; break; }
    }
}
05

Функции и лямбды

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

Структурированные привязки C++17 (auto [a, b] = ...) чисто распаковывают tuple, pair и структуры. До C++17 используйте std::tie или выходные параметры. Возврат по значению дешев благодаря move-семантике (RVO), которая устраняет копирование.

cpp
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

auto [q, r] = divide(17, 5);  // q=3, r=2 (C++17)

Default и inline

Аргументы по умолчанию позволяют вызывающим опускать хвостовые параметры. inline — подсказка компилятору развернуть функцию inline; современные компиляторы сами решают вопрос инлайнинга на основе флагов оптимизации, поэтому inline в основном касается ODR (правила одного определения).

cpp
inline int power(int base, int exp = 2) {
    int r = 1;
    for (int i = 0; i < exp; i++) r *= base;
    return r;
}
// power(3) == 9, power(2, 5) == 32

Перегрузка функций

Перегрузка позволяет функциям иметь одно имя, но различаться типами параметров. Компилятор выбирает лучшее совпадение через разрешение перегрузки. Неоднозначные перегрузки вызывают ошибки компиляции; предпочитайте шаблоны, когда тело одинаково для всех типов.

cpp
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }

add(1, 2);          // int version
add(1.5, 2.5);      // double version
add("a", "b");      // string version

Лямбда-выражения

Лямбды создают анонимные функциональные объекты inline. [] захватывает переменные: [=] по значению, [&] по ссылке, [x] конкретную по значению, [&x] конкретную по ссылке. Они необходимы для STL-алгоритмов и callback'ов. Остерегайтесь висячих ссылок при захвате по ссылке.

cpp
auto square = [](int x) { return x * x; };
std::cout << square(5);  // 25

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
    std::cout << n << " ";
});

Указатели на функции и std::function

std::function (из <functional>) хранит любой callable: функции, лямбды, функторы. Гибче сырых указателей на функции, но имеет небольшие накладные расходы во время выполнения из-за стирания типов. Используйте для callback'ов и хранения callable в контейнерах.

cpp
#include <functional>
int add(int a, int b) { return a + b; }

int (*fp)(int, int) = add;             // raw function pointer
std::function<int(int, int)> f = add;  // flexible wrapper
f = [](int a, int b) { return a * b; };

std::cout << f(3, 4);  // 12
06

Контейнеры STL

vector

vector — динамический массив и контейнер по умолчанию. push_back амортизированно O(1). .at() проверяет границы (бросает std::out_of_range), operator[] — нет. Вызовите reserve() заранее, если знаете размер, чтобы избежать перераспределений.

cpp
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size();          // 3
v[0] = 0;
v.at(5);           // throws std::out_of_range
for (int n : v) std::cout << n;

map и unordered_map

map хранит ключи отсортированными (операции O(log n)); unordered_map использует хеширование (O(1) в среднем). Используйте map, когда нужен упорядоченный обход или запросы диапазона; unordered_map для чистой скорости поиска. Обход unordered_map неупорядочен.

cpp
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered;   // sorted by key (red-black tree)
ordered["Alice"] = 30;

std::unordered_map<std::string, int> hashed;  // hash table
hashed["Bob"] = 25;

for (auto &[k, v] : ordered) {  // C++17 structured binding
    std::cout << k << ":" << v;
}

set и unordered_set

set хранит уникальные отсортированные элементы (O(log n)). unordered_set — хеш-версия (O(1) в среднем). Используйте для дедупликации и проверки членства. lower_bound/upper_bound позволяют запросы диапазона на упорядоченных множествах.

cpp
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3);  // first >= 3

array и deque

array — массив фиксированного размера, размещённый в стеке, с STL-интерфейсом (безопаснее C-массивов, без decay к указателю). deque (двусторонняя очередь) поддерживает O(1) push/pop с обоих концов, в отличие от vector, который O(n) спереди.

cpp
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3};  // fixed size, stack-allocated
arr.size();  // 3

std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4

tuple и pair

tuple хранит гетерогенные значения любых типов. pair — это tuple из 2 элементов. Структурированные привязки (C++17) раскладывают их в именованные переменные. Распространено при итерации map, чьи элементы — пары (ключ, значение).

cpp
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t;  // C++17 structured binding

std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;

auto [a, b] = std::make_pair(10, 20);
07

Указатели и память

Сырые указатели

Указатели хранят адреса памяти. & получает адрес, * разыменовывает. Арифметика указателей работает с массивами. Сырые указатели не отслеживают владение, что приводит к утечкам и висячим указателям — предпочитайте умные указатели для owned-ресурсов.

cpp
int x = 10;
int *ptr = &x;        // address of x
std::cout << *ptr;    // 10 (dereference)
*ptr = 20;
std::cout << x;       // 20

int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1);  // 2

Ссылки vs указатели

Ссылки безопаснее (никогда null, всегда валидны) и имеют более чистый синтаксис. Используйте ссылки для параметров функций и возвращаемых значений. Используйте указатели, когда null — осмысленное состояние или когда нужно перепривязать, на что указываете.

cpp
int x = 10;
int &ref = x;    // must init, cannot reseat
int *ptr = &x;   // can be null, can reassign

ref = 20;        // x = 20
*ptr = 30;       // x = 30

// References cannot be null, safer for parameters
void foo(const std::string &s);  // preferred
void bar(std::string *s);        // s might be null

unique_ptr

unique_ptr — единоличное владение heap-объектом. Не может копироваться, только перемещаться. Автоматически удаляется при выходе из области видимости (RAII). Это умный указатель по умолчанию для большинства случаев — нулевые накладные расходы над сырым указателем.

cpp
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p;  // 42
// auto p2 = p;   // ERROR: cannot copy
auto p2 = std::move(p);  // transfer ownership
// p is now nullptr

shared_ptr и weak_ptr

shared_ptr использует подсчёт ссылок; объект освобождается, когда последний shared_ptr уничтожен. weak_ptr наблюдает, не влияя на счётчик, разрывая циклы ссылок. Избегайте циклов shared_ptr (они утекают, потому что счётчик никогда не достигает нуля).

cpp
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a;  // both point to same object
std::cout << a.use_count();  // 2

std::weak_ptr<int> w = a;  // observer, no ownership
if (auto locked = w.lock()) {
    std::cout << *locked;  // 42
}

RAII и new/delete

RAII связывает время жизни ресурса с областью объекта: конструкторы захватывают, деструкторы освобождают. Это гарантирует очистку даже при распространении исключений. Предпочитайте vector и умные указатели ручным new/delete — они реализуют RAII за вас.

cpp
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;

// RAII: resource acquisition is initialization
class Buffer {
    int *data;
public:
    Buffer(size_t n) : data(new int[n]) {}
    ~Buffer() { delete[] data; }  // auto cleanup
};

// Modern: use containers/smart pointers instead
std::vector<int> buf(100);  // no manual delete needed
08

Классы и ООП

Класс и конструктор

Список инициализации членов (: name(...), age(...)) инициализирует членов до тела, эффективнее, чем присваивание в теле. Помечайте геттеры const, чтобы разрешить вызов на const-объектах. std::move избегает копирования строкового параметра.

cpp
class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(std::move(n)), age(a) {}
    std::string getName() const { return name; }
    int getAge() const { return age; }
};

Person p("Alice", 30);
std::cout << p.getName();

Модификаторы доступа и инкапсуляция

private-члены доступны только внутри класса; protected разрешает подклассам; public открыт всем. Инкапсуляция скрывает детали реализации, предоставляя стабильный интерфейс. Используйте private по умолчанию и предоставляйте только необходимое.

cpp
class Account {
private:
    double balance;
protected:
    std::string owner;
public:
    Account(double b) : balance(b) {}
    double getBalance() const { return balance; }
    void deposit(double amt) { if (amt > 0) balance += amt; }
};

Наследование и virtual

virtual обеспечивает полиморфизм во время выполнения — вызов speak() через Animal* диспетчеризуется к версии Dog. Всегда объявляйте виртуальный деструктор в базовых классах, чтобы удаление через указатель базы вызывало деструктор производного. override ловит опечатки.

cpp
class Animal {
public:
    virtual void speak() { std::cout << "..."; }
    virtual ~Animal() = default;  // virtual destructor!
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof"; }
};

Animal *a = new Dog();
a->speak();  // Woof (polymorphism)
delete a;

Абстрактные классы и интерфейсы

Чистая виртуальная функция (= 0) делает класс абстрактным — нельзя создать экземпляр. Классы только с чистыми виртуальными функциями действуют как интерфейсы Java. Конкретные подклассы должны реализовать все чистые виртуальные функции или останутся абстрактными.

cpp
class Shape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

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

Перегрузка операторов позволяет пользовательским типам работать с +, <<, == и т.д. Перегружайте только когда значение интуитивно (математические типы, итераторы). Оператор << часто перегружается через friend для потокового вывода, позволяя cout << myObject.

cpp
class Vec2 {
    double x, y;
public:
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2 &o) const {
        return {x + o.x, y + o.y};
    }
    friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
        return os << "(" << v.x << "," << v.y << ")";
    }
};

Vec2 a(1, 2), b(3, 4);
std::cout << a + b;  // (4,6)
09

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

Функциональные шаблоны

Функциональные шаблоны генерируют типоспецифичные версии во время компиляции. Компилятор выводит T из аргументов; вы можете указать его явно. Шаблоны — абстракции с нулевой стоимостью — без накладных расходов во время выполнения, но они увеличивают время компиляции и размер бинарника.

cpp
template <typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

std::cout << max_val(3, 7);           // int: 7
std::cout << max_val(3.14, 2.71);     // double: 3.14
std::cout << max_val<std::string>("a", "b");  // explicit

Шаблоны классов

Шаблоны классов параметризуют целые классы по типам. Стандартные контейнеры (vector, map) — все шаблоны. Код шаблона должен быть в заголовках (или используйте явное инстанцирование), потому что компилятору нужно полное определение для генерации кода.

cpp
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T v) { data.push_back(v); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
};

Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");

Специализация шаблонов

Полная специализация предоставляет пользовательскую реализацию для конкретного типа. Частичная специализация (только для шаблонов классов) настраивает для категории типов (например, все типы указателей). Полезно для оптимизации или особой обработки поведения.

cpp
template <typename T>
T identity(T x) { return x; }

// Full specialization for bool
template <>
bool identity<bool>(bool x) {
    std::cout << "bool!";
    return x;
}

identity(42);     // generic
identity(true);   // specialized

Вариативные шаблоны

Вариативные шаблоны принимают любое число аргументов через пакеты параметров (...). Они рекурсируют для обработки каждого аргумента. Fold-выражения C++17 упрощают это: (std::cout << ... << args). Широко используется в std::make_shared, std::tuple.

cpp
template <typename T>
void print(T v) { std::cout << v << "\n"; }

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ", ";
    print(rest...);  // recurse
}

print(1, "hello", 3.14);  // 1, hello, 3.14

Concepts (C++20)

Concepts (C++20) ограничивают параметры шаблона читаемыми требованиями, заменяя замысловатые enable_if/SFINAE. Они дают гораздо более понятные сообщения об ошибках, когда ограничения не соблюдены. Используйте стандартные концепты, такие как std::integral, std::floating_point, std::convertible_to.

cpp
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }

template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }

// Abbreviated form
auto square(std::integral auto x) { return x * x; }
10

Алгоритмы STL

sort и find

STL-алгоритмы работают с диапазонами итераторов [begin, end). sort — O(n log n). find — линейный; для отсортированных диапазонов используйте binary_search/lower_bound (O(log n)). Передавайте пользовательские компараторы (лямбды или std::greater) для пользовательского упорядочивания.

cpp
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());          // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>());  // descending

auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";

transform и for_each

transform отображает каждый элемент в новое значение (как map в функциональных языках). for_each применяет функцию для побочных эффектов. Range в C++20 позволяет v | views::transform(...) для более чистого конвейерного стиля без итераторов begin/end.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared: 1 4 9 16

std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8

accumulate и count

accumulate (из <numeric>) сворачивает диапазон с операцией. Третий аргумент — начальное значение, определяющее тип результата — используйте 0.0 для double-сумм. count возвращает, сколько элементов равно значению; count_if использует предикат.

cpp
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15
int product = std::accumulate(v.begin(), v.end(), 1,
                              std::multiplies<>());  // 120

int cnt = std::count(v.begin(), v.end(), 3);  // 1

copy, remove и unique

remove на самом деле не стирает — он сдвигает несоответствующие элементы вперёд и возвращает новый конечный итератор. Сочетайте с .erase() для идиомы erase-remove. unique аналогично уплотняет последовательные дубликаты; сначала sort для полной дедупликации.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

auto end = std::remove(v.begin(), v.end(), 3);  // erase-remove idiom
v.erase(end, v.end());

std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end());  // 1 2 3

min, max и clamp

min/max возвращают меньшее/большее из двух значений или initializer list. minmax возвращает оба как pair. clamp (C++17) ограничивает значение диапазоном, заменяя ручные проверки границ if/else — полезно для валидации ввода и UI-координат.

cpp
int a = 3, b = 7;
std::cout << std::max(a, b);   // 7
std::cout << std::min(a, b);   // 3

auto [mn, mx] = std::minmax({3, 1, 4, 1, 5});  // mn=1, mx=5

int score = 105;
int clamped = std::clamp(score, 0, 100);  // 100
11

Обработка ошибок

Исключения: try/catch

Бросайте исключения по значению, ловите по const-ссылке, чтобы избежать среза. Ловля std::exception ловит все стандартные исключения через базовый класс. Исключения — для исключительных случаев, не для обычного потока управления — они имеют накладные расходы при бросании.

cpp
#include <stdexcept>
try {
    int x = 10, y = 0;
    if (y == 0) throw std::runtime_error("division by zero");
    std::cout << x / y;
} catch (const std::exception &e) {
    std::cerr << "Error: " << e.what();
}

Стандартная иерархия исключений

<stdexcept> предоставляет стандартные типы исключений. logic_error — для ошибок программиста (обнаружимых до выполнения); runtime_error — для непредвиденных условий выполнения. Наследуйте пользовательские исключения от std::runtime_error, чтобы они интегрировались со стандартными catch-блоками.

cpp
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc();  // thrown by new on allocation failure

// std::exception is the base; .what() returns the message

Пользовательские исключения

Наследуйте пользовательские исключения от стандартной базы, чтобы они интегрировались с catch(const std::exception&). Добавляйте контекстные поля (пути к файлам, коды ошибок), помогающие отладке. Всегда передавайте сообщение в базовый конструктор, чтобы what() работал.

cpp
class FileError : public std::runtime_error {
public:
    FileError(const std::string &msg, const std::string &path)
        : std::runtime_error(msg + ": " + path), path_(path) {}
    const std::string &path() const { return path_; }
private:
    std::string path_;
};

try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
    std::cerr << e.what() << " at " << e.path();
}

noexcept и безопасность RAII

noexcept обещает, что функция не бросает исключения, включая оптимизации компилятора. Если всё же бросает, вызывается std::terminate. RAII гарантирует, что деструкторы выполняются при раскрутке стека, поэтому ресурсы освобождаются даже при распространении исключений вверх по стеку вызовов.

cpp
void safe_op() noexcept {
    // guaranteed not to throw
}

void risky() {
    std::vector<int> v(1000);  // RAII: cleanup on exception
    throw std::runtime_error("oops");
    // v's destructor still runs during stack unwinding
}

Утверждения

assert() проверяет условия в debug-сборках; он удаляется в release (когда определён NDEBUG), поэтому не используйте его для проверок в продакшене. Используйте для внутренних инвариантов, указывающих на баги. Для пользовательской валидации бросайте исключения или возвращайте коды ошибок.

cpp
#include <cassert>
double sqrt_safe(double x) {
    assert(x >= 0 && "sqrt of negative");
    return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removed
12

Файловый ввод-вывод и потоки

Чтение файла

ifstream открывает файл для чтения. Всегда проверяйте, удалось ли открытие (!file равно true при ошибке). getline читает построчно вместе с пробелами. Деструктор потока закрывает файл автоматически (RAII), поэтому ручное закрытие не требуется.

cpp
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }

std::string line;
while (std::getline(file, line)) {
    std::cout << line << "\n";
}

Запись файла

ofstream записывает в файл, по умолчанию обрезая. Используйте std::ios::app для добавления, std::ios::binary для бинарного режима. Оператор << работает точно как std::cout. Сбрасывайте через out.flush() или используйте std::endl (который также сбрасывает).

cpp
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;

out << "Line 1\n";
out << "Value: " << 42 << "\n";

out.close();  // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);

Строковые потоки

ostringstream строит строки из смешанных типов (как буфер). istringstream разбирает строки в типизированные значения. Они медленнее прямых строковых операций, но очень удобны для сериализации, построения URL и разбора токенов.

cpp
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();

// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;

Бинарные файлы

Бинарный режим избегает трансляции переводов строк и компактнее текста. write/read принимают char* и счётчик байтов — используйте reinterpret_cast для структур. Учтите: бинарные файлы не переносимы между архитектурами (порядок байтов, padding структур различаются).

cpp
#include <fstream>
struct Record { int id; double value; };

// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));

// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

Форматированный вывод (fmt в C++20)

std::format (C++20) приносит в C++ форматные строки в стиле Python, заменяя неудобные манипуляторы iomanip. Для старого кода <iomanip> предоставляет setprecision, setw, setfill. Библиотека {fmt} — популярная pre-C++20 альтернатива с тем же синтаксисом.

cpp
#include <format>  // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159);  // 3.14
std::cout << std::format("{:>10}", "right");   // padded

// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;
13

Умные указатели

unique_ptr — эксклюзивное владение

unique_ptr — умный указатель по умолчанию; используйте, когда одного владельца достаточно. Нулевые накладные расходы над сырыми указателями. make_unique предпочтителен (исключительно безопасен). Нельзя копировать, только перемещать. Пользовательские deleters обеспечивают RAII для C-ресурсов, таких как FILE* или сокеты.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);

shared_ptr — разделяемое владение

shared_ptr использует подсчёт ссылок — несколько указателей могут владеть одним объектом. Объект уничтожается, когда счётчик достигает 0. make_shared предпочтителен (одно выделение для объекта + управляющий блок). Тяжелее unique_ptr из-за атомарного счётчика и управляющего блока. Используйте, когда владение действительно разделяемое.

cpp
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: both share ownership
std::cout << *p1 << " " << p1.use_count();  // 42 2

// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}};  // uses non-array new

// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to object

weak_ptr — разрыв циклов

weak_ptr — не владеющий наблюдатель shared_ptr. Не увеличивает счётчик. Используйте lock() для временного получения shared_ptr (возвращает null, если объект уничтожен). Необходим для разрыва циклов ссылок (например, двусвязные списки, отношения родитель-потомок), которые вызвали бы утечки памяти.

cpp
#include <memory>
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // weak_ptr from shared_ptr

// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
    std::cout << "prev exists";
} else {
    std::cout << "prev expired";
}

Умные указатели с массивами

Умные указатели могут управлять массивами. unique_ptr<T[]> предоставляет operator[] и корректный delete[]. shared_ptr<T[]> поддерживается с C++17. Однако std::vector или std::array почти всегда лучше — безопаснее, эргономичнее и самодокументируемы. Используйте умные указатели на массивы только при взаимодействии с legacy-API.

cpp
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;

// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;

// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10);  // better choice

enable_shared_from_this

Когда объекту нужно вернуть shared_ptr на себя, enable_shared_from_this предоставляет безопасный shared_from_this(). Прямой вызов shared_ptr<T>(this) создал бы второй управляющий блок, что привело бы к двойному освобождению. Объект уже должен управляться shared_ptr, иначе shared_from_this() бросает bad_weak_ptr.

cpp
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this();  // safe
        // return shared_ptr<Widget>(this);  // BAD: double delete
    }
};

auto w = std::make_shared<Widget>();
auto w2 = w->getPtr();  // shares ownership, refcount = 2
14

Move-семантика и rvalue-ссылки

Lvalue, rvalue и ссылки

Lvalue имеют идентичность и существуют за пределами одного выражения (именованные объекты). Rvalue — временные или литеральные значения. T& привязывается к lvalue, T&& — к rvalue. const T& особенная — привязывается к обоим. Понимание этого различия — основа move-семантики.

cpp
int x = 10;          // x is an lvalue
int& lref = x;       // lvalue reference
int&& rref = 20;     // rvalue reference (binds to temporary)
// int& bad = 20;    // ERROR: can't bind lref to rvalue
const int& cref = 20;  // OK: const lref binds to rvalue

int y = x + 5;       // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1);  // rr is named rvalue

std::move и move-конструкторы

std::move ничего не перемещает — он приводит к rvalue, позволяя выбрать move-конструктор/присваивание. Move-операции должны быть noexcept, чтобы контейнеры могли использовать их при перераспределении (иначе они откатываются к копированию для безопасности исключений). После move исходный объект находится в валидном, но неопределённом состоянии.

cpp
class Buffer {
    int* data;
    size_t size;
public:
    // Move constructor: steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data; size = other.size;
            other.data = nullptr; other.size = 0;
        }
        return *this;
    }
};

Buffer a(100);
Buffer b = std::move(a);  // calls move ctor, a is now empty

Идеальное перенаправление

Идеальное перенаправление передаёт аргументы в другую функцию, сохраняя их категорию значения (lvalue vs rvalue). T&& в выведенном контексте — 'forwarding reference' (не rvalue-ссылка). std::forward<T> условно приводит: lvalue, если T — T&, rvalue, если T — T&&. Необходимо для фабричных функций и обёрток.

cpp
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

template <typename... Args>
void log(Args&&... args) {
    // std::forward preserves value category
    log_impl(std::forward<Args>(args)...);
}

std::string s = "hello";
log(s);              // Arg = std::string&  (lvalue)
log(std::string());  // Arg = std::string&& (rvalue)

Правило пяти / Правило нуля

Правило пяти: если класс управляет ресурсом, вы должны определить деструктор, copy-конструктор, copy-присваивание, move-конструктор, move-присваивание. Правило нуля: предпочитайте композицию RAII-типов (vector, string, умные указатели), чтобы сгенерированные компилятором специальные члены были корректны. Это устраняет подверженное ошибкам ручное управление ресурсами.

cpp
// Rule of Five: if you define any of these, define all 5
class Resource {
    int* data;
public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
    Resource(const Resource& o);             // copy ctor
    Resource& operator=(const Resource& o);  // copy assign
    Resource(Resource&& o) noexcept;         // move ctor
    Resource& operator=(Resource&& o) noexcept;  // move assign
};

// Rule of Zero: prefer to use RAII types
class Better {
    std::vector<int> data;  // handles everything
public:
    Better() = default;  // compiler-generated funcs are correct
};

Оптимизация возвращаемого значения (RVO/NRVO)

RVO/NRVO позволяет компилятору конструировать возвращаемое значение напрямую в хранилище вызывающего, полностью избегая копий/перемещений. C++17 делает RVO обязательным для prvalue. Никогда не пишите return std::move(local) — это подавляет NRVO и принуждает к (более медленному) move. Просто возвращайте локальную переменную по имени и позвольте компилятору оптимизировать.

cpp
std::vector<int> makeVec() {
    std::vector<int> v;
    v.push_back(1); v.push_back(2);
    return v;  // NRVO: no copy, no move!
}

std::string makeStr() {
    return std::string("hello");  // RVO
}

// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec();  // constructed in place

// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
    std::vector<int> v;
    return std::move(v);  // WORSE: forces move, blocks NRVO
}
15

Параллелизм (thread, mutex, async)

Основы std::thread

std::thread запускает новый поток ОС. Вы ОБЯЗАНЫ вызвать join() (ожидание) или detach() (позволить работать независимо) до уничтожения объекта потока, иначе вызывается std::terminate. Аргументы по умолчанию передаются по значению — используйте std::ref для ссылок, std::move для move-only типов. Предпочитайте join, если нет веской причины detach.

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2([]{ std::cout << "Lambda thread\n"; });

    t1.join();   // wait for t1 to finish
    t2.join();   // must join or detach before destruction
    // t1.detach();  // runs independently (risky)

    // Hardware concurrency hint
    unsigned n = std::thread::hardware_concurrency();
}

Мьютекс и lock guard

Всегда защищайте разделяемые данные мьютексом. std::lock_guard — простейший RAII-лок: захватывает при конструировании, освобождает при уничтожении. std::scoped_lock (C++17) безопасно блокирует несколько мьютексов, используя алгоритм избегания взаимных блокировок. std::unique_lock даёт больше гибкости (ручная блокировка/разблокировка, отложенное блокирование) для использования с условными переменными.

cpp
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared;

void safePush(int val) {
    // RAII lock: unlocks even if exception thrown
    std::lock_guard<std::mutex> lock(mtx);
    shared.push_back(val);
}  // lock released here

// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
    std::scoped_lock lock(m1, m2);  // atomic
}

// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();

Условные переменные

condition_variable позволяет потокам ожидать условие. Всегда используйте предикат с wait() для обработки ложных пробуждений. Мьютекс должен удерживаться unique_lock при вызове wait(), который освобождает его во время ожидания и повторно захватывает перед возвратом. notify_one будит один ожидающий, notify_all — всех. Этот паттерн реализует потокобезопасные очереди и конвейеры producer-consumer.

cpp
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(42);
    }
    cv.notify_one();  // wake one waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !tasks.empty(); });  // predicate prevents spurious wakeup
    int task = tasks.front();
    tasks.pop();
}

std::async и future

std::async — высокоуровневый способ асинхронного запуска задач, возвращающий future. std::launch::async принуждает новый поток; std::launch::deferred выполняется лениво при get(). Политика по умолчанию может выбрать любую — будьте явным для предсказуемого поведения. Для большего контроля используйте пары std::promise/future. Всегда вызывайте get() на future перед разрушением, иначе деструктор может заблокировать.

cpp
#include <future>
#include <iostream>

int slowComputation() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get();  // blocks until ready

// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses

// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get();  // 10

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

std::atomic обеспечивает lock-free потокобезопасные операции для примитивных типов. Тяжелее обычного int, но гораздо легче мьютекса для простых счётчиков/флагов. Порядок памяти влияет на видимость: relaxed (без упорядочивания), acquire/release (пара для синхронизации), seq_cst (по умолчанию, сильнейший). Используйте атомики для счётчиков/флагов; мьютекс — для сложных критических секций.

cpp
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};
std::atomic<bool> ready{false};

void worker() {
    while (!ready.load(std::memory_order_acquire));
    counter.fetch_add(1, std::memory_order_relaxed);
}

// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
    expected, 1,
    std::memory_order_acq_rel);

// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);

// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire));  // acquire
spin.clear(std::memory_order_release);  // release
16

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

Специализация шаблонов

Специализация шаблонов предоставляет пользовательские реализации для конкретных типов. Полная специализация фиксирует все параметры шаблона. Частичная специализация (только для шаблонов классов) специализирует некоторые параметры, оставляя остальные обобщёнными. Широко используется в type traits, std::vector<bool> и оптимизации для известных типов.

cpp
// Primary template
template <typename T>
struct TypeName {
    static const char* get() { return "unknown"; }
};

// Full specialization for int
template <>
struct TypeName<int> {
    static const char* get() { return "int"; }
};

// Full specialization for const char*
template <>
struct TypeName<const char*> {
    static const char* get() { return "string"; }
};

std::cout << TypeName<int>::get();           // "int"
std::cout << TypeName<double>::get();        // "unknown"
std::cout << TypeName<const char*>::get();   // "string"

SFINAE и enable_if

SFINAE (Substitution Failure Is Not An Error) позволяет включать/отключать перегрузки шаблонов на основе свойств типа. std::enable_if условно определяет тип. Когда подстановка не удаётся, перегрузка тихо удаляется вместо вызова ошибки. if constexpr в C++17 и концепты C++20 часто заменяют SFINAE более чистым синтаксисом.

cpp
#include <type_traits>

// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }

// Only enable if T is floating point
template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }

process(10);    // integral
process(3.14);  // floating

// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

if constexpr (C++17)

if constexpr вычисляет условие во время компиляции и полностью отбрасывает ложную ветку (даже не проверяет типы). Это заменяет многие паттерны SFINAE гораздо более чистым кодом. Особенно полезно для шаблонной рекурсии (базовый случай завершает рекурсию) и для ветвления по type traits без ошибок инстанцирования.

cpp
#include <type_traits>
template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // only compiled if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}

int x = 5;
int* p = &x;
getValue(x);   // 10
getValue(p);   // 5
getValue(3.14);  // 3.14

// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

Вариативные шаблоны и fold-выражения

Вариативные шаблоны принимают любое число аргументов через пакеты параметров (typename... Args). Fold-выражения C++17 применяют оператор ко всем элементам пакета: унарный fold (... op pack), бинарный fold (init op ... op pack). До C++17 нужна была рекурсия с базовым случаем. Вариативные шаблоны лежат в основе std::make_unique, std::tuple, printf-подобных функций.

cpp
#include <iostream>

// Parameter pack
template <typename... Args>
void print(Args... args) {
    // C++17 fold expression
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// Sum with fold
template <typename... T>
auto sum(T... args) {
    return (args + ...);  // binary fold: ((a+b)+c)+d
}

// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
    return (0 + ... + args);  // left fold with init
}

// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }

Concepts (C++20)

Concepts (C++20) заменяют SFINAE читаемыми, раскрывающими намерение ограничениями. Они дают гораздо лучшие сообщения об ошибках, чем SFINAE. Определяйте концепты через concept Name = constraint;. Используйте их в параметрах шаблона, requires-предложениях или сокращённых шаблонах (auto с концептом). Стандартная библиотека предоставляет множество полезных концептов в <concepts>.

cpp
#include <concepts>

// Define a concept
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }

// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }

// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }

// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.
17

Углублённое изучение лямбда-выражений

Режимы захвата

Лямбды захватывают переменные из охватывающей области. [=] захватывает всё по значению, [&] по ссылке — удобно, но подвержено ошибкам (висячие ссылки, непреднамеренные захваты). Предпочитайте явные захваты [x, &y] для ясности. Init-захваты [name = expr] (C++14) позволяют переименование, перемещение и вычисление захватываемых значений. Захватывайте [&] осторожно — лямбды, переживая область, вызывают висячие ссылки.

cpp
int x = 10, y = 20;
std::vector<int> v;

auto a = [] { /* no capture */ };
auto b = [x] { return x; };           // capture x by value
auto c = [&x] { x = 100; };           // capture x by reference
auto d = [=] { return x + y; };       // capture all by value
auto e = [&] { x = 1; y = 2; };       // capture all by reference
auto f = [x, &y] { return x + y; };   // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; };   // capture this (C++17: *this)
auto i = [x = x + 5] { return x; };   // init capture (C++14)

// Best practice: capture only what you need, explicitly

Обобщённые лямбды (C++14)

Обобщённые лямбды используют параметры auto (C++14) или явные параметры шаблона (C++20). Они по сути — сгенерированные компилятором перегрузки operator() шаблона. Шаблонные лямбды C++20 позволяют обращаться к параметру типа T напрямую. Рекурсивные лямбды нуждаются в std::function (или deducing this в C++23), потому что обычная auto-лямбда не может ссылаться на себя по имени до того, как её тип станет известен.

cpp
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);        // int
add(1.5, 2.5);    // double
add(std::string("a"), std::string("b"));

// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
    return v.size();
};

// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
    return process(std::forward<T>(arg));
};

// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
};

Mutable и stateful-лямбды

По умолчанию переменные, захваченные по значению в лямбде, — const, operator() является const. Ключевое слово mutable снимает эту константность, позволяя модификацию захваченных по значению переменных (изменение копии, не оригинала). Это включает stateful-лямбды вроде счётчиков. Каждая копия лямбды имеет собственное захваченное состояние. Используйте std::function для хранения лямбд разных типов в контейнерах.

cpp
#include <functional>
int counter = 0;

// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
    return ++counter;  // modifies the captured copy
};
inc(); inc();
std::cout << counter;  // still 0 (original unchanged)

// Stateful counter with std::function
auto makeCounter(int start) {
    return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c();  // 11, 12

// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });

Лямбды с STL-алгоритмами

Лямбды отлично подходят в качестве предикатов/компараторов для STL-алгоритмов. Они заменяют старый подход функторов (функциональных объектов) inline, читаемым кодом. Распространённые применения: компараторы для sort, предикаты для find_if/remove_if/copy_if, преобразования для transform. Range в C++20 делают это ещё чище через оператор канала для компонуемых преобразований.

cpp
#include <algorithm>
#include <vector>

std::vector<int> v = {5, 2, 8, 1, 9, 3};

// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

// Find first even
auto it = std::find_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });

// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 != 0; }), v.end());

// Transform
std::transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * x; });

// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*2; });

Функции высшего порядка

Лямбды включают паттерны функционального программирования в C++. Функции могут возвращать лямбды (замыкания) или принимать их как параметры. std::function оборачивает любой callable для хранения со стиранием типов. Композиция функций строит конвейеры, связывая лямбды. Этот стиль мощен для callback'ов, обработчиков событий и конвейеров преобразования данных, хотя подходы на основе шаблонов избегают накладных расходов std::function.

cpp
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
    return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5);  // 10
tripleIt(5);  // 15

// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
    std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; });  // 11

// Composition
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3);  // 10
18

Пространства имён и модули

Основы пространств имён

Пространства имён предотвращают коллизии имён в крупных проектах. Они могут быть вложенными и иметь алиасы для удобства. Анонимные пространства имён дают символам внутреннее связывание (видимы только в текущей единице трансляции), заменяя C-ключевое слово static для этой цели. Избегайте using namespace в заголовках — это загрязняет каждый включающий файл.

cpp
namespace math {
    double pi = 3.14159;
    double square(double x) { return x * x; }

    namespace geometry {  // nested
        double circleArea(double r) { return pi * square(r); }
    }
}

// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);

// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);

// Anonymous namespace: internal linkage (like 'static')
namespace {
    int internalVar = 42;  // only visible in this file
}

using-объявления и директивы

using-объявление (using std::cout) импортирует одно имя — безопасно и рекомендовано. using-директива (using namespace std) импортирует всё — удобно, но рискованно, особенно в заголовках (вызывает конфликты имён и неоднозначность). Ограничивайте using-директивы небольшой областью (функция/файлы реализации) и никогда в заголовках. using enum в C++20 импортирует все перечислители scoped enum.

cpp
#include <iostream>
#include <vector>

// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;

// using directive: brings entire namespace
using namespace std;  // brings ALL of std
// AVOID in headers! Pollutes global namespace.

// Namespace-scoped using (safe)
namespace mylib {
    using std::string;  // only affects mylib
    using std::vector;
    string s;
}

// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
    using enum Color;
    auto c = Red;  // no Color:: needed here
}

ADL (Argument-Dependent Lookup)

ADL (Koenig lookup) находит свободные функции на основе пространств имён их аргументов. Поэтому std::cout << x работает без std::operator<< — компилятор ищет в пространстве имён x. ADL необходим для перегрузок операторов и точек кастомизации вроде swap. Паттерн 'using std::swap; swap(a,b);' позволяет пользовательским типам предоставлять оптимизированные swap, откатываясь к std::swap.

cpp
namespace mylib {
    struct Widget {
        int value;
    };
    // Operator overload found via ADL
    std::ostream& operator<<(std::ostream& os, const Widget& w) {
        return os << "Widget(" << w.value << ")";
    }

    void helper(const Widget&) {}
}

int main() {
    mylib::Widget w{42};
    // ADL: finds operator<< in mylib without qualification
    std::cout << w;  // works! no mylib:: needed

    // ADL applies to free functions too
    helper(w);  // wait—needs mylib::helper unless ADL applies
    mylib::helper(w);  // explicit
}

// swap is the classic ADL use case
namespace ns { struct X {};
    void swap(X&, X&) {}  // customized swap
}
void f(ns::X& a, ns::X& b) {
    using std::swap;
    swap(a, b);  // ADL picks ns::swap if available
}

Inline-пространства имён (версионирование)

Inline-пространства имён предоставляют своих членов так, будто они в охватывающем пространстве. Это включает версионирование библиотек: сделайте новейшую версию inline, чтобы пользователи получали её по умолчанию, тогда как старые версии остаются доступными через явную квалификацию. Также используется для ABI-совместимости и переключения функций. Изменение того, какое пространство inline, сдвигает версию по умолчанию без модификации пользовательского кода.

cpp
// Inline namespace members are part of the enclosing namespace
namespace mylib {
    inline namespace v2 {
        struct Widget { int x, y, z; };  // newer version
        void process(Widget) {}
    }

    namespace v1 {
        struct Widget { int x, y; };  // older version
    }
}

// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w;  // actually mylib::v2::Widget
mylib::process(w);

// Explicitly use v1 if needed
mylib::v1::Widget oldW;

// ABI versioning: change which namespace is inline
// to change default version without breaking old code

Модули C++20

Модули C++20 заменяют #include более быстрой, надёжной системой. export module объявляет модуль; export помечает видимые объявления. Модули компилируются один раз (не перепарсиваются на каждую единицу трансляции), радикально ускоряя сборку. Они избегают загрязнения макросами и проблем порядка заголовков. Внедрение постепенное — поддержка инструментами (CMake, системы сборки) ещё созревает к 2024 году.

cpp
// math.cppm (module interface unit)
export module math;

export double pi = 3.14159;

export double square(double x) {
    return x * x;
}

// Internal (not exported)
double internalHelper(double x) {
    return x * 2;
}

// main.cpp
import math;
import std;  // standard library module (C++23)

int main() {
    return square(pi);  // 9.87
    // internalHelper(3);  // ERROR: not exported
}

// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);
19

Препроцессор и макросы

Include guards и #pragma once

Include guards предотвращают многократную обработку заголовка в одной единице трансляции, избегая ошибок переопределения. #ifndef/#define/#endif — стандартно и переносимо. #pragma once проще и избегает коллизий имён макросов, но технически нестандартно (поддерживается всеми основными компиляторами). Современный код часто использует #pragma once для простоты.

cpp
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// declarations here
class Widget { /* ... */ };

#endif  // MY_HEADER_H

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

class Widget { /* ... */ };

// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TU

Макросы, подобные функциям

Макросы, подобные функциям, — текстовая подстановка. ВСЕГДА берите в скобки каждый аргумент и всё выражение, чтобы избежать багов приоритета. Идиома do { ... } while (0) заставляет макрос вести себя как один оператор. У макросов нет проверки типов, нет области видимости, и они могут иметь побочные эффекты (MAX(i++, j++) инкрементирует дважды). В современном C++ предпочитайте constexpr/inline/templates.

cpp
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int m = MAX(3, 5);      // ((3) > (5) ? (3) : (5))
int s = SQUARE(4);      // ((4) * (4))

// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2);  // 1 + 2 * 1 + 2 = 5, not 9!

// Multi-line macro with do-while
#define LOG(msg) do { \
    std::cerr << __FILE__ << ":" << __LINE__ \
              << " " << msg << "\n"; \
} while (0)

// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);

Строкификация и конкатенация

# (строкификация) преобразует аргумент макроса в строковый литерал. ## (token pasting) объединяет два токена в один. Двухуровневый трюк STR/XSTR сначала раскрывает макросы, затем строкифицирует их. Эти операторы необходимы для генерации кода, макросов логирования и X-макросов. Современный C++ часто заменяет их constexpr-функциями, но они остаются полезными для compile-time генерации строк/токенов.

cpp
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world);  // "hello world"
const char* s2 = STR(42);           // "42"

// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION);   // "VERSION"
const char* v2 = XSTR(VERSION);  // "100"

// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10;  // int var_1 = 10;
int CONCAT(foo, bar) = 5;  // int foobar = 5;

// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0;  // int tmp_42 = 0; (if on line 42)

Условная компиляция

Условная компиляция (#if, #ifdef, #ifndef, #elif, #else, #endif) включает/исключает код во время компиляции. Используется для платформоспецифичного кода, debug-сборок и feature-флагов. defined(X) проверяет, определён ли X (значение не важно). #error прерывает компиляцию с сообщением. Предпочитайте constexpr if, когда возможно — он типобезопасен, и код всегда компилируется (ловя ошибки во всех конфигурациях).

cpp
#define DEBUG 1
#define PLATFORM "windows"

#if DEBUG
    std::cout << "Debug mode\n";
#endif

#ifdef DEBUG
    logDebug("entered function");
#endif

#ifndef NDEBUG
    assert(ptr != nullptr);
#endif

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#endif

// Check C++ standard
#if __cplusplus >= 202002L
    #define CPP20_OR_LATER
#elif __cplusplus >= 201703L
    #define CPP17_OR_LATER
#endif

// #error and #warning
#if !defined(VERSION)
    #error "VERSION must be defined"
#endif

Предопределённые макросы и __VA_OPT__

Предопределённые макросы предоставляют compile-time информацию: __FILE__/__LINE__ для логирования, __cplusplus для версии стандарта, __func__ для имён функций (это идентификатор, не макрос). __VA_OPT__ (C++20) условно вставляет запятую, только когда __VA_ARGS__ не пуст, устраняя проблему висящей запятой в вариативных макросах. __has_include (C++17) включает опциональное включение заголовков для переносимости.

cpp
// Standard predefined macros
std::cout << __FILE__;       // source file path
std::cout << __LINE__;       // current line number
std::cout << __DATE__;       // compilation date "Mmm dd yyyy"
std::cout << __TIME__;       // compilation time "hh:mm:ss"
std::cout << __cplusplus;    // C++ standard version
std::cout << __func__;       // current function name (not macro)

// Compiler-specific
#ifdef __GNUC__
    std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
    std::cout << "MSVC " << _MSC_VER;
#endif

// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
    printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain");              // printf("plain")
LOG("x=%d", x);            // printf("x=%d", x)

// __has_include (C++17): check if header exists
#if __has_include(<optional>)
    #include <optional>
#endif
20

Паттерны проектирования в C++

Singleton (Meyers' Singleton)

Singleton обеспечивает наличие у класса одного экземпляра с глобальным доступом. Meyers' Singleton (статическая локальная переменная) — чистейшая реализация на C++; потокобезопасная инициализация гарантирована с C++11. Удалите copy-операции для предотвращения дублирования. Singleton'ы противоречивы (глобальное состояние, трудно тестировать); предпочитайте внедрение зависимостей, когда возможно. Используйте, когда действительно нужен один экземпляр (логер, конфиг, аппаратный интерфейс).

cpp
class Logger {
public:
    static Logger& instance() {
        // Meyers' singleton: thread-safe in C++11+
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};

// Usage
Logger::instance().log("started");

// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safely

RAII (Resource Acquisition Is Initialization)

RAII — важнейшая идиома C++: захват ресурсов в конструкторах, освобождение в деструкторах. Это гарантирует очистку даже при бросании исключений. Умные указатели, std::lock_guard, std::fstream и std::vector используют RAII. Деструктор выполняется при раскрутке стека, делая исключающе-безопасный код естественным. RAII устраняет ручные new/delete, lock/unlock, open/close — применяйте везде.

cpp
// RAII: resource tied to object lifetime
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
        if (!fp) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp) fclose(fp); }
    FILE* get() { return fp; }
    // disable copy to prevent double-close
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // allow move
    FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};

{
    FileHandle f("data.txt");
    // use f.get()
}  // fclose called automatically, even on exception

Идиома Pimpl (Pointer to Implementation)

Pimpl (Pointer to Implementation) скрывает детали реализации за указателем. Преимущества: более быстрая компиляция (private-члены не появляются в заголовке), истинная стабильность ABI (изменение Impl не ломает ABI), уменьшение include-зависимостей. Деструктор должен быть определён в .cpp-файле, так как unique_ptr<Impl> требует полноты Impl. Распространено в дизайне библиотек для стабильных API.

cpp
// widget.h - public interface
class Widget {
public:
    Widget();
    ~Widget();  // must define in .cpp (unique_ptr needs complete type)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void doSomething();
private:
    class Impl;  // forward declaration
    std::unique_ptr<Impl> pimpl;
};

// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
    std::vector<int> data;
    void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }

Паттерн Observer

Observer позволяет субъектам уведомлять подписчиков об изменениях без связывания. std::function упрощает приём лямбд, функций-членов и функторов. Для продакшен-кода рассмотрите механизм отписки на основе токенов (вернуть ID при подписке, использовать его для удаления). Остерегайтесь висячих ссылок, если наблюдатели захватывают [&] и переживают субъект. signals/slots в Qt и events в C# — зрелые реализации.

cpp
#include <functional>
#include <vector>
#include <string>

class Subject {
    std::vector<std::function<void(const std::string&)>> observers;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        observers.push_back(cb);
    }
    void notify(const std::string& event) {
        for (auto& cb : observers) cb(event);
    }
};

// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello");  // both callbacks invoked

// For removal, use a token/ID system or observer objects
// instead of std::function

CRTP (Curiously Recurring Template Pattern)

CRTP (Derived : Base<Derived>) достигает статического полиморфизма — базовый класс настраивает поведение через производный тип. Без накладных расходов виртуальных функций. Используется для добавления функциональности (Comparable, Iterable) к производным классам через mixin'ы. Downcast через static_cast безопасен, так как шаблон гарантирует производный тип. CRTP лежит в основе std::enable_shared_from_this, std::iterator и многих policy-based дизайнов.

cpp
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equalTo(other);
    }
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};

Point a{1, 2}, b{1, 2};
a == b;  // true (uses Comparable::operator==)
a != b;  // false

// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
    static_cast<const T&>(shape).drawImpl();
}
21

Углублённое изучение умных указателей

unique_ptr

unique_ptr — единоличный владелец своего объекта. Не может копироваться, только перемещаться. Автоматически удаляется при выходе из области. make_unique — предпочтительный способ создания. Нулевые накладные расходы над сырыми указателями.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

shared_ptr

shared_ptr разрешает нескольких владельцев через подсчёт ссылок. use_count() показывает число владельцев. Потокобезопасен для счётчика, но не для объекта. Используйте make_shared для эффективности (одно выделение).

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: shared ownership
std::cout << p1.use_count();  // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the object

weak_ptr

weak_ptr — не владеющая ссылка на shared_ptr. Предотвращает циклические ссылки (утечки памяти). lock() пытается преобразовать в shared_ptr. Используйте expired() для проверки, существует ли ещё объект. Не влияет на счётчик ссылок.

cpp
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;  // Does not increase count
if (auto locked = weak.lock()) {  // Try to get shared_ptr
    std::cout << *locked;  // Use the object
} else {
    std::cout << "Object deleted";
}

Пользовательский deleter

Пользовательские deleters позволяют unique_ptr управлять не-памятью ресурсами, такими как файловые дескрипторы, сокеты и C-API. Тип deleter — часть типа unique_ptr. Полезно для RAII с C-библиотеками.

cpp
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);

enable_shared_from_this

enable_shared_from_this позволяет объекту безопасно получить shared_ptr на себя. Прямой вызов shared_ptr(this) создал бы второй счётчик ссылок, вызывая двойное удаление. Наследуйтесь от enable_shared_from_this и используйте shared_from_this().

cpp
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getPtr() {
        return shared_from_this();  // Safe
        // return std::shared_ptr<Node>(this);  // BUG: double delete
    }
};
22

Углублённое изучение move-семантики

Move-конструктор

Move-конструкторы похищают ресурсы вместо копирования. noexcept важен: STL-контейнеры перемещают (не копируют), только если move — noexcept. Всегда оставляйте moved-from объект в валидном состоянии.

cpp
class Buffer {
    int* data; size_t size;
public:
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr; other.size = 0;  // Leave in valid state
    }
    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
};

std::move

std::move — приведение к rvalue-ссылке. Он не выполняет само перемещение. Move-конструктор или оператор присваивания выполняет фактическую работу. После std::move исходный объект находится в валидном, но неопределённом состоянии.

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignment

Идеальное перенаправление

Идеальное перенаправление сохраняет категорию значения аргументов. T&& в шаблоне — forwarding reference, привязывается и к lvalue, и к rvalue. std::forward<T> приводит обратно к исходной категории. Необходимо для фабричных функций и обёрток.

cpp
template<typename T, typename Arg>
auto make(Arg&& arg) {
    return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)

RVO и NRVO

RVO (Return Value Optimization) и NRVO (Named RVO) устраняют копии, конструируя объект на месте. Компиляторы выполняют это автоматически. Использование std::move на локальном возвращаемом значении предотвращает NRVO и может ухудшить. Доверяйте компилятору.

cpp
std::string create() {
    std::string s = "Hello";
    return s;  // NRVO: no copy, no move
}
std::string s = create();  // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!

Правило пяти

Правило пяти: если вы определяете любой из деструктора, copy-конструктора/присваивания или move-конструктора/присваивания, определяйте все пять. Это обеспечивает корректное управление ресурсами. Предпочтительно Правило нуля: используйте RAII-типы (умные указатели, vector) для отказа от ручного управления.

cpp
class Resource {
public:
    Resource();  // Constructor
    ~Resource();  // Destructor
    Resource(const Resource&);  // Copy constructor
    Resource& operator=(const Resource&);  // Copy assignment
    Resource(Resource&&) noexcept;  // Move constructor
    Resource& operator=(Resource&&) noexcept;  // Move assignment
};
23

Углублённое изучение шаблонов

Вариативные шаблоны

Вариативные шаблоны принимают любое число аргументов. Fold-выражения (C++17) упрощают распаковку. Рекурсивный подход работает в C++11. sizeof...(args) даёт счёт. Используется в tuple, make_shared и заменах printf.

cpp
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';  // C++17 fold expression
}
print(1, "hello", 3.14, 'a');  // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);
}

SFINAE

SFINAE (Substitution Failure Is Not An Error) удаляет невалидные перегрузки из рассмотрения. enable_if добавляет условие к шаблону. Если условие ложно, специализация игнорируется. if constexpr в C++17 часто чище.

cpp
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>,
         typename = void>  // Different signature
void process(T value) { /* float version */ }

if constexpr

if constexpr (C++17) вычисляет условия во время компиляции. Компилируется только истинная ветка, другая отбрасывается. Гораздо чище SFINAE для условной компиляции. Работает с auto-выводом типа возврата.

cpp
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
// Only the matching branch is compiled
// No SFINAE needed

Concepts (C++20)

Concepts (C++20) ограничивают параметры шаблона читаемым синтаксисом. Они предоставляют лучшие сообщения об ошибках, чем SFINAE. Используйте существующие концепты (integral, floating_point) или определяйте пользовательские. Концепты можно комбинировать через && и ||.

cpp
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }

Специализация шаблонов

Полная специализация предоставляет полную реализацию для конкретного типа. Частичная специализация настраивает для категории (например, все указатели). Первичный шаблон должен быть объявлен первым. Специализации должны соответствовать интерфейсу.

cpp
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> {  // Full specialization
    // Bit-packed implementation
};
template<typename T>
class Vector<T*> {  // Partial specialization for pointers
    // Pointer-specific implementation
};
24

Углублённое изучение STL-алгоритмов

sort и stable_sort

sort — O(n log n), не стабилен. stable_sort сохраняет относительный порядок равных элементов. Используйте компаратор для пользовательского упорядочивания. Range в C++20 позволяют: std::ranges::sort(v, {}, &last_digit).

cpp
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());  // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // Sort by last digit
});

transform и accumulate

transform применяет функцию к каждому элементу. accumulate сворачивает элементы с операцией (по умолчанию +). Начальное значение определяет тип. Используйте std::reduce (C++17) для параллельной редукции.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
    [](int x) { return x * x; });  // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0);  // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());

find и count

find возвращает итератор на первое совпадение или end(). count возвращает число совпадений. find_if использует предикат. Все — O(n). Для отсортированных диапазонов используйте binary_search, lower_bound, upper_bound (O(log n)).

cpp
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2);  // First 2
size_t cnt = std::count(v.begin(), v.end(), 2);  // 2
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 2; });  // First > 2

copy и remove

copy_if копирует совпадающие элементы. back_inserter добавляет в назначение. remove на самом деле не удаляет; он сдвигает несоответствующие элементы вперёд и возвращает новый конец. erase удаляет остаток. C++20 добавляет std::erase для контейнеров.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
    [](int x) { return x % 2 == 0; });  // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);

minmax и clamp

minmax возвращает пару (min, max) за один проход. clamp ограничивает значение диапазоном. min/max принимают initializer list или два значения. Все принимают пользовательские компараторы. Структурированные привязки (C++17) упрощают результат.

cpp
auto [min, max] = std::minmax({3, 1, 4, 1, 5});  // C++17
int value = std::clamp(15, 0, 10);  // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });
25

Параллелизм

std::thread

std::thread создаёт потоки ОС. join() ожидает завершения (блокирующе). detach() выполняется независимо (может пережить создателя). Поток без обоих вызывает std::terminate. Передавайте аргументы по значению или используйте std::ref для ссылок.

cpp
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join();  // Wait for completion
t2.detach();  // Run independently
// Always join or detach before destruction

std::mutex

lock_guard — простой RAII: блокирует при конструировании, разблокирует при уничтожении. unique_lock гибче: может разблокировать/заблокировать снова, используется с условными переменными. Никогда не разблокируйте вручную с lock_guard. Используйте std::scoped_lock для нескольких мьютексов.

cpp
std::mutex mtx;
int shared = 0;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    ++shared;
}  // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock();  // Manual unlock
ulock.lock();    // Re-lock

std::async и future

std::async запускает функцию асинхронно, возвращая future. get() блокирует и получает результат. launch::async принуждает новый поток. launch::deferred выполняется синхронно при get(). По умолчанию может быть любым. Исключения распространяются через get().

cpp
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
    return 42;
});
int result = f.get();  // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()

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

condition_variable синхронизирует потоки. wait освобождает блокировку и блокирует до уведомления. Предикат обрабатывает ложные пробуждения. notify_one будит одного ожидающего, notify_all — всех. Всегда изменяйте разделяемое состояние под мьютексом перед уведомлением.

cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; });  // Spurious wakeups handled
// Notifier
{
    std::lock_guard<std::mutex> lk(mtx);
    ready = true;
}
cv.notify_one();  // Or notify_all()

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

atomic обеспечивает lock-free потокобезопасные операции. fetch_add/sub — read-modify-write. compare_exchange реализует CAS (compare-and-swap). memory_order_relaxed — самый быстрый, но слабейший. seq_cst (по умолчанию) — сильнейший. Используйте атомики для простых счётчиков и флагов.

cpp
#include <atomic>
std::atomic<int> counter{0};
counter++;  // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cst
26

Возможности современного C++

Структурированные привязки

Структурированные привязки (C++17) раскладывают pair, tuple и структуры. auto& для ссылок, auto для копий. Упрощают итерацию по map. Работают с любым агрегатным типом. Гораздо чище, чем .first/.second.

cpp
std::pair p = {1, "hello"};
auto [num, str] = p;  // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value;
}

std::optional

optional представляет значение, которое может существовать или нет. Лучше, чем указатели или значения-сигналы. has_value() или operator bool проверяют. value() бросает, если пуст, value_or() предоставляет значение по умолчанию. Используйте для функций, которые могут не дать результат.

cpp
#include <optional>
std::optional<int> find(bool found) {
    if (found) return 42;
    return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)

std::variant

variant — типобезопасное объединение. Хранит один из нескольких типов. visit применяет посетителя (перегруженная лямбда). index() возвращает индекс текущего типа. get_if<T> безопасно извлекает. Заменяет union и наследование для закрытых иерархий типов.

cpp
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg;
}, v);

Ranges (C++20)

Ranges (C++20) предоставляют компонуемые, ленивые views. Оператор | связывает операции. Без промежуточных контейнеров. Views ленивы: вычисление происходит при итерации. Гораздо читаемее вложенных вызовов алгоритмов.

cpp
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x;  // 4, 16

Корутины (C++20)

Корутины (C++20) включают async и генераторные паттерны. co_yield приостанавливает и возвращает значение. co_await ожидает другую корутину. co_return завершает. Компилятор преобразует корутины в конечные автоматы. Нужна реализация типа возврата (Generator, Task).

cpp
#include <coroutine>
Generator<int> counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // Suspend and yield
}
for (int x : counter()) {
    if (x > 5) break;
    std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutine
27

Частые ловушки

Висячие указатели

Висячие указатели указывают на освобождённую память. Доступ к ним — неопределённое поведение. Используйте умные указатели (unique_ptr, shared_ptr) для отказа от ручного управления памятью. Если используете сырые указатели, устанавливайте в nullptr после delete. Используйте инструменты вроде AddressSanitizer для обнаружения.

cpp
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p;  // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;

Инвалидация итераторов

Vector push_back может перераспределить, инвалидируя все итераторы. erase инвалидирует итераторы в точке и после. Итераторы list стабильны, кроме удалённых элементов. Проверяйте правила инвалидации итераторов для каждого контейнера. В сомнительных случаях используйте индексы.

cpp
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.push_back(4);  // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 2) v.push_back(4);
}

Неопределённое поведение

Неопределённое поведение (UB) позволяет компилятору делать что угодно. Неинициализированные переменные, доступ за пределами границ, разыменование null, переполнение знакового — всё это UB. Компиляторы оптимизируют, предполагая отсутствие UB, что приводит к неожиданным результатам. Используйте -fsanitize=undefined для обнаружения.

cpp
int x;  // Uninitialized: UB to read
std::cout << x;  // UB
int arr[5];
arr[5] = 0;  // Out of bounds: UB
int* p = nullptr;
*p;  // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1;  // UB

Срезка

Срезка объекта возникает, когда производный объект копируется в базовый тип. Производные части теряются. Передавайте по ссылке или указателю, чтобы избежать срезки. Виртуальные вызовы на срезанном объекте вызывают базовую версию. Всегда передавайте полиморфные типы по ссылке/указателю.

cpp
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d);  // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }

Статическая инициализация

Порядок статической инициализации между единицами трансляции не определён. Один файл может использовать ещё не инициализированную статику другого файла. Исправляется function-local statics (Meyers Singleton): инициализация ленивая и потокобезопасная (C++11+). Доступ через вызов функции.

cpp
// Static initialization order fiasco
// File1.cpp
int x = computeX();  // May use y
// File2.cpp
int y = computeY();  // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
    static int x = computeX();  // Lazy, thread-safe
    return x;
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.