Зачем сегодня выбирать C++ и где его возможности действительно нужны?
В интервью с C++-разработчиком обсуждают не только язык, но и инженерную работу вокруг него. Разговор опирается на опыт высоконагруженных сервисов, разработки фреймворков и поиска ошибок, которые могут проявиться уже под реальной нагрузкой.
В статье разбирают:
• почему контроль над памятью даёт производительность и повышает цену ошибок
• как санитайзеры и профилировщики помогают искать проблемы до продакшена
• где C++ оправдан, а где проще выбрать другой язык и масштабировать систему
Язык выбирают под ограничения и задачи системы, а сильная инженерная база важнее привязки к одному синтаксису.
📣 C++ Ready | #ресурс
В интервью с C++-разработчиком обсуждают не только язык, но и инженерную работу вокруг него. Разговор опирается на опыт высоконагруженных сервисов, разработки фреймворков и поиска ошибок, которые могут проявиться уже под реальной нагрузкой.
В статье разбирают:
• почему контроль над памятью даёт производительность и повышает цену ошибок
• как санитайзеры и профилировщики помогают искать проблемы до продакшена
• где C++ оправдан, а где проще выбрать другой язык и масштабировать систему
Язык выбирают под ограничения и задачи системы, а сильная инженерная база важнее привязки к одному синтаксису.
Продолжай читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍3🔥2
Почему std::move иногда всё равно копирует объект?
std::move сам ничего не переносит, а только приводит выражение к rvalue-ссылке, чтобы компилятор мог выбрать подходящий конструктор.
Посмотри на тип объекта до и после вызова:
candidate имеет тип const std::string&&. Константность не исчезает, потому что std::move сохраняет квалификаторы исходного объекта.
Обычный move-конструктор принимает неконстантную rvalue-ссылку:
Он может изменить исходную строку, забрав её внутренний буфер. С const std::string&& это невозможно, поэтому такой конструктор не подходит.
Для константного источника компилятор обычно выбирает копирующий вариант:
В результате код остаётся корректным, но большой буфер копируется. Сам факт, что написан std::move, не гарантирует перемещение.
Если владение можно передать, оставь источник неконстантным:
После операции source остаётся валидным объектом, но его значение не определено для прикладной логики. Его можно присвоить заново или уничтожить, но не стоит полагаться на прежнее содержимое.
При возврате локального объекта std::move обычно тоже не нужен:
Компилятор может применить NRVO и создать результат сразу в месте вызова. Принудительный std::move в return способен помешать этой оптимизации и не делает код быстрее автоматически.
📣 C++ Ready | #совет
std::move сам ничего не переносит, а только приводит выражение к rvalue-ссылке, чтобы компилятор мог выбрать подходящий конструктор.
Посмотри на тип объекта до и после вызова:
const std::string source = "payload";
auto&& candidate = std::move(source);
candidate имеет тип const std::string&&. Константность не исчезает, потому что std::move сохраняет квалификаторы исходного объекта.
Обычный move-конструктор принимает неконстантную rvalue-ссылку:
std::string(std::string&& other);
Он может изменить исходную строку, забрав её внутренний буфер. С const std::string&& это невозможно, поэтому такой конструктор не подходит.
Для константного источника компилятор обычно выбирает копирующий вариант:
std::string::string(const std::string& other);
В результате код остаётся корректным, но большой буфер копируется. Сам факт, что написан std::move, не гарантирует перемещение.
Если владение можно передать, оставь источник неконстантным:
std::string source = "payload";
std::string result = std::move(source);
После операции source остаётся валидным объектом, но его значение не определено для прикладной логики. Его можно присвоить заново или уничтожить, но не стоит полагаться на прежнее содержимое.
При возврате локального объекта std::move обычно тоже не нужен:
std::string make_message() {
std::string result = "ready";
return result;
}Компилятор может применить NRVO и создать результат сразу в месте вызова. Принудительный std::move в return способен помешать этой оптимизации и не делает код быстрее автоматически.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍5🔥3
Шпаргалка по пределам числовых типов в C++!
std::numeric_limits<T> помогает узнать свойства типа прямо из кода. Для целых типов можно проверить границы, а для float и double наглядно сравнить точность и специальные значения.
На картинке показаны signed и unsigned типы, диапазоны float и double, а также lowest, min, max, epsilon и infinity.
Сохрани, чтобы не потерять!
📣 C++ Ready | #ресурс
std::numeric_limits<T> помогает узнать свойства типа прямо из кода. Для целых типов можно проверить границы, а для float и double наглядно сравнить точность и специальные значения.
На картинке показаны signed и unsigned типы, диапазоны float и double, а также lowest, min, max, epsilon и infinity.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍4🔥3
Группируем метрики по минутным окнам на C++!
Представим, что сервис записывает latency каждого запроса. Для графика не всегда нужны все отдельные измерения, поэтому соберём число событий и среднее значение для каждой минуты.
Храним у каждого измерения время и значение, а в сводке накапливаем сумму и количество:
Ключом сделаем начало минуты. floor округляет время вниз, поэтому измерения в 10:14:02 и 10:14:59 попадут в одно окно:
Для каждого измерения вычисляем ключ и обновляем аккумулятор:
Здесь хранится сумма, а не готовое среднее. Так обновление занимает постоянное время и не требует пересчитывать все предыдущие значения окна.
После чтения входных данных проходим по окнам в хронологическом порядке и считаем среднее:
На выходе каждая строка содержит начало минуты, количество запросов и среднюю задержку.
📣 C++ Ready | #практика
Представим, что сервис записывает latency каждого запроса. Для графика не всегда нужны все отдельные измерения, поэтому соберём число событий и среднее значение для каждой минуты.
Храним у каждого измерения время и значение, а в сводке накапливаем сумму и количество:
struct Sample { std::chrono::system_clock::time_point at; double value; };
struct Stats { double total{}; std::size_t count{}; };Ключом сделаем начало минуты. floor округляет время вниз, поэтому измерения в 10:14:02 и 10:14:59 попадут в одно окно:
using Minute = std::chrono::time_point<
std::chrono::system_clock, std::chrono::minutes>;
std::map<Minute, Stats> buckets;
Для каждого измерения вычисляем ключ и обновляем аккумулятор:
auto minute = std::chrono::floor<std::chrono::minutes>(sample.at);
auto& stats = buckets[minute];
stats.total += sample.value;
++stats.count;
Здесь хранится сумма, а не готовое среднее. Так обновление занимает постоянное время и не требует пересчитывать все предыдущие значения окна.
После чтения входных данных проходим по окнам в хронологическом порядке и считаем среднее:
for (const auto& [minute, stats] : buckets) {
const double average = stats.total / stats.count;
writeRow(minute, stats.count, average);
}На выходе каждая строка содержит начало минуты, количество запросов и среднюю задержку.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍3🔥2
Объединяем отсортированные логи на C++!
Несколько файлов уже содержат записи в порядке времени, но для анализа нужен один общий поток. Соберём его через min-heap, читая из каждого файла только текущую запись.
В этой задаче:
Так объём дополнительной памяти зависит от числа файлов, а не от размера логов.
📣 C++ Ready | #ресурс
Несколько файлов уже содержат записи в порядке времени, но для анализа нужен один общий поток. Соберём его через min-heap, читая из каждого файла только текущую запись.
В этой задаче:
• связываем запись с её файлом
• помещаем в очередь первые записи
• после извлечения подгружаем следующую из того же файла
Так объём дополнительной памяти зависит от числа файлов, а не от размера логов.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍4🔥3
Добавляем место вызова в сообщения об ошибках на C++!
Когда ошибка обнаруживается глубоко внутри общей функции, по одному тексту лога бывает трудно найти её источник. В C++20 std::source_location позволяет получить файл и строку вызова без ручной передачи этих параметров.
Подключаем нужные заголовки:
Добавляем source_location последним параметром со значением по умолчанию:
current() вычисляется в месте вызова функции. Поэтому в лог попадёт файл и строка кода, откуда вызвали logError, а не строка внутри самой функции.
Формируем компактную запись с координатами:
Такой формат легко искать в CI-логах и локальном выводе. При необходимости к нему добавляется имя функции через where.function_name().
Вызываем логгер:
Компилятор подставит место этого вызова автоматически.
📣 C++ Ready | #практика
Когда ошибка обнаруживается глубоко внутри общей функции, по одному тексту лога бывает трудно найти её источник. В C++20 std::source_location позволяет получить файл и строку вызова без ручной передачи этих параметров.
Подключаем нужные заголовки:
#include <iostream>
#include <source_location>
#include <string_view>
Добавляем source_location последним параметром со значением по умолчанию:
void logError(std::string_view message,
std::source_location where =
std::source_location::current()) {
current() вычисляется в месте вызова функции. Поэтому в лог попадёт файл и строка кода, откуда вызвали logError, а не строка внутри самой функции.
Формируем компактную запись с координатами:
std::cerr << where.file_name()
<< ":" << where.line()
<< " " << message << "\n";
}
Такой формат легко искать в CI-логах и локальном выводе. При необходимости к нему добавляется имя функции через where.function_name().
Вызываем логгер:
logError("config file is missing");Компилятор подставит место этого вызова автоматически.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11👍4🔥4
Как игровой движок распределяет тысячи корутин между потоками?
В статье разбирают shuttle_async, экспериментальный планировщик задач для Vulkan-движка ShuttleEngine. Автор показывает, как локальные очереди и work stealing помогают распределять работу между потоками.
В статье разбирают:
• как устроены локальные деки Chase-Lev и кража задач
• зачем корутинам нужен диспетчер и structured concurrency
• как устроены три режима запуска задач
• что показали замеры на малых, больших и неравномерных задачах
Результаты относятся к конкретному проекту и тестовой машине.
📣 C++ Ready | #статья
В статье разбирают shuttle_async, экспериментальный планировщик задач для Vulkan-движка ShuttleEngine. Автор показывает, как локальные очереди и work stealing помогают распределять работу между потоками.
В статье разбирают:
• как устроены локальные деки Chase-Lev и кража задач
• зачем корутинам нужен диспетчер и structured concurrency
• как устроены три режима запуска задач
• что показали замеры на малых, больших и неравномерных задачах
Результаты относятся к конкретному проекту и тестовой машине.
Продолжай читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍2🤝2👎1🔥1
Знаете, почему optional.value_or может выполнить лишнюю работу?
Аргументы функции вычисляются до её вызова. Поэтому fallback внутри value_or запустится даже тогда, когда optional уже содержит значение.
Например, результат загрузки может не понадобиться:
Здесь loadNameFromDisk вызывается заранее, а не только при пустом cachedName. Для чтения файла, запроса к базе или построения объекта это лишняя работа.
Если запасное значение дорого получать, проверяем optional отдельно:
Условный оператор вычисляет только выбранную ветку. Поэтому загрузка выполняется лишь при отсутствии готового значения.
Для дешёвого значения value_or остаётся коротким и удобным:
Ленивое вычисление лучше выражать условием или отдельной функцией.
📣 C++ Ready | #совет
Аргументы функции вычисляются до её вызова. Поэтому fallback внутри value_or запустится даже тогда, когда optional уже содержит значение.
Например, результат загрузки может не понадобиться:
auto name = cachedName.value_or(
loadNameFromDisk());
Здесь loadNameFromDisk вызывается заранее, а не только при пустом cachedName. Для чтения файла, запроса к базе или построения объекта это лишняя работа.
Если запасное значение дорого получать, проверяем optional отдельно:
auto name = cachedName
? *cachedName
: loadNameFromDisk();
Условный оператор вычисляет только выбранную ветку. Поэтому загрузка выполняется лишь при отсутствии готового значения.
Для дешёвого значения value_or остаётся коротким и удобным:
auto port = configuredPort.value_or(8080);
Ленивое вычисление лучше выражать условием или отдельной функцией.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤7🔥3
Объединяем пересекающиеся интервалы на C++!
Дан список занятых временных интервалов. Нужно объединить пересекающиеся и соприкасающиеся отрезки.
Представляем каждый интервал парой начала и конца. Считаем границы полуоткрытыми, поэтому отрезки [2, 5) и [5, 7) тоже объединяются:
Сначала сортируем пары по началу, а при равенстве по концу. После сортировки каждый следующий интервал можно сравнить только с последним результатом:
Если новый интервал начинается позже конца последнего, добавляем его отдельно. Иначе расширяем конец уже собранного интервала до максимальной границы:
Строгое сравнение оставляет соприкасающиеся интервалы в одной группе. Для полуоткрытых границ результат примера будет таким:
После сортировки алгоритм делает один проход и работает за O(n log n), где основное время уходит на сортировку.
📣 C++ Ready | #практика
Дан список занятых временных интервалов. Нужно объединить пересекающиеся и соприкасающиеся отрезки.
Представляем каждый интервал парой начала и конца. Считаем границы полуоткрытыми, поэтому отрезки [2, 5) и [5, 7) тоже объединяются:
#include <algorithm>
#include <utility>
#include <vector>
using Interval = std::pair<int, int>;
std::vector<Interval> busy{
{1, 3}, {2, 5}, {8, 10}, {10, 12}
};
Сначала сортируем пары по началу, а при равенстве по концу. После сортировки каждый следующий интервал можно сравнить только с последним результатом:
std::sort(busy.begin(), busy.end());
std::vector<Interval> merged;
Если новый интервал начинается позже конца последнего, добавляем его отдельно. Иначе расширяем конец уже собранного интервала до максимальной границы:
for (const auto& current : busy) {
if (merged.empty() ||
merged.back().second < current.first) {
merged.push_back(current);
} else {
merged.back().second = std::max(
merged.back().second, current.second);
}
}Строгое сравнение оставляет соприкасающиеся интервалы в одной группе. Для полуоткрытых границ результат примера будет таким:
[1, 5) [8, 12)
После сортировки алгоритм делает один проход и работает за O(n log n), где основное время уходит на сортировку.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍4🔥3
Схема протокола acquire-release в C++!
Release-store и acquire-load помогают одному потоку опубликовать подготовленные данные, а другому безопасно увидеть их после сигнала готовности. Синхронизация возникает, когда acquire-load получает значение от release-store того же atomic-объекта.
На картинке показаны оба потока, общий флаг x и порядок операций до и после передачи сигнала. Подпись отдельно подчёркивает важное условие. Обе операции должны использовать один и тот же atomic-флаг.
📣 C++ Ready | #совет
Release-store и acquire-load помогают одному потоку опубликовать подготовленные данные, а другому безопасно увидеть их после сигнала готовности. Синхронизация возникает, когда acquire-load получает значение от release-store того же atomic-объекта.
На картинке показаны оба потока, общий флаг x и порядок операций до и после передачи сигнала. Подпись отдельно подчёркивает важное условие. Обе операции должны использовать один и тот же atomic-флаг.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤3🔥3🤝1
Знали, что ключ в std::map можно переименовать без пересоздания узла?
Ключи внутри map нельзя менять напрямую, потому что контейнер поддерживает порядок элементов. Начиная с C++17 элемент можно временно извлечь как node handle, изменить его ключ и вставить обратно без копирования хранимого значения.
Сначала извлекаем элемент и проверяем, что он найден:
Пустой node handle означает, что такого ключа в контейнере не было. Пока узел извлечён, он не участвует в сортировке map, поэтому ключ можно изменить:
Сам объект и его mapped value остаются внутри узла. Это полезно, когда значение дорого копировать или оно не копируется вовсе:
Теперь пробуем вернуть узел и проверяем результат, потому что newKey мог уже появиться в map:
При конфликте запись не теряется, она остаётся в result.node. Можно восстановить старый ключ и вернуть её в контейнер:
Успешная вставка возвращает тот же узел без пересоздания значения. Такой приём удобен для переименования ключей, если важно сохранить объект и явно обработать возможный конфликт.
📣 C++ Ready | #совет
Ключи внутри map нельзя менять напрямую, потому что контейнер поддерживает порядок элементов. Начиная с C++17 элемент можно временно извлечь как node handle, изменить его ключ и вставить обратно без копирования хранимого значения.
Сначала извлекаем элемент и проверяем, что он найден:
auto node = users.extract(oldKey);
if (node.empty()) return;
Пустой node handle означает, что такого ключа в контейнере не было. Пока узел извлечён, он не участвует в сортировке map, поэтому ключ можно изменить:
auto oldKey = node.key();
node.key() = newKey;
Сам объект и его mapped value остаются внутри узла. Это полезно, когда значение дорого копировать или оно не копируется вовсе:
auto& profile = node.mapped();
profile.lastSeen = now;
Теперь пробуем вернуть узел и проверяем результат, потому что newKey мог уже появиться в map:
auto result = users.insert(std::move(node));
if (result.inserted) {
std::cout << "renamed\n";
}
При конфликте запись не теряется, она остаётся в result.node. Можно восстановить старый ключ и вернуть её в контейнер:
if (!result.inserted) {
result.node.key() = oldKey;
users.insert(std::move(result.node));
}Успешная вставка возвращает тот же узел без пересоздания значения. Такой приём удобен для переименования ключей, если важно сохранить объект и явно обработать возможный конфликт.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍4🔥2
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥12❤4👍3