Начало работы
Hello World
Каждая программа на C++ начинается в main(). <iostream> предоставляет std::cout (стандартный вывод) и std::cin (стандартный ввод). Префикс std:: относится к стандартному пространству имён; using namespace std; может избежать его, но не рекомендуется в заголовках, так как загрязняет глобальное пространство имён.
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}Переменные и auto
auto выводит тип из инициализатора (C++11). Используйте auto, когда тип очевиден или многословен (итераторы). const делает значение неизменяемым; constexpr вычисляется во время компиляции для истинных констант, встроенных в бинарник.
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 и >>, чтобы отбросить его.
#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& для параметров только для чтения, чтобы избежать дорогостоящих копий.
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 при некорректном вводе.
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);Строки
Основы std::string
std::string управляет собственной памятью и растёт по мере необходимости. В отличие от C char-массивов, вам не нужно вручную управлять длиной. .find() возвращает std::string::npos (огромное значение), если подстрока не найдена, поэтому всегда сравнивайте с npos.
#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, а не трактовайте результат как булево.
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 occurrenceStringstream
stringstream связывает строки и типизированные значения, полезен для построения форматированных строк (как буфер) или разбора токенов, разделённых пробелами. Медленнее прямых операций, но очень гибок для сериализации и десериализации.
#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", чтобы разрешить ) внутри.
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() действителен только пока строка жива и не изменена.
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Числа и математика
Целые и плавающие типы
Используйте фиксированной ширины типы <cstdint> (int32_t, int64_t), когда точный размер важен на разных платформах. Разделитель ' (C++14) улучшает читаемость больших чисел. double — плавающий тип по умолчанию, предпочтительнее float для точности.
#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 с допуском.
#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 или проверяйте границы. Для финансового кода помните, что плавающая точка неточна — рассмотрите целочисленные копейки или десятичную библиотеку.
#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.
#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 и хорошо определено.
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
}Поток управления
If / Else
C++17 вводит if с инициализатором: if (auto it = m.find(k); it != m.end()) { ... }. Это ограничивает переменную блоком if/else, сохраняя окружающую область чистой и избегая случайного повторного использования.
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.
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& (неконстантная ссылка).
// 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 copyWhile и do-while
while проверяет условие перед выполнением; do-while выполняет тело хотя бы один раз перед проверкой. do-while полезен для валидации ввода и циклов меню, где тело должно выполняться до того, как условие можно оценить.
int n = 5;
while (n > 0) {
std::cout << n-- << " ";
}
int x;
do {
std::cin >> x;
} while (x < 0); // runs at least onceBreak, continue и вложенные циклы
break выходит из ближайшего охватывающего цикла; continue переходит к следующей итерации. В C++ нет помеченного break как в Java; используйте переменную-флаг или извлеките цикл в функцию и используйте return для выхода из вложенных циклов.
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; }
}
}Функции и лямбды
Определение и множественные возвраты
Структурированные привязки C++17 (auto [a, b] = ...) чисто распаковывают tuple, pair и структуры. До C++17 исп ользуйте std::tie или выходные параметры. Возврат по значению дешев благодаря move-семантике (RVO), которая устраняет копирование.
#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 (правила одного определения).
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Перегрузка функций
Перегрузка позволяет функциям иметь одно имя, но различаться типами параметров. Компилятор выбирает лучшее совпадение через разрешение перегрузки. Неоднозначные перегрузки вызывают ошибки компиляции; предпочитайте шаблоны, когда тело одинаково для всех типов.
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'ов. Остерегайтесь висячих ссылок при захвате по ссылке.
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 в контейнерах.
#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Контейнеры STL
vector
vector — динамический массив и контейнер по умолчанию. push_back амортизированно O(1). .at() проверяет границы (бросает std::out_of_range), operator[] — нет. Вызовите reserve() заранее, если знаете размер, чтобы избежать перераспределений.
#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 неупорядочен.
#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 позволяют запросы диапазона на упорядоченных множествах.
#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 >= 3array и deque
array — массив фиксированного размера, размещённый в стеке, с STL-интерфейсом (безопаснее C-массивов, без decay к указателю). deque (двусторонняя очередь) поддерживает O(1) push/pop с обоих концов, в отличие от vector, который O(n) спереди.
#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, 4tuple и pair
tuple хранит гетерогенные значения любых типов. pair — это tuple из 2 элементов. Структурированные привязки (C++17) раскладывают их в именованные переменные. Распространено при итерации map, чьи элементы — пары (ключ, значение).
#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);Указатели и память
Сырые указатели
Указатели хранят адреса памяти. & получает адрес, * разыменовывает. Арифметика указателей работает с массивами. Сырые указатели не отслеживают владение, что приводит к утечкам и висячим указателям — предпочитайте умные указатели для owned-ресурсов.
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 — осмысленное состояние или когда нужно перепривязать, на что указываете.
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 nullunique_ptr
unique_ptr — единоличное владение heap-объектом. Не может копироваться, только перемещаться. Автоматически удаляется при выходе из области видимости (RAII). Это умный указатель по умолчанию для большинства случаев — нулевые накладные расходы над сырым указателем.
#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 nullptrshared_ptr и weak_ptr
shared_ptr использует подсчёт ссылок; объект освобождается, когда последний shared_ptr уничтожен. weak_ptr наблюдает, не влияя на счётчик, разрывая циклы ссылок. Избегайте циклов shared_ptr (они утекают, потому что счётчик никогда не достигает нуля).
#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 за вас.
// 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Классы и ООП
Класс и конструктор
Список инициализации членов (: name(...), age(...)) инициализирует членов до тела, эффективнее, чем присваивание в теле. Помечайте геттеры const, чтобы разрешить вызов на const-объектах. std::move избегает копирования строкового параметра.
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 по умолчанию и предоставляйте только необходимое.
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 ловит опечатки.
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. Конкретные подклассы должны реализовать все чистые виртуальные функции или останутся абстрактными.
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.
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)Шаблоны и обобщённое программирование
Функциональные шаблоны
Функциональные шаблоны генерируют типоспецифичные версии во время компиляции. Компилятор выводит T из аргументов; вы можете указать его явно. Шаблоны — абстракции с нулевой стоимостью — без накладных расходов во время выполнения, но они увеличивают время компиляции и размер бинарника.
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) — все шаблоны. Код шаблона должен быть в заголовках (или используйте явное инстанцирование), потому что компилятору нужно полное определение для генерации кода.
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");Специализация шаблонов
Полная специализация предоставляет пользовательскую реализацию для конкретного типа. Частичная специализация (только для шаблонов классов) настраивает для категории типов (например, все типы указателей). Полезно для оптимизации или особой обработки поведения.
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.
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.14Concepts (C++20)
Concepts (C++20) ограничивают параметры шаблона читаемыми требованиями, заменяя замысловатые enable_if/SFINAE. Они дают гораздо более понятные сообщения об ошибках, когда ограничения не соблюдены. Используйте стандартные концепты, такие как std::integral, std::floating_point, std::convertible_to.
#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; }Алгоритмы STL
sort и find
STL-алгоритмы работают с диапазонами итераторов [begin, end). sort — O(n log n). find — линейный; для отсортированных диапазонов используйте binary_search/lower_bound (O(log n)). Передавайте пользовательские компараторы (лямбды или std::greater) для пользовательского упорядочивания.
#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.
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 8accumulate и count
accumulate (из <numeric>) сворачивает диапазон с операцией. Третий аргумент — начальное значение, определяющее тип результата — используйте 0.0 для double-сумм. count возвращает, сколько элементов равно значению; count_if использует предикат.
#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); // 1copy, remove и unique
remove на самом деле не стирает — он сдвигает несоответствующие элементы вперёд и возвращает новый конечный итератор. Сочетайте с .erase() для идиомы erase-remove. unique аналогично уплотняет последовательные дубликаты; сначала sort для полной дедупликации.
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 3min, max и clamp
min/max возвращают меньшее/большее из двух значений или initializer list. minmax возвращает оба как pair. clamp (C++17) ограничивает значение диапазоном, заменяя ручные проверки границ if/else — полезно для валидации ввода и UI-координат.
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Обработка ошибок
Исключения: try/catch
Бросайте исключения по значению, ловите по const-ссылке, чтобы избежать среза. Ловля std::exception ловит все стандартные исключения через базовый класс. Исключения — для исключительных случаев, не для обычного потока управления — они имеют накладные расходы при бросании.
#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-блоками.
#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() работал.
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 гарантирует, что деструкторы выполняются при раскрутке стека, поэтому ресурсы освобождаются даже при распространении исключений вверх по стеку вызовов.
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), поэтому не используйте его для проверок в продакшене. Используйте для внутренних инвариантов, указывающих на баги. Для пользовательской валидации бросайте исключения или возвращайте коды ошибок.
#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Файловый ввод-вывод и потоки
Чтение файла
ifstream открывает файл для чтения. Всегда проверяйте, удалось ли открытие (!file равно true при ошибке). getline читает построчно вместе с пробелами. Деструктор потока закрывает файл автоматически (RAII), поэтому ручное закрытие не требуется.
#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 (который также сбрасывает).
#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 и разбора токенов.
#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 структур различаются).
#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 альтернатива с тем же синтаксисом.
#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;Умные указатели
unique_ptr — эксклюзивное владение
unique_ptr — умный указатель по умолчанию; используйте, когда одного владельца достаточно. Нулевые накладные расходы над сырыми указателями. make_unique предпочтителен (исключительно безопасен). Нельзя копировать, только перемещать. Пользовательские deleters обеспечивают RAII для C-ресурсов, таких как FILE* или сокеты.
#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 из-за атомарного счётчика и уп равляющего блока. Используйте, когда владение действительно разделяемое.
#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 objectweak_ptr — разрыв циклов
weak_ptr — не владеющий наблюдатель shared_ptr. Не увеличивает счётчик. Используйте lock() для временного получения shared_ptr (возвращает null, если объект уничтожен). Необходим для разрыва циклов ссылок (например, двусвязные списки, отношения родитель-потомок), которые вызвали бы утечки памяти.
#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.
#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 choiceenable_shared_from_this
Когда объекту нужно вернуть shared_ptr на себя, enable_shared_from_this предоставляет безопасный shared_from_this(). Прямой вызов shared_ptr<T>(this) создал бы второй управляющий блок, что привело бы к двойному освобождению. Объект уже должен управляться shared_ptr, иначе shared_from_this() бросает bad_weak_ptr.
#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 = 2Move-семантика и rvalue-ссылки
Lvalue, rvalue и ссылки
Lvalue имеют идентичность и существуют за пределами одного выражения (именованные объекты). Rvalue — временные или литеральные значения. T& привязывается к lvalue, T&& — к rvalue. const T& особенная — привязывается к обоим. Понимание этого различия — основа move-семантики.
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 rvaluestd::move и move-конструкторы
std::move ничего не перемещает — он приводит к rvalue, позволяя выбрать move-конструктор/присваивание. Move-операции должны быть noexcept, чтобы контейнеры могли использовать их при перераспределении (иначе они откатываются к копированию для безопасности исключений). После move исходный объект находится в валидном, но неопределённом состоянии.
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&&. Необходимо для фабричных функций и обёрток.
#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, умные указатели), чтобы сгенерированные компилятором специальные члены были корректны. Это устраняет подверженное ошибкам ручное управление ресурсами.
// 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. Просто возвращайте локальную переменную по имени и позвольте компилятору оптимизировать.
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
}Параллелизм (thread, mutex, async)
Основы std::thread
std::thread запускает новый поток ОС. Вы ОБЯЗАНЫ вызвать join() (ожидание) или detach() (позволить работать независимо) до уничтожения объекта потока, иначе вызывается std::terminate. Аргументы по умолчанию передаются по значению — используйте std::ref для ссылок, std::move для move-only типов. Предпочитайте join, если нет веской причины detach.
#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 даёт больше гибкости (ручная блокировка/разблокировка, отложенное блокирование) для использования с условными переменными.
#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.
#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 перед разрушением, иначе деструктор может заблокировать.
#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 (по умолчанию, сильнейший). Используйте атомики для счётчиков/флагов; мьютекс — для сложных критических секций.
#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Шаблонное метапрограммирование
Специализация шаблонов
Специализация шаблонов предоставляет пользовательские реализации для конкретных типов. Полная специализация фиксирует все параметры шаблона. Частичная специализация (только для шаблонов классов) специализирует некоторые параметры, оставляя остальные обобщёнными. Широко используется в type traits, std::vector<bool> и оптимизации для известных типов.
// 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 более чистым синтаксисом.
#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 без ошибок инстанцирования.
#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-подобных функций.
#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>.
#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.Углублённое изучение лямбда-выражений
Режимы захвата
Лямбды захватывают переменные из охватывающей области. [=] захватывает всё по значению, [&] по ссылке — удобно, но подвержено ошибкам (висячие ссылки, непреднамеренные захваты). Предпочитайте явные захваты [x, &y] для ясности. Init-захваты [name = expr] (C++14) позволяют переименование, перемещение и вычисление захватываемых значений. Захватывайте [&] осторожно — лямбды, переживая область, вызывают висячие ссылки.
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-лямбда не может ссылаться на себя по имени до того, как её тип станет известен.
// 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 для хранения лямбд разных типов в контейнерах.
#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 делают это ещё чище через оператор канала для компонуемых преобразований.
#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.
#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Пространства имён и модули
Основы пространств имён
Пространства имён предотвращают коллизии имён в крупных проектах. Они могут быть вложенными и иметь алиасы для удобства. Анонимные пространства имён дают символам внутреннее связывание (видимы только в текущей единице трансляции), заменяя C-ключевое слово static для этой цели. Избегайте using namespace в заголовках — это загрязняет каждый включающий файл.
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.
#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.
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, сдвигает версию по умолчанию без модификации пользовательского кода.
// 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 году.
// 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);Препроцессор и макросы
Include guards и #pragma once
Include guards предотвращают многократную обработку заголовка в одной единице трансляции, избегая ошибок переопределения. #ifndef/#define/#endif — стандартно и переносимо. #pragma once проще и избегает коллизий имён макросов, но технически нестандартно (поддерживается всеми основными компиляторами). Современный код часто использует #pragma once для простоты.
// 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.
// 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 генерации строк/токенов.
// # (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, когда возможно — он типобезопасен, и код всегда компилируется (ловя ошибки во всех конфигурациях).
#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) включает опциональное включение заголовков для переносимости.
// 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Паттерны проектирования в C++
Singleton (Meyers' Singleton)
Singleton обеспечивает наличие у класса одного экземпляра с глобальным доступом. Meyers' Singleton (статическая локальная переменная) — чистейшая реализация на C++; потокобезопасная инициализация гарантирована с C++11. Удалите copy-операции для предотвращения дублирования. Singleton'ы противоречивы (глобальное состояние, трудно тестировать); предпочитайте внедрение зависимостей, когда возможно. Используйте, когда действительно нужен один экземпляр (логер, конфиг, аппаратный интерфейс).
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-safelyRAII (Resource Acquisition Is Initialization)
RAII — важнейш ая идиома C++: захват ресурсов в конструкторах, освобождение в деструкторах. Это гарантирует очистку даже при бросании исключений. Умные указатели, std::lock_guard, std::fstream и std::vector используют RAII. Деструктор выполняется при раскрутке стека, делая исключающе-безопасный код естественным. RAII устраняет ручные new/delete, lock/unlock, open/close — применяйте везде.
// 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.
// 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# — зрелые реализации.
#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::functionCRTP (Curiously Recurring Template Pattern)
CRTP (Derived : Base<Derived>) достигает статического полиморфизма — базовый класс настраивает поведение через производный тип. Без накладных расходов виртуальных функций. Используется для добавления функциональности (Comparable, Iterable) к производным классам через mixin'ы. Downcast через static_cast безопасен, так как шаблон гарантирует производный тип. CRTP лежит в основе std::enable_shared_from_this, std::iterator и многих policy-based дизайнов.
// 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();
}Углублённое изучение умных указателей
unique_ptr
unique_ptr — единоличный владелец своего объекта. Не может копироваться, только перемещаться. Автоматически удаляется при выходе из области. make_unique — предпочтительный способ создания. Нулевые накладные расходы над сырыми указателями.
#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 nullptrshared_ptr
shared_ptr разрешает нескольких владельцев через подсчёт ссылок. use_count() показывает число владельцев. Потокобезопасен для счётчика, но не для объекта. Используйте make_shared для эффективности (одно выделение).
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 objectweak_ptr
weak_ptr — не владеющая ссылка на shared_ptr. Предотвращает циклические ссылки (утечки памяти). lock() пытается преобразовать в shared_ptr. Используйте expired() для проверки, существует ли ещё объект. Не влияет на счётчик ссылок.
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-библиотеками.
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().
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
}
};Углублённое изучение move-семантики
Move-конструктор
Move-конструкторы похищают ресурсы вместо копирования. noexcept важен: STL-контейнеры перемещают (не копируют), только если move — noexcept. Всегда оставляйте moved-from объект в валидном состоянии.
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 исходный объект находится в валидном, но неопределённом состоянии.
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> приводит обратно к исходной категории. Необходимо для фабричных функций и обёрток.
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 и может ухудшить. Доверяйте компилятору.
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) для отказа от ручного управления.
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
};Углублённое изучен ие шаблонов
Вариативные шаблоны
Вариативные шаблоны принимают любое число аргументов. Fold-выражения (C++17) упрощают распаковку. Рекурсивный подход работает в C++11. sizeof...(args) даёт счёт. Используется в tuple, make_shared и заменах printf.
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 часто чище.
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-выводом типа возврата.
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 neededConcepts (C++20)
Concepts (C++20) ограничивают параметры шаблона читаемым синтаксисом. Они предоставляют лучшие сообщения об ошибках, чем SFINAE. Используйт е существующие концепты (integral, floating_point) или определяйте пользовательские. Концепты можно комбинировать через && и ||.
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; }Специализация шаблонов
Полная специализация предоставляет полную реализацию для конкретного типа. Частичная специализация настраивает для категории (например, все указатели). Первичный шаблон должен быть объявлен первым. Специализации должны соответствовать интерфейсу.
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
};Углублённое изучение STL-алгоритмов
sort и stable_sort
sort — O(n log n), не стабилен. stable_sort сохраняет относительный порядок равных элементов. Используйте компаратор для пользовательского упорядочивания. Range в C++20 позволяют: std::ranges::sort(v, {}, &last_digit).
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) для параллельной редукции.
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)).
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 > 2copy и remove
copy_if копирует совпадающие элементы. back_inserter добавляет в назначение. remove на самом деле не удаляет; он сдвигает несоответствующие элементы вперёд и возвращает новый конец. erase удаляет остаток. C++20 добавляет std::erase для контейнеров.
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) упрощают результат.
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; });Параллелизм
std::thread
std::thread создаёт потоки ОС. join() ожидает завершения (блокирующе). detach() выполняется независимо (может пережить создателя). Поток без обоих вызывает std::terminate. Передавайте аргументы по значению или используйте std::ref для ссылок.
#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 destructionstd::mutex
lock_guard — простой RAII: блокирует при конструировании, разблокирует при уничтожении. unique_lock гибче: может разблокировать/заблокировать снова, используется с условными переменными. Никогда не разблокируйте вручную с lock_guard. Используйте std::scoped_lock для нескольких мьютексов.
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-lockstd::async и future
std::async запускает функцию асинхронно, возвращая future. get() блокирует и получает результат. launch::async принуждает новый поток. launch::deferred выполняется синхронно при get(). По умолчанию может быть любым. Исключения распространяются через get().
#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 — всех. Всегда изменяйте разделяемое состояние под мьютексом перед уведомлением.
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 (по умолчанию) — сильнейший. Используйте атомики для простых счётчиков и флагов.
#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Возможности современного C++
Структурированные привязки
Структурированные привязки (C++17) раскладывают pair, tuple и структуры. auto& для ссылок, auto для копий. Упрощают итерацию по map. Работают с любым агрегатным типом. Гораздо чище, чем .first/.second.
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() предоставляет значение по умолчанию. Используйте для функций, которые могут не дать результат.
#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 и наследование для закрытых иерархий типов.
#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 ленивы: вычисление происходит при итерации. Гораздо читаемее вложенных вызовов алгоритмов.
#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).
#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Частые ловушки
Висячие указатели
Висячие указатели указывают на освобождённую память. Доступ к ним — неопределённое поведение. Используйте умные указатели (unique_ptr, shared_ptr) для отказа от ручного управления памятью. Если используете сырые указатели, устанавливайте в nullptr после delete. Используйте инструменты вроде AddressSanitizer для обнаружения.
// 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 стабильны, кроме удалённых элементов. Проверяйте правила инвалидации итераторов для каждого контейнера. В сомнительных случаях используйте индексы.
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 для обнаружения.
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Срезка
Срезка объекта возникает, когда производный объект копируется в базовый тип. Производные части теряются. Передавайте по ссылке или указателю, чтобы избежать срезки. Виртуальные вызовы на срезанном объекте вызывают базовую версию. Всегда передавайте полиморфные типы по ссылке/указателю.
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+). Доступ через вызов функции.
// 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;
}Связанные сниппеты C++
Copy-paste ready code for common tasks.
Умные указатели
unique_ptr, shared_ptr, weak_ptr.
RAII
Resource Acquisition Is Initialization.
Семантика перемещения
Rvalue-ссылки и move-конструкторы.
Лямбда-выражения
Лямбды и захваты.
Шаблоны
Шаблоны функций и классов.
Контейнеры STL
Распространённые операции с контейнерами.
Алгоритмы STL
Распространённые функции алгоритмов.
Итератор
Типы и использование итераторов.
Обработка исключений
try-catch и пользовательские исключения.
Многопоточность
thread, mutex, condition_variable.
Файловый ввод-вывод
Опе рации чтения и записи файлов.
Строки
Операции std::string.
Регулярные выражения
Сопоставление и замена std::regex.
Вывод типов
auto, decltype, вывод шаблонов.
constexpr
Константы и вычисления времени компиляции.
Was this helpful?