Мини-пул потоков на std::jthread
Создадим простой пул, который принимает лямбды, параллельно их выполняет и авто-join-ит потоки при выходе из области.
Храним очередь задач, мьютекс, условную переменную и вектор рабочих потоков. Флаг
Конструктор порождает
Деструктор ставит флаг
🔥 Чистый стандарт C++20 — ни Boost, ни сторонних либ!
Создадим простой пул, который принимает лямбды, параллельно их выполняет и авто-join-ит потоки при выходе из области.
class ThreadPool {
std::queue<std::function<void()>> jobs; // очередь заданий
std::mutex m;
std::condition_variable cv;
bool stop = false;
std::vector<std::jthread> workers; // jthread = RAII-joinХраним очередь задач, мьютекс, условную переменную и вектор рабочих потоков. Флаг
stop сигнализирует о завершении.public:
ThreadPool(size_t n = std::thread::hardware_concurrency()) {
for (size_t i = 0; i < n; ++i)
workers.emplace_back([this](std::stop_token st) {
while (!st.stop_requested()) {
std::function<void()> job;
{ std::unique_lock lk(m);
cv.wait(lk, [this]{ return stop || !jobs.empty(); });
if (stop && jobs.empty()) return;
job = std::move(jobs.front()); jobs.pop();
}
job(); // выполняем задачу
}
});
}
Конструктор порождает
n потоков. Каждый рабочий ждёт, пока в очереди появится задача, — или пока пулу не придёт сигнал остановиться.template<typename F>
void enqueue(F&& f) {
{ std::lock_guard lk(m); jobs.emplace(std::forward<F>(f)); }
cv.notify_one(); // будим один поток
}
enqueue кладёт новую лямбду в очередь и будит спящего воркера. ~ThreadPool() {
{ std::lock_guard lk(m); stop = true; }
cv.notify_all(); // будим всех для выхода
}
};Деструктор ставит флаг
stop, будит все потоки, а std::jthread автоматически дожидается их завершения.int main() {
ThreadPool pool(4); // 4 рабочих потока
for (int i = 0; i < 8; ++i)
pool.enqueue([i]{ std::printf("job %d\n", i); });
} // ← при выходе pool завершит все задачиPlease open Telegram to view this post
VIEW IN TELEGRAM
👀209
Например, HTTP передаёт данные в открытом виде, без шифрования. HTTPS работает поверх TLS и защищает соединение: выполняется проверка сертификата, обмен ключами и дальнейшая передача данных в зашифрованном виде.
На картинке — ключевые этапы установления соединения.
Сохрани, чтобы не забыть!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀75
Атомарный доступ к полю без изменения его типа!
Иногда нужно синхронизировать доступ к полю структуры, но менять его тип на
Объявим структуру и создадим атомарную ссылку на поле:
Теперь можно безопасно работать с полем из нескольких потоков:
🔥
Иногда нужно синхронизировать доступ к полю структуры, но менять его тип на
std::atomic<T> невозможно — например, он используется сторонним API или загружается из сериализованного формата. В таких случаях поможет std::atomic_ref из C++20.Объявим структуру и создадим атомарную ссылку на поле:
#include <atomic>
struct Data {
int counter = 0;
};
Data d;
std::atomic_ref<int> a(d.counter);
Теперь можно безопасно работать с полем из нескольких потоков:
#include <thread>
std::jthread t1([&]{ a++; });
std::jthread t2([&]{ a += 2; });
std::atomic_ref делает атомарными любые переменные — удобно, когда нельзя менять исходные типы, но нужна потокобезопасность.Please open Telegram to view this post
VIEW IN TELEGRAM
👀95
scope_exit в C++: cleanup без goto и забытых close()
Когда в функции несколько
В таких местах удобно использовать маленький RAII-хелпер, который выполнит переданную лямбду при выходе из scope. Это особенно полезно в коде с исключениями и несколькими точками выхода.
Начнём с самого класса:
Он хранит функцию cleanup и флаг, нужно ли её вызывать.
В деструкторе cleanup выполнится автоматически, если guard ещё активен.
Теперь добавим маленький helper, чтобы не писать тип вручную:
С ним использование получается заметно чище.
Открываем файл и сразу вешаем cleanup рядом:
Это главный плюс паттерна: ресурс и его освобождение находятся буквально рядом.
Дальше могут быть любые ранние выходы:
Даже если функция завершится раньше,
Такой helper быстро окупается в коде, где много веток, ошибок и временных ресурсов. Он убирает дублирование и делает cleanup предсказуемым.
Когда в функции несколько
return, ручной cleanup почти всегда становится хрупким. Один выход забыли — и файл не закрыт, ресурс не освобождён, а ошибка всплывёт уже сильно позже.В таких местах удобно использовать маленький RAII-хелпер, который выполнит переданную лямбду при выходе из scope. Это особенно полезно в коде с исключениями и несколькими точками выхода.
Начнём с самого класса:
template <class F>
class scope_exit {
F fn;
bool active = true;
Он хранит функцию cleanup и флаг, нужно ли её вызывать.
public:
explicit scope_exit(F f) : fn(std::move(f)) {}
~scope_exit() { if (active) fn(); }
В деструкторе cleanup выполнится автоматически, если guard ещё активен.
void release() noexcept { active = false; }
};release() нужен, если в каком-то сценарии cleanup уже не требуется.Теперь добавим маленький helper, чтобы не писать тип вручную:
template <class F>
scope_exit<F> make_scope_exit(F f) {
return scope_exit<F>(std::move(f));
}
С ним использование получается заметно чище.
Открываем файл и сразу вешаем cleanup рядом:
FILE* f = std::fopen("data.bin", "wb");
if (!f) throw std::runtime_error("open failed");
auto guard = make_scope_exit([&] { std::fclose(f); });Это главный плюс паттерна: ресурс и его освобождение находятся буквально рядом.
Дальше могут быть любые ранние выходы:
if (std::rand() % 2)
return;
std::fwrite("abc", 1, 3, f);
Даже если функция завершится раньше,
fclose всё равно вызовется автоматически.Такой helper быстро окупается в коде, где много веток, ошибок и временных ресурсов. Он убирает дублирование и делает cleanup предсказуемым.
👀92
Например, O(1) — выполняется за постоянное время, а O(n) растёт линейно от размера входных данных.
На картинке — основные виды сложности, которые чаще всего встречаются в алгоритмах и структурах данных.
Сохрани, чтобы не забыть!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀84
Например, Load Balancing распределяет нагрузку между серверами, а Caching помогает ускорить ответы за счёт хранения данных в памяти.
На картинке — 12 основных концепций, которые должен понимать каждый разработчик.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀77
Фиксим проблему с конструированием POD типов
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
Теперь можно использовать все преимущества метода emplace_back и писать такой код:
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
struct A {
int a;
int&& r;
};
int f();
int n = 10;
A a1{1, f()}; // OK, lifetime is extended
A a2(1, f()); // well-formed, but dangling reference
A a3{1.0, 1}; // error: narrowing conversion
A a4(1.0, 1); // well-formed, but dangling reference
A a5(1.0, std::move(n)); // OKТеперь можно использовать все преимущества метода emplace_back и писать такой код:
std::vector<Point> vec;
vec.emplace_back(1, 2, 3);
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
vec.emplace_back(Point{1, 2, 3});
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
👀79
Например, CPU cache и RAM дают минимальную задержку и используются для быстрых вычислений, а cloud storage и базы данных — для долговременного хранения и масштабирования.
На картинке — основные типы storage, которые используются в современных системах: от памяти процессора до облачных решений.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀74
Например, CPU хорошо справляется с логикой, ветвлениями и задачами с низкой задержкой. GPU — про массовый параллелизм: подходит для обработки больших массивов данных, графики и вычислений.
На картинке — наглядное сравнение архитектуры, модели вычислений и типичных сценариев применения для каждого типа процессоров.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀69
Например,
код 200 означает, что всё прошло успешно, а 404 сообщает, что страница не найдена.Очень полезно держать под рукой, когда работаешь с API или отлаживаешь backend.
На картинке показаны самые часто используемые статусы от 100 до 599.
Сохрани, чтобы не забыть!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀65
🔥 Оптимизация кода на C++: Ранний возврат вместо вложенных условий
Привет, друзья! Сегодня хочу поговорить об одной важной технике, которая делает код чище и читабельнее — ранний возврат (early return). Часто встречаю код, который уходит в глубину вложенных if, превращаясь в настоящий лабиринт. Давайте разберем, как этого избежать.
❌ Плохой пример: Вложенные условия
Здесь код уходит вглубь из-за множества вложенных if, что делает его сложным для чтения.
✅ Хороший пример: Ранний возврат
Теперь код сразу проверяет граничные условия и делает ранний возврат (return), если условия не выполнены. В итоге у нас получился плоский код, который проще читать и сопровождать.
🎯 Вывод:
- Избегайте вложенных if, если можно этого не делать.
- Используйте ранний возврат, чтобы код был линейным и понятным.
- Чем меньше уровней вложенности — тем легче отладка и сопровождение.
Привет, друзья! Сегодня хочу поговорить об одной важной технике, которая делает код чище и читабельнее — ранний возврат (early return). Часто встречаю код, который уходит в глубину вложенных if, превращаясь в настоящий лабиринт. Давайте разберем, как этого избежать.
❌ Плохой пример: Вложенные условия
void process(int value) {
if (value > 0) {
if (value % 2 == 0) {
if (value < 100) {
std::cout « "Обрабатываем " « value « std::endl;
} else {
std::cout « "Слишком большое число" « std::endl;
}
} else {
std::cout « "Нечетное число" « std::endl;
}
} else {
std::cout « "Отрицательное число" « std::endl;
}
}Здесь код уходит вглубь из-за множества вложенных if, что делает его сложным для чтения.
✅ Хороший пример: Ранний возврат
void process(int value) {
if (value <= 0) {
std::cout « "Отрицательное число" « std::endl;
return;
}
if (value % 2 != 0) {
std::cout « "Нечетное число" « std::endl;
return;
}
if (value >= 100) {
std::cout « "Слишком большое число" « std::endl;
return;
}
std::cout « "Обрабатываем " « value « std::endl;
}Теперь код сразу проверяет граничные условия и делает ранний возврат (return), если условия не выполнены. В итоге у нас получился плоский код, который проще читать и сопровождать.
🎯 Вывод:
- Избегайте вложенных if, если можно этого не делать.
- Используйте ранний возврат, чтобы код был линейным и понятным.
- Чем меньше уровней вложенности — тем легче отладка и сопровождение.
👀94
std::to_address
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
Use the right tool. Stay cool.
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
obj.operator->(). Однако для простых указателей это не прокатит: они не классы и у них нет методов. Да и не прокатит для любых других объектов, у которых не определен этот оператор. Что делать?Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
template<class T>
constexpr T* to_address(T* p) noexcept {
static_assert(!std::is_function_v<T>);
return p;
}
template<class T>
constexpr auto to_address(const T& p) noexcept {
if constexpr (requires{ std::pointer_traits<T>::to_address(p); })
return std::pointer_traits<T>::to_address(p);
else
return std::to_address(p.operator->());
}
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
void c_api_func(const int*);
template<typename T>
void call_c_api_func(T && obj) {
c_api_func(std::to_address(obj));
}
std::vector<int> data{10, 20, 30};
call_c_api_func(data.begin()); // works
auto ptr = std::make_unique<int>(42);
call_c_api_func(ptr); // works
call_c_api_func(ptr.get()); // also works
Use the right tool. Stay cool.
👀91
Правильно захватываем по ссылке объект в лямбду
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
struct Task {
int id;
std::function<void()> GetNotifier() {
return [&]{
std::cout << "notify " << id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
struct Task {
int id;
std::function<void()> GetNotifier() {
return [curr = std::shared_ptr<Task>(this)]{
std::cout << "notify " << curr->id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
curr будет думать, что только он и его копии будут владеть объектом, а оригинальный объект просто уничтожится и curr будет ссылаться на невалидный объект. Получили то же самое UB.Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
struct Task : std::enable_shared_from_this<Task> {
int id;
std::function<void()> GetNotifier() {
// Захватываем shared_ptr на текущий объект
return [self = shared_from_this()] {
std::cout << "notify " << self->id << std::endl;
};
}
};
int main() {
auto notify = std::make_shared<Task>(5)->GetNotifier();
notify(); // Теперь безопасно - объект не будет уничтожен
}В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
👀105
Concurrency - это про структуру программы: несколько задач могут продвигаться вперёд в перекрывающиеся промежутки времени. Parallelism - это уже физическое одновременное выполнение, например на нескольких ядрах CPU.
На картинке — визуальное отличие concurrent execution от parallel execution. Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀83
Оптимизация кода с std::optional в C++
Привет, друзья! Сегодня поговорим про std::optional — мощный инструмент, который делает код чище и безопаснее.
Зачем нужен std::optional?
Обычно, если функция не может вернуть корректное значение, приходится использовать:
Возвращаемое значение с ошибочным кодом (неудобно, особенно если 0 или -1 могут быть валидными).
Выброс исключения (дорого по ресурсам).
Указатели (nullptr, но требует дополнительных проверок).
Альтернатива? Используем std::optional!
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> findUser(int id) {
if (id == 42) return "John Doe";
return std::nullopt;
}
int main() {
auto user = findUser(42);
if (user) {
std::cout « "User found: " « *user « std::endl;
} else {
std::cout « "User not found!" « std::endl;
}
}
Код стал чище: нет лишних проверок nullptr, исключений или специальных значений.
Когда использовать?
Когда функция может вернуть "ничего", но исключения и специальные значения не подходят.
Для более понятного API (например, парсинг строки в число).
Когда важно избежать неопределенного состояния (например, с переменной внутри класса).
Привет, друзья! Сегодня поговорим про std::optional — мощный инструмент, который делает код чище и безопаснее.
Зачем нужен std::optional?
Обычно, если функция не может вернуть корректное значение, приходится использовать:
Возвращаемое значение с ошибочным кодом (неудобно, особенно если 0 или -1 могут быть валидными).
Выброс исключения (дорого по ресурсам).
Указатели (nullptr, но требует дополнительных проверок).
Альтернатива? Используем std::optional!
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> findUser(int id) {
if (id == 42) return "John Doe";
return std::nullopt;
}
int main() {
auto user = findUser(42);
if (user) {
std::cout « "User found: " « *user « std::endl;
} else {
std::cout « "User not found!" « std::endl;
}
}
Код стал чище: нет лишних проверок nullptr, исключений или специальных значений.
Когда использовать?
Когда функция может вернуть "ничего", но исключения и специальные значения не подходят.
Для более понятного API (например, парсинг строки в число).
Когда важно избежать неопределенного состояния (например, с переменной внутри класса).
👀95
Динамический полиморфизм. std::function
#новичкам
В прошлом посте поговорили, что динамический полиморфизм реализуется не только через иерархии классов и виртуальные методы.
Есть другой прекрасный инструмент - std::function. Это обертка над всеми callable объектами, которая позволяет их единообразоно вызывать. Никаких иерархий, только функциональные объекты.
Теперь в очереди хранятся какие-то вызываемые объекты. Воркеру не важно, что это за объекты. Главное, что продюсеры могут разные функциональные объекты положить в один и тот же контейнер, попутно обернув их в std::function и тем самым полностью обезличив их. А легитимность такого мува достигается за счет того, что эти объекты имеют единый интерфейс - их можно вызвать без аргументов и не получить никакого возвращаемого значения.
Уже сейчас можно заметить, что для динамического полиморфизма нужно какого-то рода type erasure(стирание типов). Структура, которая хранит полиморфные объекты, не должна иметь полную информации о конкретном типе этих объектов. Объекты лишь должны иметь какой-то общий интерфейс. И тогда тип неважен: мы можем оперировать объектами через этот общий интерфейс.
std::function довольно интересно внутри устроен. После верхнеуровневого разговора про все полиморфизмы, вернемся к нему.
Но в плюсах есть еще много примеров полиморфизма времени выполнения, о которых поговорим в следующий раз.
Extract common traits. Stay cool.
#новичкам
В прошлом посте поговорили, что динамический полиморфизм реализуется не только через иерархии классов и виртуальные методы.
Есть другой прекрасный инструмент - std::function. Это обертка над всеми callable объектами, которая позволяет их единообразоно вызывать. Никаких иерархий, только функциональные объекты.
void Worker(std::deque<std::function<void()>>& tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
tasks.pop_front();
task(); // call callable
}
}
void Producer1(const std::string& bucket, const std::vector<std::string>& paths,
const std::shared_ptr<S3Client>& client, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
client_->Upload(bucket, path);
std::cout << "Uploaded: " << bucket << ", path " << path << std::endl;
});
}
void Producer2(const std::vector<std::string>& paths, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
std::remove(path.c_str());
std::cout << "Deleted: " << path << std::endl;
});
}Теперь в очереди хранятся какие-то вызываемые объекты. Воркеру не важно, что это за объекты. Главное, что продюсеры могут разные функциональные объекты положить в один и тот же контейнер, попутно обернув их в std::function и тем самым полностью обезличив их. А легитимность такого мува достигается за счет того, что эти объекты имеют единый интерфейс - их можно вызвать без аргументов и не получить никакого возвращаемого значения.
Уже сейчас можно заметить, что для динамического полиморфизма нужно какого-то рода type erasure(стирание типов). Структура, которая хранит полиморфные объекты, не должна иметь полную информации о конкретном типе этих объектов. Объекты лишь должны иметь какой-то общий интерфейс. И тогда тип неважен: мы можем оперировать объектами через этот общий интерфейс.
std::function довольно интересно внутри устроен. После верхнеуровневого разговора про все полиморфизмы, вернемся к нему.
Но в плюсах есть еще много примеров полиморфизма времени выполнения, о которых поговорим в следующий раз.
Extract common traits. Stay cool.
👀68
Засовываем исключение в исключение
#опытным
Вы знали, что в плюсах есть вложенные исключения? Такие исключения могут хранить в себе несколько исключений. Сегодня мы посмотрим, что это за зверь такой.
Начнем с применения. В прошлом посте мы создавали новое исключение на основе строки ошибки обрабатываемого исключения. В этом случае нужно писать определенное количество бойлерплейта и теряется информация о типе изначального исключения. Чтобы избежать этих проблем, мы можем бросить новое исключение, которое будет в себе содержать старое:
Теперь исключение, которое вылетит из HandlingCalculations будет на самом деле содержать 3 исключения: от базы данных, от ComplicatedCalculations и от HandlingCalculations.
Вложенные исключения существуют с С++11 и очень интересно устроены. Рассмотрим несколько упрощенные версии сущностей, которые находятся под капотом механизма вложенных исключений. Есть класс std::nested_exception:
Этот класс ответственен за захват текущего исключения с помощью вызова std::current_exception().
Дальше имеется класс, который хранит в себе все множество исключений:
Объекты этого класса наследуются от nested_exception, в котором захвачено старое исключение, и от Except - нового исключения.
Ну и последний компонент - std::throw_with_nested:
При вызове throw_with_nested создается объект Nested_exception на основе переданного типа исключения и, неявно, nested_exception, которых сохраняет в себе указатель на старое исключение.
Получается, что мы при каждом вызове throw_with_nested подмешиваем новое исключение к старому с помощью множественного наследования.
Очень прикольная техника, которая позволяет строить цепочки объектов. Это как тупл, только расширяемый в рантайме.
Это все хорошо и интересно. Прокидывать вложенные исключения мы научились. Но рано или поздно их придется обработать. Как это сделать? Об этом будем говорить в следующем посте.
Inherit knowledge from your ancestor. Stay cool.
#опытным
Вы знали, что в плюсах есть вложенные исключения? Такие исключения могут хранить в себе несколько исключений. Сегодня мы посмотрим, что это за зверь такой.
Начнем с применения. В прошлом посте мы создавали новое исключение на основе строки ошибки обрабатываемого исключения. В этом случае нужно писать определенное количество бойлерплейта и теряется информация о типе изначального исключения. Чтобы избежать этих проблем, мы можем бросить новое исключение, которое будет в себе содержать старое:
void ComplicatedCalculations() try {
// use db
} catch (std::exception& ex) {
std::throw_with_nested(std::runtime_error("Complicated Calculations Error"));
}
void HandlingCalculations() try {
ComplicatedCalculations();
} catch (std::exception& ex) {
std::throw_with_nested(std::runtime_error("Handling Calculations Error"));
}Теперь исключение, которое вылетит из HandlingCalculations будет на самом деле содержать 3 исключения: от базы данных, от ComplicatedCalculations и от HandlingCalculations.
Вложенные исключения существуют с С++11 и очень интересно устроены. Рассмотрим несколько упрощенные версии сущностей, которые находятся под капотом механизма вложенных исключений. Есть класс std::nested_exception:
class nested_exception
{
exception_ptr _M_ptr;
public:
/// The default constructor stores the current exception (if any).
nested_exception() noexcept : _M_ptr(current_exception()) { }
...
};
Этот класс ответственен за захват текущего исключения с помощью вызова std::current_exception().
Дальше имеется класс, который хранит в себе все множество исключений:
template<typename Except>
struct Nested_exception : public Except, public nested_exception
{
explicit Nested_exception(const Except& ex)
: Except(ex) { }
};
Объекты этого класса наследуются от nested_exception, в котором захвачено старое исключение, и от Except - нового исключения.
Ну и последний компонент - std::throw_with_nested:
template<typename Tp>
[[noreturn]]
inline void throw_with_nested(Tp&& t)
{
throw Nested_exception<remove_cvref_t<Tp>>{std::forward<Tp>(t)};
}
При вызове throw_with_nested создается объект Nested_exception на основе переданного типа исключения и, неявно, nested_exception, которых сохраняет в себе указатель на старое исключение.
Получается, что мы при каждом вызове throw_with_nested подмешиваем новое исключение к старому с помощью множественного наследования.
Очень прикольная техника, которая позволяет строить цепочки объектов. Это как тупл, только расширяемый в рантайме.
Это все хорошо и интересно. Прокидывать вложенные исключения мы научились. Но рано или поздно их придется обработать. Как это сделать? Об этом будем говорить в следующем посте.
Inherit knowledge from your ancestor. Stay cool.
👀161
🔧 Что делать, если std::sort тормозит?
Привет! Сегодня хочу поделиться с вами одной типичной ситуацией, с которой сталкивался не раз — сортировка больших контейнеров через std::sort, которая неожиданно начинает тормозить. Вызываешь вроде обычную сортировку, а работает медленно. Почему так?
🔍 Проблема — не std::sort, а компаратор!
В 90% случаев проблема не в std::sort, а в лямбде или компараторе, который вы передаёте. Особенно если он:
1. Вызывает копирование: вы сравниваете по значениям, а не по ссылке.
2. Делает что-то тяжёлое внутри: например, вызывает метод, делает std::string копию, обращается к БД (да, и такое видел!).
3. Некеширует результат: например, каждый раз считает длину строки.
✅ Как ускорить сортировку:
- Передавайте данные по ссылке, особенно если у вас вектор структур:
std::sort(vec.begin(), vec.end(), [](const MyStruct& a, const MyStruct& b) {
return a.key < b.key;
});
- Если у вас есть вычисление ключа — используйте схему "decorate-sort-undecorate":
std::vector<std::pair<int, size_t» temp;
for (size_t i = 0; i < vec.size(); ++i)
temp.emplace_back(compute_key(vec[i]), i);
std::sort(temp.begin(), temp.end());
std::vector<MyStruct> result;
for (const auto& [_, i] : temp)
result.push_back(vec[i]);
🧠 Мораль: Если std::sort "медленный", не спешите винить алгоритм. Лучше проверьте, что вы передаёте ему на вход.
Привет! Сегодня хочу поделиться с вами одной типичной ситуацией, с которой сталкивался не раз — сортировка больших контейнеров через std::sort, которая неожиданно начинает тормозить. Вызываешь вроде обычную сортировку, а работает медленно. Почему так?
🔍 Проблема — не std::sort, а компаратор!
В 90% случаев проблема не в std::sort, а в лямбде или компараторе, который вы передаёте. Особенно если он:
1. Вызывает копирование: вы сравниваете по значениям, а не по ссылке.
2. Делает что-то тяжёлое внутри: например, вызывает метод, делает std::string копию, обращается к БД (да, и такое видел!).
3. Некеширует результат: например, каждый раз считает длину строки.
✅ Как ускорить сортировку:
- Передавайте данные по ссылке, особенно если у вас вектор структур:
std::sort(vec.begin(), vec.end(), [](const MyStruct& a, const MyStruct& b) {
return a.key < b.key;
});
- Если у вас есть вычисление ключа — используйте схему "decorate-sort-undecorate":
std::vector<std::pair<int, size_t» temp;
for (size_t i = 0; i < vec.size(); ++i)
temp.emplace_back(compute_key(vec[i]), i);
std::sort(temp.begin(), temp.end());
std::vector<MyStruct> result;
for (const auto& [_, i] : temp)
result.push_back(vec[i]);
🧠 Мораль: Если std::sort "медленный", не спешите винить алгоритм. Лучше проверьте, что вы передаёте ему на вход.
👀83
std::array
#новичкам
На самом деле, это очень-очень тонкая обертка над сишными массивами. Вот несколько упрощенная реализация, которая тем не менее полностью передает смысл и необходимые особенности.
За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами.
А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.
Обычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен.
std::array не мимикрирует ни под какой другой тип, так как это кастомный класс. Внутри себя он также инкапсулирует все необходимые операторы сравнения. В операциях с ним нет никакой путаницы, потому что они явно определены конкретно для этого класса. Его можно спокойно принимать в функцию по ссылке и по значению, а также указывать в качестве возвращаемого значения. И все это с привычной семантикой.
Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.
Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент!
Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции.
#новичкам
На самом деле, это очень-очень тонкая обертка над сишными массивами. Вот несколько упрощенная реализация, которая тем не менее полностью передает смысл и необходимые особенности.
template<typename T, size_t N>
struct array
{
T& operator[](size_t index) {
return _data[index];
}
T& front() {
return _data[0];
}
T& back() {
return _data[N-1];
}
T* data() {
return _data;
}
constexpr size_t size() const {
return N;
}
constexpr bool empty() const {
return N == 0;
}
// еще const версии перечисленных методов и некоторые другие методы и алиасы типов
T _data[N];
};
За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами.
А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.
std::array arr{1, 2, 3};
static_assert(arr.size() == 3); // здесь не упадемОбычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен.
std::array не мимикрирует ни под какой другой тип, так как это кастомный класс. Внутри себя он также инкапсулирует все необходимые операторы сравнения. В операциях с ним нет никакой путаницы, потому что они явно определены конкретно для этого класса. Его можно спокойно принимать в функцию по ссылке и по значению, а также указывать в качестве возвращаемого значения. И все это с привычной семантикой.
template<typename T, size_t N>
std::array<T, N> double_elements(std::array<T, N>& array) {
std::array<T, N> result = array;
for (auto& elem: result)
elem = elem * 2;
return result;
}
Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.
int c_arr[N];
std::array<int, N> cpp_arr;
sizeof(cpp_arr) == cpp_arr.size() * sizeof(int) ==
sizeof(c_arr) == N * sizeof(int) == std::size(c_arr) * sizeof(int);
Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент!
Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции.
👀71