tgindex
C++ | Programming

C++ | Programming

Статистика
@mir_cppрусский

Реклама: @sochnosochno

Последний пост
12 авг.
Последнее чтение
12 авг.
Постов за неделю
1
Всего постов
37
Тип
открытый
Язык
русский
В каталоге с
12 авг.
Подписчики
24 245
−852 за 6 дн.
Сутки
−184
−0,75%
Неделя
 
Месяц
 
Просмотров на пост
5 721
37 постов
Вовлечённость
23,6%
к подписчикам
Постов в день
0,1
всего 37
Упоминаний
0
каналов
Охват размещения
оценка
1/24сутки в ленте
4 384
1/48двое суток
5 024
1/72трое суток
5 418

Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.

Посты

  • 12 авг.4 94367

    Шпаргалка по STL в C++! Например, vector удобен для быстрого доступа по индексу, set хранит элементы в отсортированном виде, а priority_queue помогает быстро получать максимальный элемент. На картинке основные контейнеры STL, частые операции, асимптотика, примеры использования sort, binary_search, lower_bound, upper_bound, min_element, max_element, pair, tuple и других полезных инструментов.

  • 6 авг.5 07179

    Дефолтный конструктор. Введение #новичкам Описание корректного интерфейса создания объекта - важная часть проектирования класса. Поэтому надо понимать нюансики работы с конструкторами, чтобы все правильно организовать. Ну и конечно самый базовый и, потенциально, самый сложный с точки зрения языка - конструктор по-умолчанию. class NoConstructor { int total; public: void accumulate (int x) { total += x; } }; Example ex; Казалось бы, у Example вообще не определено ни одного конструктора. Но тем не менее объект успешно создался. Как так? Дефолтный конструктор умеет за вас генерировать сам компилятор. А почему бы и нет? Смотря со стороны даже вполне логично и понятно, что он должен делать: вызывать конструкторы по умолчанию для всех нестатических членов и базовых классов в порядке объявления. Если мне от дефолтного конструктора нужно только это, то я могу просто положиться на компилятор. Таким образом конструктор по-умолчанию входит в число специальных методов классов, которые компилятор сам умеет генерить. Однако, не все так просто. Как только вы определите хотя бы один другой конструктор, компилятор перестанет генерить дефолтный: class ParametrizedConstructor { int total; public: ParametrizedConstructor(int initial_value) : total(initial_value) { }; void accumulate (int x) { total += x; }; }; ParametrizedConstructor ex (100); // ok ParametrizedConstructor ex1; // error: no default constructor Оно и понятно: если вы не определяли никакой конструктор, значит вы довольны дефолтным поведением. Но как только вы сами определили конструктор, вы сказали компилятору, что дефолтное поведение вам не подходит и вы берете ответственность за способы создания объектов этого класса. И компилятор не смеет перечить вашей задумке. Очень может быть, что вы хотите, чтобы Example2 создавался только через параметрический конструктор и больше никак. По сути, именно это и прописано сейчас в классе. Если бы компилятор неявно добавил конструктор по-умолчанию, то это нарушило бы контракт вашего класса. Если вы все-таки хотите, чтобы у вас была возможность создать объект по-умолчанию, то вам явно нужно добавить конструктор без аргументов: class ParametrizedAndDefaultConstructor { int total; public: ParametrizedAndDefaultConstructor() = default; // или так ParametrizedAndDefaultConstructor() {} // или так ParametrizedAndDefaultConstructor(int initial_value) : total(initial_value) { }; void accumulate (int x) { total += x; }; }; ParametrizedAndDefaultConstructor ex(100); // ok ParametrizedAndDefaultConstructor ex1; // ok ParametrizedAndDefaultConstructor() = default; - вы явно определяете конструктор по-умолчанию, но так же явно просите компилятор о том, чтобы его поведение было как если бы сам компилятор его генерировал. ParametrizedAndDefaultConstructor() {/something/} - это вы уже самостоятельно определяете, какую дополнительную логику должен иметь этот конструктор. Он делает все то же самое, что и тривиальный, только вдобавок выполняется еще и то, что вы указали внутри фигурных скобок. Такой конструктор называется уже нетривиальным, даже если его тело в итоге оказалось пустым.

  • 26 июл.8 043941

    Размер enum'а #опытным Перечисления - это по факту именованные числа. Каждому перечислителю ставится в соответствие число, к которому перечислитель может приводиться. Оно либо указывается явно, либо проставляется компилятором. Но тогда встает вопрос: а сколько весит enum? Мы же про эффективность и хотим, чтобы данные занимали минимально возможное пространство. Мы можем явно написать: enum MY_FAVOURITE_FRUITS { E_APPLE = 0x01, E_WATERMELON = 0x02, E_COCONUT = 0x04, E_STRAWBERRY = 0x08, E_CHERRY = 0x10, E_PINEAPPLE = 0x20, E_BANANA = 0x40, E_MANGO = 0x80, E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell? }; Мы как бы явно говорим, что ограничиваем размер enum'а 8-ью битами. Но будет ли его размер реально 8 бит? Не факт. Компилятор может выбрать любой подходящий по размеру тип, главное, чтобы он мог вместить все элементы перечисления. Это может быть char, short или int. И все это разного размера. Неприятно, что на это нельзя было влиять. Но прочь неприятности, потому что в С++11, помимо enum class появилась возможность указания размера scoped и unscoped enum'ов: enum class E_MY_FAVOURITE_FRUITS : unsigned char { E_APPLE = 0x01, E_WATERMELON = 0x02, E_COCONUT = 0x04, E_STRAWBERRY = 0x08, E_CHERRY = 0x10, E_PINEAPPLE = 0x20, E_BANANA = 0x40, E_MANGO = 0x80, E_DEVIL_FRUIT = 0xFF }; И теперь вы может контролировать и сами задавать размер перечисления.

  • 23 июл.6 876174

    Сколько потоков можно запустить на одной машине? #опытным Из всех утюгов говорят про асинхронность вычислений и экономию ресурсов ОС. Зачем это нужно? И почему я не могу на каждую задачу запускать отдельный поток? Есть ли вообще какой-нибудь предел по количеству потоков? Потоки - вещь хорошая и полезная. Не зря же недавно в питон таки завезли возможность их прямого использования. Но с потоками нужно быть аккуратным. Это ресурс. А ресурсы имеют свойство заканчиваться, когда их нерационально используют. Классический пример: к вам в приложение приходит какой-то запрос на обработку. Первая мысль - а давайте под него выделим свой поток! Они же независимо и параллельно исполняются! Ну и уже здесь мы поймали первую ошибку. Да, потоки могут параллельно исполняться. Но количество реально параллельно исполняемых потоков в системе ограничено. И ограничено оно количеством вычислительных юнитов процессора. Максимальное число реально параллельных потоков можно узнать с помощью std::thread::hardware_concurrency(). Есть кстати технология HyperThreading от Intel, которая позволяет на одном физическом ядре получить 2 логических, увеличивая таким образом число потенциально параллельных потоков в 2 раза. Ускорения в 2 раза конечно не будет, но тем не менее. "Ну хорошо. Не все 100500 потоков, которые я создал, исполняются параллельно. Не всегда нужно параллельное одновременное исполнение всех тредов. Почему всем не нравится много потоков? Поток - это ресурс, который занимает место и им нужно управлять. Как минимум под каждый поток выделяется свой стек. На десктопах это 1-8 МБ. Поделите количество оперативной памяти на размер стека и получите максимально возможное количество потоков, которое в принципе может поместиться в память. В реальности количество будет еще меньше. Есть же еще затраты на управление потоками. Их же нужно переключать и выдавать каждому подходящий квант времени. Переключает потоки шедулер ядра и на это тоже тратится процессорное время. С ростом количества потоков можно прийти к ситуации, когда затраты на управление потоками будут превышать количество полезной работы, совершаемой потоками. Ну и в некоторых системах явно прописано ограничение на количество потоков. В линуксе, например, можно увидеть заветную числеку с помощью команды cat /proc/sys/kernel/threads-max. Асинхронность - это в первую очередь про асинхронный ввод-вывод. Тут все завязано на событийно ориентированное апи системы и файловые дискрипторы, которых фактически может быть гораздо больше, чем количество потоков. Если у вас есть сервис, который активно работает с сетью - без асинхронности не обойтись. Подход с созданием отдельного потока на каждый запрос перестанет адекватно масштабироваться уже после сотни другой одновременных подключений.

  • 16 июл.5 79737

    Ковариантные возвращаемые типы Есть такое интересное понятие, о котором вы возможно ни разу не слышали. Пример из поста выше с методами clone и create можно было написать иначе: class Shape { public: virtual ~Shape() { } // A virtual destructor // ... virtual Shape* clone() const = 0; // Uses the copy constructor virtual Shape* create() const = 0; // Uses the default constructor }; class Circle : public Shape { public: Circle* clone() const override; Circle* create() const override; // ... }; Circle* Circle::clone() const { return new Circle(this); } Circle* Circle::create() const { return new Circle(); } Вы скажете: "Сигнатуры не совпадают! Код не скомпилируется!". А я скажу: "Shape и Circle - ковариантные типы". С++ разрешает наследнику переопределять методы с возвращаемым типом, который является наследником типа метода из базового класса. Говорят, что это даже называется идиомой С++. Какие юзкейсы у этой идиомы? По факту всего один. Представьте, что все методы возвращают один тип Shape. Вы создали объект Circle в куче и присвоили указатель на него к указателю на Circle. Тогда при клонировании объекта Circle вам вернется указатель на объект базового класса. И по хорошему его надо динамик кастить к Circle, чтобы работать с конкретным типом наследника. А это оверхэд: Circle *circle1 = new Circle(); Shape *shape = d1->clone(); Circle *circle2 = dynamic_cast<Circle *>(shape); if(circle2) { // Use circle2 here. } Выглядит не очень. Посмотрим, как изменится код, если методы Circle будут возвращать указатель на Circle: Circle *circle1 = new Circle(); Circle *circle2 = d1->clone(); Выглядит намного лучше. Но вот вопрос: почему вы нигде не увидите в коде применения ковариантных типов? Потому что этот подход не работает с умными указателями, которые де факто являются стандартом при возвращении объектов из фабрик. std::unique_ptr<Circle> не является наследником std::unique_ptr<Shape>, поэтому они и не ковариантные типы и сигнатуры методов будут несовместимы. Возвращение сырых указателей - супер bad practice, один только этот факт заставляет отказаться от такого подхода. Тем более полиморфные объекты и придумали для того, чтобы использовать их полиморфно. То есть через ссылку или указатель на базовый класс. Зачем оперировать полиморфным объектом с указателем на конкретный тип - не очень понятно. Раньше, до изобретения умных указателей, идиома была легитимна. Теперь же она отправилась на свалку истории.

  • 14 июл.6 174103

    std::byte: честные “сырые байты” вместо char/uint8_t! Он помогает не путать бинарные данные с текстом и не ловить случайную арифметику над “байтами”. Часто под байты берут uint8_t или char: void send(const std::vector<uint8_t>& data); int main() { std::vector<uint8_t> buf = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; send(buf); } Проблема в том, что такие типы легко начинают жить как “маленькие числа” или “символы”: кто-то делает buf[i] + 1, кто-то печатает как текст, и смысл буфера расползается. В C++17 для этого есть std::byte — отдельный тип именно для бинарщины. С ним нельзя случайно делать арифметику, зато можно явно делать побитовые операции. Как хранить и передавать байты: void send(std::span<const std::byte> data); int main() { std::vector<std::byte> buf = { std::byte{0x48}, std::byte{0x65}, std::byte{0x6C}, std::byte{0x6C}, std::byte{0x6F} }; send(buf); } Если нужно получить число — делай это явно: #include <bit> std::byte b{0xFF}; int x = std::to_integer<int>(b); std::byte делает работу с бинарными данными ясной: меньше неявных превращений, меньше путаницы “текст vs байты”, больше контроля.

  • 12 июл.8 11556

    Генерируем UUID v4 на чистом C++! Создадим случайный идентификатор формата xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx, пригодный для токенов, сессий и тестовых данных. std::string uuid_v4() { uint8_t bytes[16]; Инициализируем буфер из 16 байтов для будущего UUID. std::random_device rd; std::uniform_int_distribution<int> dist(0, 255); for (auto &b : bytes) b = static_cast<uint8_t>(dist(rd)); Генерируем 16 случайных байтов с помощью std::random_device и равномерного распределения. bytes[6] = (bytes[6] & 0x0F) | 0x40; // версия 4 bytes[8] = (bytes[8] & 0x3F) | 0x80; // вариант RFC 4122 Корректируем 7-й и 9-й байты под спецификацию UUID v4 (версия и вариант). std::ostringstream oss; for (int i = 0; i < 16; ++i) { oss << std::hex << std::setw(2) << std::setfill('0') << int(bytes[i]); if (i == 3 || i == 5 || i == 7 || i == 9) oss << '-'; } return oss.str(); } Форматируем байты в шестнадцатеричный вид, вставляя дефисы в нужных позициях. Всё на чистом стандартном C++, без единой дополнительной библиотеки!

  • 11 июл.4 12383

    emplace_back vs push_back #новичкам Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора). В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back. template< class... Args > reference emplace_back( Args&&... args ); // returns ref to created element void push_back( const T& value ); void push_back( T&& value ); По сигнатуре видно, что они предназначены немного для разного. Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так: template <typename... Args> reference emplace_back(Args&&... args) { if (size == capacity) grow(); return *new (start + size++) T(std::forward<Args>(args)...); } Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться. push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back: void push_back(T&& value) { emplace_back(std::move(value)); } Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов. То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая. Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.

  • 10 июл.5 11771

    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 должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции.

  • 9 июл.7 11683

    🔧 Что делать, если 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 "медленный", не спешите винить алгоритм. Лучше проверьте, что вы передаёте ему на вход.

  • 8 июл.4 10085

    std::unordered_map::emplace_hint() std::unordered_map::emplace_hint() позволяет вставлять элементы в хеш-таблицу с подсказкой для оптимизации. Это особенно полезно, если известно, куда примерно должен встать новый элемент, ускоряя операцию вставки.

  • 7 июл.7 101161

    Засовываем исключение в исключение #опытным Вы знали, что в плюсах есть вложенные исключения? Такие исключения могут хранить в себе несколько исключений. Сегодня мы посмотрим, что это за зверь такой. Начнем с применения. В прошлом посте мы создавали новое исключение на основе строки ошибки обрабатываемого исключения. В этом случае нужно писать определенное количество бойлерплейта и теряется информация о типе изначального исключения. Чтобы избежать этих проблем, мы можем бросить новое исключение, которое будет в себе содержать старое: 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.

  • 6 июл.4 10368

    Динамический полиморфизм. std::function #новичкам В прошлом посте поговорили, что динамический полиморфизм реализуется не только через иерархии классов и виртуальные методы. Есть другой прекрасный инструмент - 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.

  • 5 июл.7 10695

    Оптимизация кода с 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 (например, парсинг строки в число). Когда важно избежать неопределенного состояния (например, с переменной внутри класса).

  • 4 июл.4 10283

    📂 Concurrency и parallelism - это не одно и то же! Concurrency - это про структуру программы: несколько задач могут продвигаться вперёд в перекрывающиеся промежутки времени. Parallelism - это уже физическое одновременное выполнение, например на нескольких ядрах CPU. На картинке — визуальное отличие concurrent execution от parallel execution. Сохрани, чтобы не потерять!

  • 3 июл.8 104105

    Правильно захватываем по ссылке объект в лямбду #опытным В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к 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.

  • 30 июн.4 10391

    std::to_address #опытным В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции 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.

  • 27 июн.5 10394

    🔥 Оптимизация кода на C++: Ранний возврат вместо вложенных условий Привет, друзья! Сегодня хочу поговорить об одной важной технике, которая делает код чище и читабельнее — ранний возврат (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, если можно этого не делать. - Используйте ранний возврат, чтобы код был линейным и понятным. - Чем меньше уровней вложенности — тем легче отладка и сопровождение.

  • 21 июн.4 10783

    std::initializer_list Присваивайте значения контейнерам непосредственно с помощью списка инициализаторов, как это можно делать с C-массивами. Это справедливо и для вложенных контейнеров. Скажите спасибо С++11.

  • 15 июн.5 11165

    📂 Напоминалка по HTTP-статусам! Например, код 200 означает, что всё прошло успешно, а 404 сообщает, что страница не найдена. Очень полезно держать под рукой, когда работаешь с API или отлаживаешь backend. На картинке показаны самые часто используемые статусы от 100 до 599. Сохрани, чтобы не забыть!