C++ | Programming
23.9K subscribers
73 photos
3 links
Реклама: @sochnosochno
Download Telegram
При каких условиях ReadFile или WriteFile могут не передать все байты, и как это обнаружить?

Один из клиентов хотел понять, в каких случаях функции ReadFile и WriteFile могут не передать все байты, и как определить, что это произошло.

Очевидная причина, по которой ReadFile может не прочитать все байты, - это то, что просто нет такого количества данных для чтения.

- Для файлов на диске это обычно происходит, если попытаться читать за концом файла.
- То же может случиться и с другими типами дескрипторов: для pipe в неблокирующем режиме в пайпе может не оказаться достаточного количества данных.
- Если это message pipe, сообщение может быть меньше, чем размер вашего буфера.
- Или при обращении к устройству - оно может не иметь всех байтов в наличии.

Аналогично, очевидная причина, по которой WriteFile может не записать все байты, — это нехватка места.

- Для файлов на диске это может быть полный диск или достижение лимита квоты.
- Для pipe в неблокирующем режиме запись может быть укорочена, если в буфере пайпа недостаточно места, чтобы принять все запрошенные данные.

Во всех таких случаях укороченную запись можно обнаружить, проверив, меньше ли число реально записанных байт по сравнению с запрошенным.

Если количество реально переданных байт больше нуля, то функции ReadFile и WriteFile вернут успех, но при этом передадут меньше байт, чем было запрошено.
Что такое виртуальный деструктор и зачем он используется в C++?

В C++ виртуальный деструктор используется для правильного освобождения памяти при удалении объекта через указатель на базовый класс. Если базовый класс имеет виртуальный деструктор, то при удалении объекта через указатель на базовый класс будет вызван деструктор не только базового класса, но и всех его производных классов. Это позволяет избежать утечек памяти и неопределенного поведения при работе с полиморфными объектами.

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

Вы можете написать функцию, которая будет возвращать два и более значений, с помощью std::tuple и std::tie.
Состоялся релиз третьей мажорной версии легковесной интегрированной среды разработки Red Panda C++ для языков C, C++ и GNU Assembly. Предыдущая стабильная мажорная версия проекта Red Panda C++ 2.1 вышла в октябре 2022 года.
Согласно данным OpenNET, проект Red Panda C++ продолжает развитие IDE Dev-C++, которая была переписана с Delphi на С++ с использованием библиотеки Qt.
Исходный код проекта опубликован на GitHub под лицензией GPLv3. Изначально среда разработки Dev-C++ поддерживала только платформу Windows, но в Red Panda C++ добавлена поддержка платформ Linux и macOS.
Особенности проекта Red Panda C++:
• поддержка экранов с высокой плотностью пикселей;
• автодополнение кода и проверка синтаксиса во время редактирования, сворачивания блоков кода, автоформатирование;
• расширенные возможности отладчика, такие как стек вызовов и интерфейс для просмотра содержимого памяти;
• поддержка тем оформления и цветовых схем. Наличие тёмного режима интерфейса;
• поддержка кодировки UTF-8;
• системы для ведения списков задач (TODO) и закладок;
• значительно улучшенные функции поиска и замены;
• возможности для проведения рефакторинга (переименования имён);
• поддержка компилятора SDCC.
Основные изменения и дополнения в Red Panda C++ 3.0:
• расширена поддержка символов Unicode и внесены улучшения, связанные со шрифтами;
• поддержка визуализации управляющих ASCII-символов и пробелов;
• улучшена подсветка синтаксиса, добавлена подсветка атрибутов C++ и подсветка слов в строках и комментариях;
• проведена оптимизация кода для загрузки и редактирования файлов;
• добавлена поддержка анонимных классов и лямбда-выражений в C++;
• в настройки добавлена панель со списком шрифтов;
• в таблицу с трассировкой стека в отладчике добавлены всплывающие подсказки;
• обеспечено открытие в редакторе файлов ".def" (определения состава модуля);
• добавлена возможность обращения к элементам меню через горячие клавиши;
• переработан интерфейс для управления темами оформления;
• появились дополнительные режимы форматирования кода;
• добавлено меню для управления дополнением вводимого кода.
Обогащаем исключение
#новичкам

Еще один простой и известный лайфхак, как можно более эффективно работать с исключениями.

Вот пилишь проект на 100500 строк кода. Запускаешь и вылетает ошибка: "абракадабра, ты хреновый программист". И как понять, откуда это исключение прилетело?

И начинается. Все обкладывается принтами и смотрится после какого принта программа падает. Но можно сделать немного умнее.

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

В прошлом посте мы просто прокидывали исключение дальше, без добавления в него какой-то информации. Но можно бросить новое исключение и передать туда сообщение об ошибке первого:

void ComplicatedCalculations() try {
// use db
} catch (std::exception& ex) {
throw std::runtime_error(std::string("ComplicatedCalculations Error:") + ex.what());
}

void HandlingCalculations() try {
ComplicatedCalculations();
} catch (std::exception& ex) {
throw std::runtime_error(std::string("HandlingCalculations Error:") + ex.what());
}


В итоге вы на верхнем слое приложения финально обрабатываете все исключения и получаете фактически стек вызовов, которые они пролетают. И уже намного проще найти место конкретной проблемы, особенно если есть информативные логи.

Track your problems to the bottom. Stay cool.
Раскрываем вложенное исключение
#опытным

Каша заварилась, пришло время расхлебывать. В прошлом посте мы успешно завернули кучу исключений в одно, теперь нужно как-то все обратно развернуть. Для этого существует функция std::rethrow_if_nested. Работает она вот так:

class nested_exception
{
exception_ptr _M_ptr;
public:
/// The default constructor stores the current exception (if any).
nested_exception() noexcept : _M_ptr(current_exception()) { }

[[noreturn]] void rethrow_nested() const {
if (_M_ptr)
// just throw _M_ptr
rethrow_exception(_M_ptr);
std::terminate();
}
...
};

template<class E>
void rethrow_if_nested(const E& e) {
if (auto p = dynamic_cast<const std::nested_exception*>(std::addressof(e)))
p->rethrow_nested();
}



Так как объект вложенного исключения - это наследник std::nested_exception, то мы имеем право динамически привести указатель переданного в rethrow_if_nested исключения к std::nested_exception. Если каст прошел успешно, то бросается исключение, сохраненное в nested_exception. Если каст провалился, значит E - это уже не наследник nested_exception и у нас в руках самое первое исключение в цепочке.

Обычно в статьях по этой теме приводят мягко говоря странные примеры того, как надо обрабатывать вложенные исключения:

int main() {
try {
Login("test@example.com", "secret");
} catch (SecurityError &e) {
std::cout << "Caught a SecurityError";
try {
std::rethrow_if_nested(e);
} catch (AuthenticationError &e) {
std::cout << "\nNested AuthenticationError: " << e.Email;
}
}
std::cout << "\nProgram recovered";
}


Встречайте вложенные try-catch с заранее известной глубиной вложенности. Это конечно никуда не годится.

В реальности при повсеместном использовании вложенных исключений мы не знаем, сколько раз нам нужно сделать rethrow_if_nested. Но при этом мы точно знаем, что если исключение последнее в цепочке, то rethrow_if_nested ничего не сделает. Это же идеальное условие для остановки рекурсии!

Рекурсия решает проблему неопределенного количества уровней вложенности исключений и помогает удобно подряд печатать соответствующие сообщения об ошибках:

void print_exception(const std::exception &e, int level = 0) {
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception &nestedException) {
print_exception(nestedException, level + 1);
} catch (...) {
}
}

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"));
}

int main() {
try {
HandlingCalculations();
} catch (const std::exception &e) {
print_exception(e);
}
}
// OUTPUT:
// Handling Calculations Error
// Complicated Calculations Error
// Some DB Error


В этом случае на добавление дополнительной информации к исключению вы тратите всего 1 строчку, которая мало чем отличается от возбуждения нового исключения. Плюс в коде логики проекта вы одной строчкой обрабатываете все вложенные исключения. Красота!

Очень нишевая функциональность, которую вы вряд ли встретите в реальных проектах. Однако будет прикольно попробовать ее в своих пет-проектах, если вы не гнушаетесь исключений.

Have a good error handling system. Stay cool.
Динамический полиморфизм: разделяемые библиотеки
#опытным

В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.

Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь libc и libstdc++ например неявно подгружаются на старте). Основную часть таких библиотек мы прописываем в опциях линковки.

Однако динамические библиотеки можно неявно подгружать прямо из кода! Для этого на разных системах существует разное системное апи, но для юниксов это dlopen+dlsym.

dlopen по заданному пути файла библиотеки возвращает void указатель на хэндлер этой либы. С помощью хэндлера, функции dlsym и текстового названия определенной функции можно получить указатель на эту функцию и вызвать ее.

Тут пример будет довольно длинный, поэтому начнем с начала.

У вас есть какой-то интерфейс и вы хотите передать реализацию этого интерфейса другой команде, которая имеет чуть больше скилла в данной доменной области:

class PluginInterface {
public:
virtual int method() = 0;
};

extern "C" PluginInterface* create_plugin();

extern "C" void destroy_plugin(PluginInterface* obj);


Эта команда берет и реализует этот интерфейс:

#include "PluginInterface.hpp"
#include <iostream>

class MyPlugin : public PluginInterface {
public:
virtual void method() override;
};

int MyPlugin::method() {
std::cout << "Method is called\n";
return 42;
}

extern "C" PluginInterface* create_plugin() {
return new MyPlugin();
}

extern "C" void destroy_plugin(PluginInterface* obj) {
delete obj;
}


Также вы договорились, что каждая реализация интерфейса предоставляет 2 функции: создания и уничтожения наследников.

Функции create_plugin и destroy_plugin обязаны иметь сишную линковку, чтобы достать указатели на них по их имени из библиотеки с помощью dlsym:

#include "PluginInterface.hpp"
#include <dlfcn.h>
#include <iostream>

typedef PluginInterface *(*creatorFunction)();
typedef void (*destroyerFunction)(PluginInterface *);

int main() {
void *handle = dlopen("myplugin.so", RTLD_LAZY);
if (!handle) {
std::println("dlopen failure: {}", dlerror());
return 1;
}
creatorFunction create = reinterpret_cast<creatorFunction>(dlsym(handle, "create_plugin"));
destroyerFunction destroy = reinterpret_cast<destroyerFunction>(dlsym(handle, "destroy_plugin"));

PluginInterface *plugin = (*create)();
std::println("{}", plugin->method());
(*destroy)(plugin);
dlclose(handle);
}


С помощью dlopen и пути к библиотеке-реализации интерфейса получает хэндлер либы. Дальше получаем указатели на функции создания и уничтожения плагина с помощью dlsym, хэндлера и текстовому имени функции.

Разве по имени функции можно получить указатель на нее? Похоже на какую-то рефлексию с первого взгляда.

Тут дело в именах функций и отображении их в символы бинарного файла при компиляции. В С нет никакого манглинга имен, поэтому в готовом бинарном файле можно найти символ, соответствующий названию функции, и связанный с ним адрес этой фукнции. Именно поэтому create_plugin и destroy_plugin помечены extern "C", чтобы их имена обрабатывались по правилам С.

По сути, это все еще про указатели на функции, просто интересно, что на момент компиляции программы у вас может не быть реализации этих функции.

Choose the right name. Stay cool.
Книга по C++, которую ты обязан прочитать в 2025 году

Я почему-то уверен, что она у тебя уже есть. Либо в списке литературы для самообразования, либо где-то среди скачанных pdf, а может, даже в печатном виде. Возможно, ты даже начинал её читать, но… работа, таски, собеседования… деньги вроде и так платят… и ты так и не осилил Мейерса "Эффективный и современный С++".

А ведь именно эта книга — ключ, который поможет тебе пробить потолок уровня middle и начать движение к senior.

Да, можно скачать список из 400 популярных вопросов, выучить их и бодро ответить на вопрос: «Расскажи про std::unique_ptr». А потом тебе прилетает дополнительный вопрос: «А как изменится размер std::unique_ptr при использовании пользовательского удалителя?» Иии… Я не буду додумывать твой уровень знаний. Попробуй сам ответить на этот вопрос и поставь себе оценку
И это только один из примеров тонких нюансов, которые описаны в этой книге и которые реально спрашивают на собеседованиях.

Ещё одна сильная сторона книги: почти каждая фича современного C++ иллюстрируется примерами её использования в реальном проде. А это большая редкость для технической литературы.

Хватит откладывать эту книгу на «потом». Поставь цель на этот год и, наконец-то, найди время, чтобы прочитать 300 страниц "Эффективного и современного С++".
starts_with, ends_with
#новичкам

До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.

В нем появляются стандартные методы std::string/std::string_view .starts_with() и .ends_with():

constexpr bool starts_with(string_view sv) const noexcept;
constexpr bool starts_with(CharT c ) const noexcept;
constexpr bool starts_with(const CharT* s ) const;

constexpr bool ends_with(string_view sv )const noexcept;
constexpr bool ends_with(CharT c ) const noexcept;
constexpr bool ends_with(const CharT* s ) const;


Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.

const std::string url { "https://isocpp.org" };

// string literals
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";

// a single char:
if (url.starts_with('h') && url.ends_with('g'))
std::cout << "letters matched!\n";


Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.

Примерчик:

const std::vector<std::string> tokens { 
"<header>",
"<h1>",
"Hello World",
"</h1>",
"<p>",
"This is my super cool new web site.",
"</p>",
"<p>",
"Have a look and try!",
"</p>",
"</header>"
};

auto text = tokens |
std::views::filter([](const std::string& s) {
if (s.starts_with("<") || s.ends_with(">"))
return false;

return true;
});

for (const auto& str : text)
std::cout << str << std::endl;

// OUTPUT:
// Hello World
// This is my super cool new web site.
// Have a look and try!


Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.

В общем, полезные штуки, которые помогают заменить громоздкие кастомные проверки через find на использование выразительных функций.

Be expressive. Stay cool.
Примеры C++ кода до и после Ranges

Объединяем все строки в данном диапазоне в одно значение.
Можно ли в C++ умножать или делить целое значение на степени двойки, не используя операций "*" или "/"?

Это можно делать в любом языке, где есть побитовый сдвиг. Умножению значения x на 2, например, соответствует операция x > 2.
С какой книги начать изучение многопоточки в C++

Традиционно изучение многопоточки рекомендуют начать с книги Энтони Уильямса "C++. Практика многопоточного программирования". Но, на мой взгляд, эта рекомендация сродни совету новичку читать Страуструпа или K&R. Да, в профессионализме этих авторов никто не сомневается, но все эти книги предназначены для инженеров с опытом, а не для разработчиков, которые делают свои первые шаги.

Поэтому рекомендую вам на время отложить книгу Уильямса, пока вы не познакомитесь с книгой Райнера Гримма "Параллельное программирование на современном С++". Уточню сразу, что главу по устройству памяти при первом знакомстве с многопоточкой лучше пропустить, как это и советует сделать автор, восприятие остального материала от этого не пострадает

Лично мне книга понравилась, с одной стороны, кратким описанием многопоточных конструкций С++, а, с другой стороны, множеством наглядных примеров их использования. Книгу достаточно легко читать, и я периодически ловил себя на мысли, что опять прочитал несколько десятков страниц и даже этого не заметил. Одним словом, рекомендую попробовать, возможно Гримм подойдет вам больше, чем Уильямс
Конфигурация и переменные окружения
#опытным

Любой серьезный сервис нуждается в конфигурации. Файлы конфигурации (JSON, YAML, INI) — популярный способ хранения настроек приложений. Так параметры можно хранить в репозитории, версионировать, да и просто удобно, когда все можно менять в одном месте и никак не менять команду запуска.

Однако не одними конфигами едины. Не всегда они подходят для решения определенных задач.

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

Не совсем. Что если какой-нибудь умник после тестирования приложения случайно закоммитит ключ в репозиторий? Это серьезная опасность: репозиторий вашей команды скорее всего может читать любой сотрудник, у которого есть доступ к вашей системе совместной разработки. А если у вас еще сторонние лица имеют доступ к репе... Не завидую вам. Безопасники будут радостно потирать ладоши, когда будут вам пистоны вставлять за эту ошибку. Потом еще ключ перевыпускать скомпрометированный, долго и мучительно заменять его... Сам наступал на эти грабли, приятного мало.

{
"data_key": "qwerty123" // Утечка при публикации кода!
}


Да и хранить ключ в открытом виде в файле на сервере такое себе. А если кто-нибудь подглядит?

То же самое можно сказать про креды базы данных, in-memory кэша, брокеров сообщений и прочего. Пароли могут быть скомпрометированы.

# config.yml (попадает в Git)
db:
host: db.example.com
username: admin
password: "P@ssw0rd123!" # Утечка при публикации кода!


А как с докерами и кубернетисами вашими работать? Иметь 100500 образов с разными настройками кредов и множить их постоянно? Выглядит, как не очень расширяемое решение.

Конечно же никто не хранит в конфигах чувствительные данные и специфичные для конкретного инстанса переменные. Вместо этого используют переменные окружения.

Переменные окружения можно установить видимыми только для конкретного запущенного docker контейнера:

docker run -e MY_VAR=value my_image


В k8s можно брать переменые окружения из отдельно развернутого и защищенного Vault. В этом случае вообще отсутсвует явное указание секрета:

env:
- name: MY_VAR
- value: vault:my_group/my_service#my_var


Переменные окружения не попадают в репозиторий -> нет компрометации секретов.

Можно без изменения конфига на одном и том же сервере тестировать приложение в разных контурах:

# Local
export DB_HOST=localhost

# Dev
export DB_HOST=dev-db.example.com


В общем, переменные окружения в приложении - полезная вещь, не стоит ими принебрегать.

К чему это я и причем здесь С++?

Ну нам же нужно выяснить, как в стандартных плюсах можно получать значения переменных окружения. Об этом поговорим в следующем посте.

Protect your secrets. Stay cool.
Изменяемые лямбда-функции

Ключевое слово mutable используется для сохранения состояния в лямбда-функциях. Обычно оператор вызова функции замыкания является константным. Другими словами — лямбда не может модифицировать переменные, захваченные по значению.

Но ключевое слово mutable может быть применено ко всей лямбда-функции, что сделает все её переменные изменяемыми.

Следует заметить, что в отличии от mutable-переменных в объявлении класса, мутабельные лямбда-функции должны использоваться относительно редко и очень аккуратно. Сохранение состояния между вызовами лямбда-функции может быть опасным и контринтуитивным.
Перегружаем шаблоны классов
#опытным

В прошлом посте я говорил, что нельзя перегружать шаблоны классов.

В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.

Но как и практически любое ограничение в С++, его можно хакнуть.

Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.

template <typename... T>
struct Foo;

template <typename T1>
struct Foo<T1> {};

template <typename T1, typename T2>
struct Foo<T1,T2> {};



Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.

Вот такие фокусы.

Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:

template <typename T, int N>
struct Foo<T, N> {}; // forbidden


Но все равно это хороший инструмент, которым можно пользоваться.
ssize_t
#новичкам

Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.

Зачем такой тип нужен?

В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void buf, size_t count);


Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:

char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) { / Обработка ошибки */ }


Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.

Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.

Use the right tool. Stay cool.
std::lcm

std::lcm вычисляет наименьшее общее кратное (НОК) двух чисел. Это полезно для задач на синхронизацию событий, работы с дробями и математических расчётов.
📂 Напоминалка по иерархии памяти и работе storage!

Например, регистры — это самый быстрый уровень памяти внутри CPU, кэш (L1/L2/L3) снижает latency за счёт локальности данных, RAM хранит рабочее состояние приложений, а SSD/HDD используются для персистентного хранения с существенно более высоким временем доступа.

На картинке — базовая иерархия памяти, взаимодействие CPU — cache — RAM — disk, а также упрощённая модель работы HDD и архитектура SSD.

Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
Недостатки std::make_shared. Непубличные конструкторы
#новичкам

std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.

Если конструктор публичный - проблем нет, просто вызываем std::make_shared.

Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.

Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:

struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};


Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.

В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.

А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.

Hide your secrets. Stay cool.
std::exchange

std::exchange заменяет значение переменной на новое и возвращает старое. Это полезно для сброса состояния, перемещения значений и написания атомарных операций.