Примеры решений в С++
1. Базовые структуры управления и ввод-вывод
1.1. Безопасное чтение строки с std::getline и cin.ignore()
#include <iostream>
#include <string>
std::string safe_getline() {
std::string line;
std::getline(std::cin, line);
return line;
}
int main() {
std::cout << "Имя: ";
std::string name = safe_getline();
std::cout << "Фамилия: ";
std::string surname = safe_getline();
std::cout << "Привет, " << name << ' ' << surname << "!\n";
}
⚠️ Важно: после
cin >>остаётся символ\n, который захватываетсяgetline. Используйтеcin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'), если смешиваете операторы.
1.2. Чтение чисел с валидацией
#include <iostream>
#include <limits>
template<typename T>
T input_number(const std::string& prompt) {
T value;
while (true) {
std::cout << prompt;
if (std::cin >> value) {
break;
}
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Некорректный ввод. Повторите.\n";
}
// Сброс остатка строки (на случай, если было что-то вроде "123 abc")
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return value;
}
// Использование:
// int n = input_number<int>("Введите целое: ");
// double x = input_number<double>("Введите дробное: ");
2. RAII и управление ресурсами
2.1. Кастомный RAII-объект для временного отключения std::cout
#include <iostream>
#include <streambuf>
class CoutGuard {
std::streambuf* old_buf;
std::ostringstream silent_stream;
public:
CoutGuard() : old_buf(std::cout.rdbuf(silent_stream.rdbuf())) {}
~CoutGuard() { std::cout.rdbuf(old_buf); }
std::string captured() const { return silent_stream.str(); }
};
// Пример:
/*
{
CoutGuard guard;
std::cout << "Это не будет выведено\n";
std::string captured = guard.captured(); // "Это не будет выведено\n"
}
*/
2.2. Умный указатель с пользовательским делетером (для C-библиотек)
#include <memory>
#include <cstdio>
auto make_file(const char* path, const char* mode) {
return std::unique_ptr<FILE, decltype(&std::fclose)>(
std::fopen(path, mode),
&std::fclose
);
}
// Использование:
/*
auto f = make_file("log.txt", "w");
if (f) {
std::fprintf(f.get(), "Hello, RAII!\n");
}
// fclose вызовется автоматически
*/
3. STL: контейнеры, алгоритмы, итераторы
3.1. Удаление по условию (erase-remove idiom)
#include <vector>
#include <algorithm>
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Удалить чётные
v.erase(
std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }),
v.end()
);
🔁 Для
std::listпредпочтительно использоватьlist::remove_if(O(n), без перемещения).
3.2. Разделение строки по разделителю (C++17)
#include <string>
#include <vector>
#include <string_view>
std::vector<std::string_view> split(std::string_view str, char delim = ' ') {
std::vector<std::string_view> result;
size_t start = 0;
size_t end = 0;
while ((end = str.find(delim, start)) != std::string_view::npos) {
if (end != start) { // избегаем пустых частей
result.emplace_back(str.substr(start, end - start));
}
start = end + 1;
}
if (start < str.size()) {
result.emplace_back(str.substr(start));
}
return result;
}
// Пример:
// auto parts = split("a,b,c", ','); // {"a", "b", "c"}
💡 Возвращает
string_view— эффективно для readonly-обработки без аллокаций.
3.3. Группировка элементов (аналог GROUP BY)
#include <vector>
#include <map>
#include <string>
#include <algorithm>
struct Person {
std::string name;
std::string city;
};
std::map<std::string, std::vector<Person>> group_by_city(const std::vector<Person>& people) {
std::map<std::string, std::vector<Person>> groups;
for (const auto& p : people) {
groups[p.city].push_back(p);
}
return groups;
}
// C++20: можно использовать std::ranges::views::group_by (если доступен)
4. Шаблоны и метапрограммирование
4.1. Статический assert для enum-типов (проверка exhaustiveness)
enum class Color { Red, Green, Blue };
constexpr const char* to_string(Color c) {
switch (c) {
case Color::Red: return "Red";
case Color::Green: return "Green";
case Color::Blue: return "Blue";
default: static_assert(false, "Non-exhaustive switch"); // ❌ не сработает (false всегда)
}
}
✅ Исправленный вариант (C++17):
template<Color C>
constexpr const char* color_name() {
if constexpr (C == Color::Red) return "Red";
else if constexpr (C == Color::Green) return "Green";
else if constexpr (C == Color::Blue) return "Blue";
else static_assert(false, "Unhandled Color variant");
}
// Использование: color_name<Color::Red>()
✅ Альтернатива: использовать
std::to_underlying+switch+default: __builtin_unreachable()(GCC/Clang) +-Wswitch-enum.
4.2. CRTP: статический полиморфизм
template<typename Derived>
struct Drawable {
void draw() {
static_cast<Derived*>(this)->drawImpl();
}
};
struct Circle : Drawable<Circle> {
void drawImpl() { std::cout << "Drawing Circle\n"; }
};
struct Square : Drawable<Square> {
void drawImpl() { std::cout << "Drawing Square\n"; }
};
// Использование:
// Circle c; c.draw(); // → "Drawing Circle"
Подходит для виртуализации без виртуальной таблицы — нулевые накладные расходы.
5. Move semantics и эффективное владение
5.1. Move-only класс (например, для уникального ресурса)
#include <memory>
#include <utility>
class UniqueResource {
std::unique_ptr<int> data_;
public:
explicit UniqueResource(int v) : data_(std::make_unique<int>(v)) {}
// Запрет копирования
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
// Разрешение перемещения
UniqueResource(UniqueResource&&) noexcept = default;
UniqueResource& operator=(UniqueResource&&) noexcept = default;
int value() const { return *data_; }
};
5.2. Передача временных объектов через std::move в функции
#include <string>
#include <vector>
class Repository {
std::vector<std::string> items_;
public:
// Приём rvalue — перемещаем
void add(std::string&& item) {
items_.push_back(std::move(item)); // избегаем копирования строки
}
// Приём lvalue — копируем
void add(const std::string& item) {
items_.emplace_back(item);
}
};
// Использование:
// repo.add("temp"); // вызывает add(string&&)
// std::string s = "val"; repo.add(s); // вызывает add(const string&)
💡 Лучше: использовать универсальную ссылку + perfect forwarding (см. ниже).
5.3. Perfect forwarding и factory-функция
#include <memory>
#include <utility>
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_resource(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
// Пример:
// auto ptr = make_unique_resource<std::vector<int>>(10, 42); // 10 элементов по 42
✅
std::make_unique— стандартное решение с C++14. Пример показывает принцип.
6. Работа с временем (C++17 chrono)
6.1. Измерение времени выполнения
#include <chrono>
#include <iostream>
template<typename Func>
auto time_it(Func&& f) {
auto start = std::chrono::steady_clock::now();
std::invoke(std::forward<Func>(f));
auto end = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start);
}
// Пример:
/*
auto dur = time_it([]{ heavy_computation(); });
std::cout << "Выполнено за " << dur.count() << " мкс\n";
*/
6.2. Таймер с обратным вызовом (на основе std::thread + std::this_thread::sleep_for)
#include <thread>
#include <chrono>
#include <functional>
class Timer {
std::thread thread_;
public:
Timer(std::chrono::milliseconds delay, std::function<void()> cb)
: thread_([delay, cb = std::move(cb)]() mutable {
std::this_thread::sleep_for(delay);
cb();
}) {}
~Timer() {
if (thread_.joinable()) {
thread_.join();
}
}
Timer(const Timer&) = delete;
Timer& operator=(const Timer&) = delete;
Timer(Timer&&) = default;
Timer& operator=(Timer&&) = default;
};
// Использование:
// Timer t(2000ms, []{ std::cout << "Прошло 2 сек!\n"; });
// (блокирует деструктор до завершения)
⚠️ Это упрощённый пример. Для продакшена — лучше использовать
asio::steady_timerилиstd::jthread(C++20).
7. Многопоточность и синхронизация
7.1. Безопасный потокобезопасный контейнер (обёртка через std::mutex)
#include <mutex>
#include <vector>
#include <shared_mutex> // C++17
template<typename T>
class ThreadSafeVector {
mutable std::shared_mutex mutex_;
std::vector<T> data_;
public:
void push_back(const T& value) {
std::lock_guard lock(mutex_);
data_.push_back(value);
}
void push_back(T&& value) {
std::lock_guard lock(mutex_);
data_.push_back(std::move(value));
}
size_t size() const {
std::shared_lock lock(mutex_);
return data_.size();
}
std::vector<T> snapshot() const {
std::shared_lock lock(mutex_);
return data_; // копия — caller получает консистентный снимок
}
// Доступ по индексу с проверкой границ
std::optional<T> at(size_t idx) const {
std::shared_lock lock(mutex_);
if (idx < data_.size()) {
return data_[idx];
}
return std::nullopt;
}
};
✅ Используется
shared_mutexдля разделения чтения/записи: несколько читателей — один писатель.
🔁 Альтернатива:std::mutex, если читателей мало и overheadshared_mutexне оправдан.
7.2. Асинхронный вызов с std::async и обработка исключений
#include <future>
#include <stdexcept>
#include <iostream>
int risky_computation(int n) {
if (n < 0) throw std::invalid_argument("n must be non-negative");
return n * n;
}
int main() {
auto fut = std::async(std::launch::async, risky_computation, -5);
try {
int result = fut.get(); // get() бросает исключение, если оно было в потоке
std::cout << "Результат: " << result << "\n";
} catch (const std::exception& e) {
std::cerr << "Ошибка: " << e.what() << "\n";
}
}
✅
std::asyncсstd::launch::asyncгарантирует запуск в отдельном потоке (в отличие отdeferred).
⚠️ Если не вызвать.get()или.wait(), деструкторfutureзаблокируется.
7.3. std::atomic для флагов и счётчиков
#include <atomic>
#include <thread>
#include <vector>
std::atomic<bool> stop_flag{false};
std::atomic<int> counter{0};
void worker(int id) {
while (!stop_flag.load(std::memory_order_relaxed)) {
++counter; // memory_order по умолчанию — seq_cst (безопасно)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::cout << "Поток " << id << " завершён\n";
}
// Запуск:
/*
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(worker, i);
}
std::this_thread::sleep_for(1s);
stop_flag.store(true);
for (auto& t : threads) t.join();
std::cout << "Счётчик: " << counter.load() << "\n";
*/
🔧 Для инкремента в высоконагруженных сценариях лучше
fetch_add(1, std::memory_order_relaxed).
8. Работа с файловой системой (filesystem, C++17)
8.1. Рекурсивный обход каталога и фильтрация по расширению
#include <filesystem>
#include <vector>
#include <string>
namespace fs = std::filesystem;
std::vector<fs::path> find_files_by_extension(
const fs::path& root,
const std::string& ext // например, ".cpp"
) {
std::vector<fs::path> matches;
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (entry.is_regular_file() && entry.path().extension() == ext) {
matches.push_back(entry.path());
}
}
return matches;
}
// Использование:
// auto cpp_files = find_files_by_extension("/src", ".cpp");
⚠️
recursive_directory_iteratorможет выброситьfilesystem_errorпри недоступных правах.
8.2. Создание временного каталога с гарантией удаления (RAII)
#include <filesystem>
#include <random>
#include <stdexcept>
class TempDirectory {
fs::path path_;
static fs::path generate_unique_path() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<uint64_t> dis;
auto parent = fs::temp_directory_path();
for (int attempts = 0; attempts < 100; ++attempts) {
auto name = "tmp_" + std::to_string(dis(gen));
auto candidate = parent / name;
if (!fs::exists(candidate)) {
fs::create_directory(candidate);
return candidate;
}
}
throw std::runtime_error("Failed to create unique temp dir");
}
public:
TempDirectory() : path_(generate_unique_path()) {}
~TempDirectory() { fs::remove_all(path_); }
const fs::path& path() const noexcept { return path_; }
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
TempDirectory(TempDirectory&&) = default;
TempDirectory& operator=(TempDirectory&&) = default;
};
✅ Используется
remove_all— рекурсивное удаление. Гарантируется даже при исключении.
9. Обработка ошибок без исключений (до C++23)
9.1. Простой expected-подобный контейнер (до std::expected в C++23)
#include <variant>
#include <optional>
#include <system_error>
template<typename T, typename E = std::error_code>
class Expected {
std::variant<T, E> data_;
public:
// Конструктор успеха
Expected(T&& value) : data_(std::move(value)) {}
Expected(const T& value) : data_(value) {}
// Конструктор ошибки
Expected(E&& error) : data_(std::move(error)) {}
Expected(const E& error) : data_(error) {}
bool has_value() const { return std::holds_alternative<T>(data_); }
explicit operator bool() const { return has_value(); }
const T& value() const & {
if (!has_value()) throw std::bad_variant_access{};
return std::get<T>(data_);
}
T&& value() && {
if (!has_value()) throw std::bad_variant_access{};
return std::move(std::get<T>(data_));
}
const E& error() const {
if (has_value()) throw std::bad_variant_access{};
return std::get<E>(data_);
}
};
🔁 В C++23 — используйте
#include <expected>иstd::expected<T, E>.
🔧 Альтернатива:std::optional<T>+ отдельный код ошибки илиstd::pair<bool, T>— но это менее выразительно.
9.2. Функция с возвратом кода ошибки (POSIX-стиль)
#include <cerrno>
#include <cstring>
#include <string>
struct ReadResult {
std::string data;
int error_code = 0; // 0 — успех
};
ReadResult read_file(const std::string& path) {
ReadResult res;
FILE* f = std::fopen(path.c_str(), "rb");
if (!f) {
res.error_code = errno;
return res;
}
if (std::fseek(f, 0, SEEK_END) != 0) {
res.error_code = errno;
std::fclose(f);
return res;
}
long size = std::ftell(f);
if (size < 0) {
res.error_code = errno;
std::fclose(f);
return res;
}
std::rewind(f);
res.data.resize(static_cast<size_t>(size));
size_t nread = std::fread(res.data.data(), 1, static_cast<size_t>(size), f);
std::fclose(f);
if (nread != static_cast<size_t>(size)) {
res.error_code = errno ? errno : EIO;
res.data.clear();
}
return res;
}
// Использование:
/*
auto r = read_file("test.txt");
if (r.error_code) {
std::cerr << "Ошибка: " << std::strerror(r.error_code) << "\n";
} else {
std::cout << "Прочитано " << r.data.size() << " байт\n";
}
*/
✅ Подходит для системных утилит, embedded, или когда исключения отключены (
-fno-exceptions).
10. Тестирование и отладка
10.1. Минималистичный compile-time unit-test через static_assert
constexpr bool test_factorial() {
auto fact = [](int n) -> long long {
long long res = 1;
for (int i = 2; i <= n; ++i) res *= i;
return res;
};
return fact(0) == 1 &&
fact(1) == 1 &&
fact(5) == 120 &&
fact(10) == 3628800;
}
static_assert(test_factorial(), "factorial implementation is broken");
✅ Выполняется на этапе компиляции. Не создаёт runtime overhead.
10.2. Простой runtime-assert с выводом в stderr
#include <iostream>
#include <source_location> // C++20
#define CHECK(cond) do { \
if (!(cond)) { \
std::cerr << "[ASSERT FAILED] " << #cond \
<< " at " << std::source_location::current().file_name() \
<< ":" << std::source_location::current().line() << "\n"; \
std::abort(); \
} \
} while (false)
// Использование:
// int x = 5; CHECK(x > 0);
🔁 Для C++17 — замените
source_locationна__FILE__,__LINE__и макрос:
#define CHECK_IMPL(cond, file, line) do { \
if (!(cond)) { \
std::cerr << "[ASSERT FAILED] " << #cond \
<< " at " << (file) << ":" << (line) << "\n"; \
std::abort(); \
} \
} while (false)
#define CHECK(cond) CHECK_IMPL(cond, __FILE__, __LINE__)
11. Сериализация и парсинг
11.1. Простой бинарный протокол (фиксированная структура)
#include <cstdint>
#include <vector>
#include <cstring>
struct MessageHeader {
uint32_t magic = 0xABCDEF01;
uint16_t version = 1;
uint16_t payload_size = 0;
};
struct Message {
MessageHeader header;
std::vector<std::byte> payload;
bool is_valid() const {
return header.magic == 0xABCDEF01 && header.version == 1;
}
// Сериализация в буфер
std::vector<std::byte> serialize() const {
std::vector<std::byte> buf;
buf.resize(sizeof(MessageHeader) + header.payload_size);
std::memcpy(buf.data(), &header, sizeof(header));
std::memcpy(buf.data() + sizeof(header), payload.data(), payload.size());
return buf;
}
// Десериализация из span (C++20) или const byte*
static std::optional<Message> deserialize(std::span<const std::byte> data) {
if (data.size() < sizeof(MessageHeader)) return std::nullopt;
Message msg;
std::memcpy(&msg.header, data.data(), sizeof(msg.header));
if (!msg.is_valid()) return std::nullopt;
if (data.size() != sizeof(MessageHeader) + msg.header.payload_size) return std::nullopt;
msg.payload.assign(
data.begin() + sizeof(MessageHeader),
data.begin() + sizeof(MessageHeader) + msg.header.payload_size
);
return msg;
}
};
✅ Подходит для внутренних IPC, embedded, когда не требуется совместимость с big-endian (иначе —
htonlи т.д.).
11.2. CSV-парсер (минималистичный, без экранирования)
#include <string>
#include <vector>
#include <sstream>
std::vector<std::vector<std::string>> parse_csv(std::string_view csv) {
std::vector<std::vector<std::string>> rows;
std::istringstream ss(std::string(csv)); // string_view → string для stringstream
std::string line;
while (std::getline(ss, line)) {
if (line.empty()) continue;
std::vector<std::string> row;
std::istringstream line_ss(line);
std::string field;
while (std::getline(line_ss, field, ',')) {
// trim (простой вариант)
auto start = field.find_first_not_of(" \t");
auto end = field.find_last_not_of(" \t");
if (start == std::string::npos) {
row.emplace_back("");
} else {
row.emplace_back(field.substr(start, end - start + 1));
}
}
rows.push_back(std::move(row));
}
return rows;
}
// Пример:
// auto data = parse_csv("a, b, c\n1, 2, 3");
// data[0] → {"a", "b", "c"}
⚠️ Не поддерживает запятые внутри кавычек (
"a,b",c). Для production — используйте библиотеку (например,rapidcsv).
12. C++20 и новейшие практики
12.1. std::format (C++20) — безопасная и производительная замена printf/stringstream
#include <format>
#include <iostream>
int main() {
std::string s = std::format("Hello, {}! You have {} new messages.",
"Timur", 5);
std::cout << s << "\n"; // → "Hello, Timur! You have 5 new messages."
// Локализация, ширина, точность:
double pi = 3.1415926535;
auto s2 = std::format("π ≈ {:.3f}", pi); // → "π ≈ 3.142"
}
✅ Безопаснее
printf(проверка типов на этапе компиляции приconsteval-расширениях).
📦 В GCC 12+, Clang 14+ (с-std=c++20и-lstdc++fsне нужен).
12.2. Concepts (C++20) — ограничения на шаблоны
#include <concepts>
#include <vector>
#include <list>
template<typename T>
concept Container = requires(T t) {
typename T::value_type;
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::sentinel_for<decltype(t.begin())>;
};
template<Container C>
void print_size(const C& c) {
std::cout << "Size: " << c.size() << "\n"; // ошибка, если size() нет → SFINAE-friendly
}
// Или более строго:
template<typename C>
requires requires(C c) { c.size(); }
void print_size_strict(const C& c) {
std::cout << "Size: " << c.size() << "\n";
}
// Использование:
// std::vector<int> v; print_size(v); // OK
// std::forward_list<int> fl; // print_size(fl); — ошибка (нет size())
✅ Concepts улучшают сообщения об ошибках и позволяют делать
requiresвif constexpr.
12.3. std::ranges (C++20) — ленивые трансформации
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
auto squares = nums
| std::views::filter([](int x) { return x % 2 == 1; })
| std::views::transform([](int x) { return x * x; });
for (int x : squares) {
std::cout << x << ' '; // → 1 9 25
}
}
⚡ Вычисления происходят лениво — без временного вектора.
12.4. std::expected (C++23) — официальная замена optional + ошибка
#include <expected>
#include <string>
#include <iostream>
std::expected<int, std::string> safe_divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
// Использование:
/*
auto res = safe_divide(10, 2);
if (res) {
std::cout << "Result: " << *res << "\n";
} else {
std::cerr << "Error: " << res.error() << "\n";
}
*/
📌 Требует C++23 и компилятор с поддержкой (GCC 12+, Clang 16+ с
-std=c++23).
13. Интерфейсы и дизайн API
13.1. Pimpl (Pointer to implementation) — скрытие реализации и ускорение компиляции
// widget.h
#pragma once
#include <memory>
class Widget {
public:
Widget();
~Widget(); // определён в .cpp!
Widget(const Widget&);
Widget& operator=(const Widget&);
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void do_work();
private:
class Impl; // объявлен, но не определён
std::unique_ptr<Impl> p_;
};
// widget.cpp
#include "widget.h"
#include <iostream>
class Widget::Impl {
int internal_state_ = 42;
public:
void do_impl() {
std::cout << "Impl: " << internal_state_ << "\n";
}
};
Widget::Widget() : p_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(const Widget& other) : p_(std::make_unique<Impl>(*other.p_)) {}
Widget& Widget::operator=(const Widget& other) {
if (this != &other) *p_ = *other.p_;
return *this;
}
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::do_work() {
p_->do_impl();
}
✅ Преимущества:
– ИзменениеImplне требует перекомпиляции клиентского кода.
– Уменьшение времени сборки (особенно при изменении деталей реализации).
– Сокрытие зависимостей (например,#include <heavy_lib.h>остаётся в.cpp).
⚠️ Накладные расходы: один дополнительный аллокатор (unique_ptr) и косвенный вызов.
13.2. NVI (Non-Virtual Interface) — инверсия вызова для контроля контракта
class Logger {
public:
// Публичный не-виртуальный интерфейс
void log(const std::string& msg) {
if (msg.empty()) return; // пре-условие
do_log(timestamp(), msg); // делегирование
++counter_; // пост-условие / учёт
}
int messages_logged() const { return counter_; }
protected:
// Виртуальная заглушка для наследников
virtual void do_log(const std::string& ts, const std::string& msg) = 0;
private:
std::string timestamp() const {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
return std::ctime(&time_t); // упрощённо
}
int counter_ = 0;
};
class FileLogger : public Logger {
protected:
void do_log(const std::string& ts, const std::string& msg) override {
std::ofstream f("log.txt", std::ios::app);
f << "[" << ts << "] " << msg << "\n";
}
};
✅ Позволяет централизовать:
– Валидацию аргументов
– Логирование вызовов
– Метрики (счётчики, тайминги)
– Обработку исключений
без дублирования в каждом наследнике.
13.3. Fluent interface (цепочка вызовов)
class QueryBuilder {
std::string table_;
std::vector<std::string> columns_;
std::string where_clause_;
int limit_ = -1;
public:
QueryBuilder& select(std::initializer_list<std::string> cols) {
columns_.insert(columns_.end(), cols);
return *this;
}
QueryBuilder& from(std::string table) {
table_ = std::move(table);
return *this;
}
QueryBuilder& where(std::string cond) {
where_clause_ = std::move(cond);
return *this;
}
QueryBuilder& limit(int n) {
limit_ = n;
return *this;
}
std::string build() const {
std::string sql = "SELECT ";
if (columns_.empty()) {
sql += "*";
} else {
for (size_t i = 0; i < columns_.size(); ++i) {
if (i > 0) sql += ", ";
sql += columns_[i];
}
}
sql += " FROM " + table_;
if (!where_clause_.empty()) {
sql += " WHERE " + where_clause_;
}
if (limit_ >= 0) {
sql += " LIMIT " + std::to_string(limit_);
}
return sql;
}
};
// Использование:
/*
auto query = QueryBuilder{}
.select({"id", "name", "email"})
.from("users")
.where("age > 18")
.limit(100)
.build();
// → "SELECT id, name, email FROM users WHERE age > 18 LIMIT 100"
*/
✅ Удобно для DSL-подобных API. Все методы возвращают
*thisпо ссылке (не по значению!).
14. Интеграция с C
14.1. Экспорт C++ API в C (стабильный ABI)
// api.h (чистый C — заголовок для клиентов на C)
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct my_handle my_handle;
my_handle* my_create(int config);
void my_destroy(my_handle* h);
int my_process(my_handle* h, const char* input, char** output);
#ifdef __cplusplus
}
#endif
// api.cpp
#include "api.h"
#include <string>
#include <memory>
struct my_handle { // opaque type
std::unique_ptr<class Impl> impl;
};
class Impl {
int config_;
public:
Impl(int c) : config_(c) {}
std::string process(std::string_view input) {
return "Config " + std::to_string(config_) + ": processed '" + std::string(input) + "'";
}
};
my_handle* my_create(int config) {
try {
auto h = new my_handle{};
h->impl = std::make_unique<Impl>(config);
return h;
} catch (...) {
return nullptr;
}
}
void my_destroy(my_handle* h) {
delete h; // вызовёт ~unique_ptr → ~Impl
}
int my_process(my_handle* h, const char* input, char** output) {
if (!h || !h->impl || !input || !output) return -1;
try {
std::string result = h->impl->process(input);
*output = static_cast<char*>(std::malloc(result.size() + 1));
if (!*output) return -2;
std::memcpy(*output, result.c_str(), result.size() + 1);
return 0;
} catch (...) {
return -1;
}
}
✅ Правила:
– В заголовке — толькоstruct,typedef,extern "C".
– В C++ — RAII внутри, но управление памятью —malloc/freeна границе.
– Все исключения ловятся — C не умеет их обрабатывать.
–opaque structскрывает детали реализации.
14.2. Callback с void* user_data (C-style обратный вызов)
// callback.h
typedef void (*progress_callback)(int percent, void* user_data);
void long_operation(progress_callback cb, void* user_data);
// callback.cpp
#include "callback.h"
#include <thread>
#include <chrono>
void long_operation(progress_callback cb, void* user_data) {
for (int i = 0; i <= 100; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
if (cb) cb(i, user_data);
}
}
// usage.cpp (C++ wrapper)
#include <functional>
#include <iostream>
extern "C" void c_style_callback(int percent, void* user_data) {
auto* fn = static_cast<std::function<void(int)>*>(user_data);
(*fn)(percent);
}
void cpp_usage() {
std::function<void(int)> cb = [](int p) {
std::cout << "Progress: " << p << "%\n";
};
long_operation(c_style_callback, &cb);
}
⚠️ Внимание:
user_dataдолжен жить дольше, чем вызов. Никаких&local_lambdaбез обёртки.
15. Профилирование и отладка
15.1. Интеграция sanitizers (пример использования)
# Сборка с sanitizers (GCC/Clang)
g++ -g -fsanitize=address,undefined -fno-omit-frame-pointer -O1 main.cpp -o app
./app # при ошибке — подробный стек, адрес, тип
// Пример: use-after-free (детектируется ASan)
int* p = new int(42);
delete p;
*p = 100; // 🚨 AddressSanitizer: heap-use-after-free
// Пример: undefined behavior (UBSan)
int x = 1 << 32; // 🚨 UBSan: shift exponent too large
✅ Рекомендуется:
–asan+ubsan— для CI и локальной отладки
–tsan— для data race detection (многопоточка)
–lsan— утечки памяти (часто входит вasan)
15.2. Статический таймер с RAII (для profiling отдельных блоков)
#include <chrono>
#include <iostream>
class ScopedTimer {
const char* label_;
std::chrono::steady_clock::time_point start_;
public:
explicit ScopedTimer(const char* label) : label_(label), start_(std::chrono::steady_clock::now()) {}
~ScopedTimer() {
auto end = std::chrono::steady_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end - start_);
std::cerr << "[TIMER] " << label_ << ": " << dur.count() << " µs\n";
}
};
// Использование:
/*
{
ScopedTimer t("heavy_computation");
heavy_computation();
} // → [TIMER] heavy_computation: 12345 µs
*/
🔧 Расширения:
– Передачаstd::ostream&для вывода
– Сбор статистики (min/max/avg) через статические поля
– Условная активация через#ifdef PROFILE
16. Embedded-специфика (без исключений, RTTI, STL-аллокаций)
16.1. Fixed-size arena allocator (стековая аллокация)
#include <array>
#include <cstddef>
#include <cstdint>
class StackAllocator {
alignas(std::max_align_t) std::array<std::byte, 1024> buffer_;
size_t offset_ = 0;
public:
void* allocate(size_t n, size_t align = alignof(std::max_align_t)) {
auto addr = reinterpret_cast<std::uintptr_t>(buffer_.data()) + offset_;
auto aligned = (addr + align - 1) & ~(align - 1);
size_t delta = aligned - addr;
if (offset_ + delta + n > buffer_.size()) return nullptr;
offset_ = delta + n;
return reinterpret_cast<void*>(aligned);
}
void reset() { offset_ = 0; } // ⚠️ Не вызывает деструкторы!
template<typename T, typename... Args>
T* make(Args&&... args) {
void* mem = allocate(sizeof(T), alignof(T));
if (!mem) return nullptr;
return new (mem) T(std::forward<Args>(args)...);
}
};
// Использование:
/*
StackAllocator alloc;
auto p = alloc.make<int>(42);
alloc.reset(); // повторное использование буфера
*/
✅ Подходит для realtime-систем, где
malloc/newнедопустимы.
⚠️ Нет поддержкиdelete— толькоreset()(RAII не нарушается, если объекты уничтожаются доreset).
16.2. Memory-mapped register access (volatile + reinterpret_cast)
#include <cstdint>
struct Register {
volatile uint32_t value;
void set_bit(int n) { value |= (1U << n); }
void clear_bit(int n) { value &= ~(1U << n); }
bool get_bit(int n) const { return (value & (1U << n)) != 0; }
};
// Предположим, что регистр GPIO находится по адресу 0x4000'0000
inline Register& GPIO() {
return *reinterpret_cast<Register*>(0x4000'0000);
}
// Использование:
/*
GPIO().set_bit(5); // установить 5-й бит
bool state = GPIO().get_bit(5);
*/
✅
volatileпредотвращает оптимизацию чтения/записи компилятором.
⚠️ Адреса зависят от SoC — выносите в#defineилиconstexpr.
17. Паттерны проектирования в C++
17.1. Visitor с std::variant (без виртуальных функций, C++17+)
#include <variant>
#include <iostream>
#include <string>
struct Circle {
double radius;
};
struct Rectangle {
double width, height;
};
struct Triangle {
double a, b, c;
};
using Shape = std::variant<Circle, Rectangle, Triangle>;
// Перегрузка для std::visit
template<typename... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template<typename... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) -> double { return 3.14159 * c.radius * c.radius; },
[](const Rectangle& r) -> double { return r.width * r.height; },
[](const Triangle& t) -> double {
double p = (t.a + t.b + t.c) / 2.0;
return std::sqrt(p * (p - t.a) * (p - t.b) * (p - t.c));
}
}, s);
}
// Использование:
/*
Shape s = Rectangle{3.0, 4.0};
std::cout << "Area: " << area(s) << "\n"; // → 12
*/
✅ Преимущества перед классическим Visitor:
– Нет наследования, виртуальных вызовов, RTTI
– Контейнерыstd::vector<Shape>компактны (нет указателей vtable)
– Compile-time проверка покрытия всех типов (еслиvariantнеstd::monostate)
⚠️std::visitиспользуетswitchпо внутреннему индексу — O(1), но без инлайна.
17.2. State-машина
#include <memory>
#include <iostream>
class Context; // forward declaration
class State {
public:
virtual ~State() = default;
virtual void handle(Context& ctx) = 0;
};
class Context {
std::unique_ptr<State> state_;
public:
explicit Context(std::unique_ptr<State> initial) : state_(std::move(initial)) {}
void set_state(std::unique_ptr<State> new_state) {
state_ = std::move(new_state);
}
void request() {
if (state_) state_->handle(*this);
}
};
class ConcreteStateA : public State {
public:
void handle(Context& ctx) override {
std::cout << "State A → switching to B\n";
ctx.set_state(std::make_unique<ConcreteStateB>());
}
};
class ConcreteStateB : public State {
public:
void handle(Context& ctx) override {
std::cout << "State B → switching to A\n";
ctx.set_state(std::make_unique<ConcreteStateA>());
}
};
// Использование:
/*
Context ctx(std::make_unique<ConcreteStateA>());
ctx.request(); // A → B
ctx.request(); // B → A
*/
✅ Чистый RAII: при замене состояния старое удаляется автоматически.
🔁 Альтернатива:enum class StateId + switch, но теряется инкапсуляция логики.
17.3. Strategy через std::function (лёгкая замена наследованию)
#include <functional>
#include <iostream>
#include <vector>
#include <algorithm>
class Sorter {
std::function<bool(int, int)> comparator_;
public:
explicit Sorter(std::function<bool(int, int)> comp)
: comparator_(std::move(comp)) {}
void sort(std::vector<int>& data) const {
std::sort(data.begin(), data.end(), comparator_);
}
};
// Использование:
/*
Sorter asc([](int a, int b) { return a < b; });
Sorter desc([](int a, int b) { return a > b; });
std::vector<int> v = {3, 1, 4, 1, 5};
asc.sort(v); // → {1,1,3,4,5}
desc.sort(v); // → {5,4,3,1,1}
*/
✅ Гибкость: стратегия может быть лямбдой с захватом,
std::bind, илиstd::less<int>{}.
⚠️ Накладные расходы:std::function— виртуальный вызов внутри (но инлайнится при простом захвате в современных компиляторах).
18. CMake: современные практики
18.1. Минималистичный CMakeLists.txt для библиотеки (C++17)
cmake_minimum_required(VERSION 3.14)
project(MyLib VERSION 1.0.0 LANGUAGES CXX)
# Требуем C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # строгий стандарт — без GNU-расширений
# Библиотека
add_library(MyLib
src/core.cpp
include/mylib/core.h
)
# Публичные заголовки
target_include_directories(MyLib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
# Проверка компилятора
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(MyLib PRIVATE -Wall -Wextra -Wpedantic)
endif()
# Установка (для CPack или `make install`)
install(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(DIRECTORY include/ DESTINATION include)
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
18.2. FetchContent: встраивание зависимостей
include(FetchContent)
# Пример: GoogleTest
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.14.0
)
FetchContent_MakeAvailable(googletest)
# Библиотека с тестами
add_library(MyLib ...)
add_executable(MyLibTest tests/main.cpp tests/core_test.cpp)
target_link_libraries(MyLibTest PRIVATE MyLib GTest::gtest GTest::gtest_main)
✅ Преимущества:
– Нет внешних зависимостей при клонировании
– Версионирование черезGIT_TAG
– Изоляция: зависимости собираются в подкаталоге_deps
⚠️ Избегайтеadd_subdirectoryдля чужих проектов — нарушает инкапсуляцию.
18.3. CMakePresets.json (для унификации сборки)
{
"version": 3,
"configurePresets": [
{
"name": "dev",
"displayName": "Development",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build-dev",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "release",
"displayName": "Release",
"inherits": "dev",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "asan",
"displayName": "AddressSanitizer",
"inherits": "dev",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
},
"environment": {
"CC": "clang",
"CXX": "clang++"
}
}
],
"buildPresets": [
{
"name": "dev",
"configurePreset": "dev"
},
{
"name": "asan",
"configurePreset": "asan",
"jobs": 4
}
]
}
Использование:
cmake --preset dev
cmake --build --preset dev
cmake --preset asan -DSANITIZE=ON
cmake --build --preset asan
✅ Позволяет стандартизировать сборку в команде без shell-скриптов.
19. Тестирование: GoogleTest и mocking
19.1. Базовый тест с параметризацией
#include <gtest/gtest.h>
int factorial(int n) {
if (n < 0) throw std::domain_error("n < 0");
int res = 1;
for (int i = 2; i <= n; ++i) res *= i;
return res;
}
TEST(FactorialTest, Positive) {
EXPECT_EQ(factorial(0), 1);
EXPECT_EQ(factorial(1), 1);
EXPECT_EQ(factorial(5), 120);
}
TEST(FactorialTest, ThrowsOnNegative) {
EXPECT_THROW(factorial(-1), std::domain_error);
}
// Параметризованный тест
class FactorialParamTest : public ::testing::TestWithParam<std::pair<int, int>> {};
TEST_P(FactorialParamTest, Values) {
auto [input, expected] = GetParam();
EXPECT_EQ(factorial(input), expected);
}
INSTANTIATE_TEST_SUITE_P(
Factorial,
FactorialParamTest,
::testing::Values(
std::make_pair(0, 1),
std::make_pair(1, 1),
std::make_pair(3, 6),
std::make_pair(6, 720)
)
);
✅
TEST_P+INSTANTIATE_TEST_SUITE_P— избегает дублированияEXPECT_EQ.
19.2. Mock-объект через MOCK_METHOD
#include <gmock/gmock.h>
class Database {
public:
virtual ~Database() = default;
virtual int query(const std::string& sql) = 0;
};
class MockDatabase : public Database {
public:
MOCK_METHOD(int, query, (const std::string& sql), (override));
};
class Service {
Database& db_;
public:
explicit Service(Database& db) : db_(db) {}
int get_user_count() {
return db_.query("SELECT COUNT(*) FROM users");
}
};
TEST(ServiceTest, CallsCorrectQuery) {
MockDatabase mock_db;
EXPECT_CALL(mock_db, query("SELECT COUNT(*) FROM users"))
.Times(1)
.WillOnce(::testing::Return(42));
Service srv(mock_db);
EXPECT_EQ(srv.get_user_count(), 42);
}
✅
EXPECT_CALLпроверяет:
– Количество вызовов
– Аргументы (можно::testing::StartsWith,::testing::_)
– Возвращаемое значение / side effects
⚠️ Mock должен жить дольше, чем тестируемый объект.
19.3. Тестирование смерти (death test)
TEST(DeathTest, FactorialNegativeDies) {
EXPECT_DEATH({ factorial(-5); }, "n < 0");
}
// Для многопоточных death-тестов:
TEST(MultiThreadDeathTest, ConcurrentAccessDies) {
GTEST_FLAG_SET(death_test_style, "threadsafe");
SomeUnsafeClass obj;
std::thread t([&] { obj.use_after_free(); });
EXPECT_DEATH({ obj.use_after_free(); }, "");
t.join();
}
⚠️
EXPECT_DEATHзапускает код в отдельном процессе — не подходит дляstd::cout/файлов без осторожности.
20. Производительность: cache-friendly структуры
20.1. AoS (Array of Structures) vs SoA (Structure of Arrays)
// AoS — плохо для SIMD и cache line utilisation
struct ParticleAoS {
float x, y, z;
float vx, vy, vz;
int id;
};
std::vector<ParticleAoS> particles_aos(1'000'000);
// SoA — cache-friendly при работе с подмножеством полей
struct ParticlesSoA {
std::vector<float> x, y, z;
std::vector<float> vx, vy, vz;
std::vector<int> id;
void resize(size_t n) {
x.resize(n); y.resize(n); z.resize(n);
vx.resize(n); vy.resize(n); vz.resize(n);
id.resize(n);
}
};
ParticlesSoA particles_soa;
particles_soa.resize(1'000'000);
// Обновление скоростей — только `vx, vy, vz` читаются/пишутся
for (size_t i = 0; i < particles_soa.vx.size(); ++i) {
particles_soa.vx[i] += 0.01f;
particles_soa.vy[i] += 0.01f;
particles_soa.vz[i] += 0.01f;
}
📊 Эффект:
– AoS: 1 cache miss на частицу (при 64-байтной строке — ~8 частиц/строка)
– SoA: 1 cache miss на ~16 значенийfloat(64 / 4 = 16) → 16× меньше промахов при обработке только скоростей
✅ Используйте SoA, если:
– Доступ идёт к подмножеству полей
– Размер структуры > L1 cache line
– Возможна векторизация (#pragma omp simd/std::transform)
20.2. Alignment и padding: alignas, [[no_unique_address]] (C++20)
struct Bad {
bool flag; // 1 байт + 3 padding
int count; // 4 байта
double value; // 8 байт
}; // sizeof = 16 (но полезных — 13)
struct Better {
double value; // 8
int count; // 4
bool flag; // 1 + 3 padding → total 16
}; // то же, но порядок улучшает доступ
// C++20: экономия на пустых базах
struct EmptyBase {};
struct [[no_unique_address]] Optimized : EmptyBase {
int data;
};
static_assert(sizeof(Optimized) == sizeof(int)); // true
✅ Проверяйте
sizeofиalignofв тестах производительности.
🔧alignas(64)для false sharing prevention в многопотоке:
struct alignas(64) Counter {
std::atomic<int> value{0};
};
// каждый Counter — в отдельной cache line
20.3. Избегаем std::vectorbool
// ❌ Антипаттерн: std::vector<bool> — не контейнер, proxy-итераторы, медленный доступ
std::vector<bool> flags(1000000); // bitset-like, but non-STL-compliant
// ✅ Альтернативы:
std::vector<char> flags_char(1000000); // 1 байт/флаг — просто и быстро
std::vector<int8_t> flags_i8(1000000); // явный signed char
std::bitset<1000000> flags_static; // если размер известен на этапе компиляции
boost::dynamic_bitset<> flags_boost(n); // динамический, но корректный
❗
std::vector<bool>не имеет.data(),&v[0]— UB, итераторы неT*.
✅ Используйте только если критична память и вы точно знаете ограничения.