Грокаем C++
5.98K subscribers
23 photos
3 files
312 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Экспресс совет для объявления некопируемых классов

Коротенький совет, как сделать ваш код более читаемым и понятным. Здесь мы говорили, что хорошей практикой запрещения копирования является определение копирующего конструктора и копирующего оператора присваивания как =delete. Это просто-напросто удалит этот метод, компилятор не будет для него генерировать код. Но иногда эти методы растворяются где-то в середине класса и они в глаза не бросаются. Хотя читающему код должно быть сразу понятно, что объект не предназначен для копирования.

Поэтому предлагаю один прием. Объявить макрос, в который передается имя класса и он подменяется на строку, которая содержит удаленные копирующий конструктор и копирующий оператор присваивания. И назвать этот макрос как-нибудь наподобие MAKE_ONLY_MOVABLE или DISALLOW_COPY. И поместить его первой же строчкой в теле класса.

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

Stay useful. Stay cool.

#goodpractice #cppcore #cpp11
Продолжение про вызов метода через указатель

В прошлом мы выяснили, как можно вызвать нестатический метод класса через указатель. Пост с кодом находится здесь. Для удобства, прикреплю его и к сюда. Сегодня мы попробуем разобраться, почему код такой и он еще и рабочий? Пойдем по порядку. Совсем тривиальные вещи затрагиваться не будут, будут только важные в контексте объяснения.

18 строка - Объявляется синоним к типу указателя на функцию, которая ничего не возвращает и принимает указатель на тип RandomType. Позже будет понятно, зачем это нужно.

19 строка - Есть 2 возможных синтаксиса для получения указателя на функцию: с амперсантом(сюрприз, кто не знал) и без. Без амперсанта не работает, потому такой синтаксис уже используется для вызова статических методов и будет путаница. Итак, мы получили какого-то неизвестного нам типа указатель на функцию. Идем дальше.

20 строка - Это трюк, который позволяет превратить указатель на функцию в указатель на число. Это необходимо для того, чтобы скастовать указатель _ptr из 19 строки к указателю на функцию типа func_ptr. Такого рода касты очень опасные и ведут к неопределенному поведению. Поэтому компиляторы просто их запрещают. Поэтому и нужно какое-то прокси состояние. Только компиляторы также запрещают кастовать указатель на функцию к указателю на число. Поэтому в ход идет наращивание индирекции указателя.

Мы можем посмотреть не на сам указатель, а на ячейку памяти, которая хранит наш указатель. И сказать, что по этому адресу лежит не указатель на функцию, а указатель на число. Вот так сделать можно. А потом кастуем указатель на число к указателю на функцию. Так тоже можно сделать.

И, наконец, важнейший пункт. Помните в книжках всегда говорили, что в методы скрытно передается указатель на вызывающий его объект this? Так вот сейчас вам скорее всего впервые понадобится это знание на практике!
Методы неполиморфных классов - те же самые обычные функции(кто не знал). От остальных их отличает лишь этот параметр this, который скрытно передается первым аргументом. Именно поэтому, чтобы вызвать нестатический метод, нам нужен объект. Чтобы передать его первым параметром в функцию. Иначе вызов будет не соответствовать сигнатуре.
Поэтому мы берем созданный на стеке объект, находим адрес первого байта и передаем его в метод в качестве аргумента.

И вуаля. Все работает. Выводится пятёрочка.

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

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

Конструируй абстракции и погружайся в детали. Stay cool.

#fun #memory #howitworks #cppcore #hardcore
Потокобезопасные константные методы

Представьте, что у вас есть есть кастомный контейнер, который хранит ваши данные каким-то нетривиальным образом. Как и у любого контейнера, у него должны быть методы аля insert(…) для вставки значения в контейнер и методы для доступа к элементам get(…)(оператор [], at() и тд). Согласно правилам организации хорошего и понятного кода вы объявили get() как const. Оно и понятно, ведь любой пользователь вашего контейнера будет понимать, что доступ к элементам контейнера никак не будет отражаться на его внутреннем устройстве. Это делает код более понятным и безопасным для пользователя. Все хорошо и прекрасно.

Но тут вам приходит идея сделать этот контейнер потокобезопасным. Проблем нет, заводим shared_mutex как поле объекта и лочим операции вставки и доступа. Используем read-write lock, чтобы несколько потоков одновременно могли безопасно читать из контейнера значения, а как только придёт пишущий поток, блокировать операции чтения и записи для других потоков. Компилируем это дело и получаем ошибку. Причём гцц вам как всегда выдаст самое понятное сообщение об ошибке. Такое, что глаза вытекают и мозг плавится. Но рано или поздно осознание придёт. Вы не можете изменять поля класса в константных методах. Что делать?

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

Поэтому в языке есть ключевое слово, про которое все, вплоть до мидлов, забывают и встречаются с ним только на собесах. Mutable. Этот keyword разрешает изменять какое-то поле класса в константных методах. Объявим наш shared_mutex как mutable и все заработает.

Решение элегантное, красивое и лаконичное. Только вот само использование этого приема попахивает нарушением инкапсуляции. Немножко совсем. Поэтому не стоит злоупотреблять этим приемом и скрывать им недостатки архитектуры класса. Если вам нужно поменять объект в константном методе и это не общепринятый кейс использования mutable - вам скорее всего нужно пересмотреть дизайн.

Stay safe. Stay cool.

#multitasking #cppcore #design
Как использовать RAII с сишным API

Все мы с вами используем сишный API. Базы данных, работа с сетью, криптография. Перечислять области можно долго. И все мы с вами немного недолюбливаем такой способ взаимодействия с сущностями. Оно и понятно. Не зря умные дяди придумывали объектно-ориентированное программирование и не зря мы, не такие умные дяди, стараемся этой методологии следовать. А тут нужны какие-то сырые указатели, байтовые буфферы и прочие вульгарности. Это не только неудобно, но может приводить к трудноотловимым ошибкам, недостаточной гарантии безопасности. Старшие ребята, естественно, знают, как правильно использовать плюсовые объекты в таких случаях, а вот молодняк может не знать этого или не осознавать подходов, которые они использовали. Поэтому поделюсь своей интерпретацией адекватного подхода, который поможет грамотно использовать RAII с сишным апи.

На помощь нам неожиданно приходят std::array и std::vector. Это простые RAII обертки над статическими и динамическими массивами, которые предлагают следующие фичи:

1️⃣ Автоматическое управление памятью. std::array в конструкторе аллоцирует память на стеке, std::vector - на куче. Их деструктор вызывается при выходе из скоупа.

2️⃣ Детерминированная инициализация. Инициализация этих контейнеров происходи в конструкторе, что предотвращает обращение к неинициализированной памяти.

3️⃣ Безопасный и удобный доступ к элементам с помощью методов .at(), .back(), .front() и итераторов.

4️⃣ Легкий доступ к буферу через метод .data().

Как их использовать для взаимодействия с С API? Гениально и просто.

👉🏿 Объявить нужный массив. Если размер структуры известен на момент компиляции - std::array, если нет - std::vector. Инициализировать его в конструкторе нужными значениями: дефолтовыми - в конструкторе, кастомными - через memcpy(array_ptr, struct_ptr, struct_size).

👉🏿 Передать в Сишный апи. Например так:
AES_cbc_encrypt(plaintext_array.data, ciphertext_array.size(), plaintext_array.size() ...);

👉🏿 Наслаждаться жизнью, ибо больше вам не нужно ни о чем беспокоиться.

Если уж вы передаете в С API более сложные структуры, чем массивы, то вам могут понадобиться методы сериализации и десериализации.

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

Делитесь своим опытом взаимодействия с API C и используйте modern C++ для более надежного и эффективного кода.

Stay cool.

#goodoldc #design #cppcore #STL
Вызов метода через указатель на метод

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

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

Суть в том, что есть специальный синтаксис для определения именно что указателя на метод. Делается это так:

using fnptr = void (RandomType::*)();

В этом объявлении синонима явно указывается, что это указатель на метод конкретного класса RandomType. И вызывается конкретный экземпляр этого метода через объект и оператор точка. И все. Никаких танцев с бабнами и прочих шалостей. В общем, все можете посмотреть в коде на картинке.

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

Stay versatile. Stay cool.

#fun #cppcore
Выражайте свои намерения в коде явно

Всегда, когда мы пишем код, мы должны помнить одну вещь. То, что мы написали, потом будут читать. Много много раз. Фактически, вы пишите книгу о том, что делается у вас проекте. Безусловно, эта книга не будет понятна 99.9% жителей планеты. Однако иногда такой код пишется, что 99.9% программистов он непонятен)

Есть много разных способов сделать свой код понятнее для коллег. Расскажу про один из таких способов.

Если назначение какого-либо кода не указано (например, в именах переменных или комментариях), трудно сказать, делает ли код то, что должен делать(да и в принципе что этот код делает). Например:

int i = 0;
while (i < v.size()) {
// ... a lot of operations with v[i]...
}
...
Что в этом куске кода не так? Он непонятный. Ну то есть тут все читаемо и мы понимаем механику, как какие-то операции делаются. Но мы не понимаем главного. Намерений. Смысла.

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

Вот возьмем код сверху. Мы понимаем, что есть какой-то цикл и есть переменная i, которая вроде как индекс и вот мы чет-то делаем до тех пор, пока не этот индекс не достигнет размера какого-то объекта. Непонятно, почему индекс объявлен вне цикла. Непонятно, будет ли он использоваться дальше и зачем. А человек всего лишь хотел обойти элементы массива и сделать с ними что-то. Почему бы не написать сразу вот так:

for (const auto& point : points_for_drawing) { DrawPoint(point);}

Это даже читать можно. Для каждой точки из набора точек для рисования нужно отрисовать точку. Причем сразу понятно, что DrawPoint не может изменить точку. Или так:

for (auto& point : points_for_drawing) { ReflectOx(point);}

Для каждой точки из набора точек для рисования нужно отразить ее относительно оси абсцисс. Это потребует изменения точек, поэтому ссылка неконстантная.
Здесь нет акцента на итерировании и способе прохождения по массиву. Я здесь выражаю намерение, а не способ достижения результата. ЧТО я хочу сделать, а не КАК.
Иногда можно использовать именованные алгоритмы для большей ясности повествования.

std::for_each (points_for_drawing.begin(), points_for_drawing.end(), ReflectOx(point));

Вариант не универсальный, но всегда нужно оценивать возможность этого подхода.

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

Stay clear. Stay cool.

#goodpractice
Удобное представление чисел

Всем привет! Хочу рассказать про небольшую подсказку, которая сделает ваш код понятнее как для себя, так и для других.

Часто замечаю, что когда пишется длинная численная константа, она выглядит как куча-мала и требует внимательного прочтения:
uint64_t speed = 299792458;
double size = 0.4315973;


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

При программировании, например, FPGA приходится использовать задокументированные численные константы к конкретной железке. Очень часто их пишут в двоичной или шестнадцатеричной СИ:
uint32_t SPI_hex_code = 0x374F0FB4;
uint16_t SPI_bin_code = 0b1000111110110100;



Начиная с C++14 появилась поддержка бинарных литералов, которые позволяют делать вот так:
// Binary literals since C++14

// Разделение на порядки
uint64_t speed = 299'792'458;
double size = 0.431'597'3;

// Разделение на октеты (байты)
uint32_t SPI_hex_code = 0x37'4F'0F'B4;
uint16_t SPI_bin_code = 0b10001111'10110100;


На мой взгляд, группировка на порядки или байты позволяет быстро и легко воспринимать код. Как говорится, разделяй и властвуй 😉

#cpp14 #goodpractice #fun
std::unique_ptr

Снова пост по запросам подписчиков. @load_balancer попросил пояснить за std::unique_ptr и в чем его преимущества по сравнению со связкой new/delete. Когда-то я сам не понимал, в чем прикол этих умных указателей. Но со временем осознал, насколько это маст-хэв в современной плюсовой разработке.

Как было раньше. Концепция умных указателей появилась довольно давно и даже до стандартных классов, все пользовались или кастомными вариантами, или бустовскими. Но без семантики перемещения все работало довольно костыльно(яркий пример std::auto_ptr). Потому не буду брать это в расчет. Представим, что давным-давно не было никаких умных указателей. В С++ есть 2(4 если учитывать варианты для массивов) оператора для работы с кучей. new и delete. Когда нам нужен объект, время жизни которого мы хотим полностью контролировать от и до, мы вызываем оператор new. В этот момент происходят 2 вещи: выделяется память на куче и на этой памяти создается объект с помощью конструктора. И нам возвращается типизированный указатель на объект. Далее мы этим объектом работаем, работаем. И когда он нам больше не нужен, мы его удаляем с помощью delete. Тут тоже происходят 2 вещи: вызывается деструктор объекта и память возвращается системе. Когда-то все так писали и ничего страшного. В языке С до сих пор оперируют сырыми указателями и память менеджится самостоятельно.

Но у такого подхода есть проблемы. Системы, которые мы строим, имеют намного большую сложность, чем средний программист может воспринять. Иногда мы вообще работаем только с одной частью этой системы и чхать хотели на остальные компоненты. Все еще сильно усложняется тем, что приложения обычно многопоточные и мыслить в парадигме многопоточности - дело нетривиальное даже для самых профиков. Всвязи с этим логичное следствие - мы не до конца понимает и не осознаем полный цикл жизни объектов. А из этого проистекают 2 проблемы: человек может просто не удалить объект и удалить этот объект повторно. Первая приводит к утечкам памяти и все увеличивающему потреблению памяти приложением, а вторая приводит просто к падению этого приложения. Обе вещи пренеприятнейшие и хотелось бы не сталкиваться с ними.

Что придумали взрослые толстые дяди. В начале появилась идиома RAII. У нас есть довольно подробный пост на эту тему тут. Благодаря пониманию, что мы можем перенести ответственность за удаление объекта на систему при вызове деструктора, и благодаря внедрению move-семантики, появились привычные нам умные указатели std::unique_ptr и std::shared_ptr.

Их общее преимущество, по сравнению со старым подходом new/delete - разработчик теперь не заботится об удалении объекта. Он определяет только момент создания объекта и знает условия, при которых тот или иной умный указатель освободит память.

Специфика конкретно unique_ptr - объект этого указателя реализует семантику владения ресурсом. То есть нет другого объекта, который может повлиять на время жизни объекта-хозяина. Объект-хозяин может только удалиться сам и освободить ресурс, и передать права на владение ресурсом другому объекту. Все! Удаление происходит в предсказуемом моменте времени - при выходе из скоупа. А передача прав происходит при создании другого объекта в его конструкторе перемещения с явным вызовом std::move, который делает программист руками, когда хочет передать владение.

Нет ни одной причины не использовать unique_ptr для управления объектами. Памяти он обычно занимает столько же, как и обычный указатель. Благодаря нему мы пишем код в объектно-ориентированном стиле. И не заботимся о менеджменте памяти. Недостатков не наблюдаю.

Надеюсь, я довольно подробно описал мотивацию использования smart pointers и их преимущества. Будут вопросы - в удовольствием поговорим в комментах.

Stay smart. Stay cool.

#cpp11 #memory #goodpractice
Перенос элементов между ассоциативными контейнерами

Пост для тех, кто много работает с древовидными структурами данных. Таких среди нас, думаю, немного. Но мне кажется фишка интересная. Слышали когда-нибудь об одной изящной функции, представленной в C++17, которая значительно облегчила нам жизнь при работе с ассоциативными контейнерами? Я говорю о методе extract, который как бы вырезает ноду из внутренней структуры контейнера и помещает его наружу, в специальный хэндлер node_type, не дергая конструкторы класса.

Что было до С++17

В эпоху до C++17 перемещение или перенос узлов между ассоциативными контейнерами было обременительной задачей. Представьте, что у вас есть узел в одном std::map или std::set, и вы хотите переместить его в другой контейнер. Вам приходилось вручную выполнять операцию копирования/перемещения объекта во временный объект, удаления этой ноды из одной мапы и вставки в другую копированием/перемещением . Это было не очень красиво эстетически, нельзя было трансферрить объекты, у которых не было конструкторов копирования и перемещения. Ну и конечно были совершенно лишние вызовы конструкторов.

Splicing в C++17

С появлением C++17 нам была предоставлена возможность для так называемого «сращивания». Вот как это работает:

👉🏿std::map::extract() и std::set::extract()

Эти новые функции-члены позволяют вам извлекать узел из std::map или std::set и возвращать его как «извлеченный» узел типа node_type. Эта операция эффективно отвязывает узел от исходного контейнера и внутри этого узла объект остаётся нетронутым. Никаких вызовов конструкторов объекта!

👉🏿std::map::insert() и std::set::insert()

Имея извлеченный узел на руках, вы теперь можете легко вставить его в другой std::map или std::set, используя метод insert(). Перенесенный узел будет легко интегрирован в новый контейнер без необходимости вовлечения конструкторов.

И все! Такой приём можно провернуть с любыми видами упорядоченных и неупорядоченных ассоциативных контейнеров. И да, это все ещё может вызывать перебалансировку дерева. Куда же без этого.

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

Короче говоря, C++17 в целом значительно облегчил нам жизнь. Эта фича не геймченджер, но очень приятное нововведение, которое позитивно сказалось на качестве кода и его производительности.

Stay updated. Stay cool.

#cpp17 #datastructures #STL
Грокаем C++ pinned «Приветственный пост Рады приветствовать всех на нашем канале! Вы устали от скучного, монотонного, обезличенного контента по плюсам? Тогда мы идем к вам! Здесь не будет бесполезных 30 IQ постов, сгенеренных ChatGPT, накрученных подписчиков и активности.…»
Можно ли явно вызывать деструктор объекта?

Вопрос скорее из категории "А что если?" и скорее всего большинство разработчиков не столкнуться с надобностью это делать. Однако менее интересным этот вопрос не становится, если немного глубже копнуть. А копаться в плюсах всегда интересно)

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

Возьмем простую структурку

struct SomeDefaultDestructedType
{
SomeDefaultDestructedType(int b): a{b} {}
~SomeDefaultDestructedType() = default;
int getNumber() {return a;}
int a;
};

Как видим, у нее дефолтовый деструктор, то есть отдает генерацию кода для него в руки компилятора. А компилятор в этих вопросах тупой и прямолинейный. Для нетривиальных полей он вызывает деструкторы, а для тривиальных типов - ничего не делает. И все. Поэтому для нашего класса вызывали мы деструктор для объекта, не вызывали, значения не имеет. Даже можно вызвать метод getNumber после явного вызова деструктора и он вернет валидное значение. Семантически объект уничтожен, а на самом деле живее всех живых. Память для него на стеке есть, данные в этой области лежат, поэтому мы и можем оперировать им как полноценным объектом. И хорошо, если у класса все его поля будут рекурсивно default deconstructed, тогда мы ничего не сломает. Проблемы начинаются, когда класс имеет нетривиальный деструктор. Посмотрим на следующую структурку:

struct SomeNonTrivialDestructedType
{
SomeNonTrivialDestructedType(int b): a{new int(b)} {}
~SomeNonTrivialDestructedType() { delete a;}
int getNumber() {return *a;}
int * a;
};

Простенькое выделение и освобождение памяти в куче. Что будет сейчас при явном вызове деструктора? Ресурс освободится, а объект останется лежать на стеке. А что происходит с объектами, которые выделяются в автоматической области, при выходе из области их создания? У них вызывается деструктор. Поэтому будет двойное освобождение памяти. А если этот объект еще использовать как-то, например вызвать у него метод getNumber, это еще и неопределенное поведение, так как обращаемся к освобожденной памяти.

Для объектов, созданных в куче через new, проблема остается такой же. Потому что хорошей практикой разработки является вызывать delete на каждый выделенный с помощью new объект. А delete вызывает деструктор.

Да и вообще для любого класса с нетривиальным деструктором, явный вызов этого самого деструктора будет сопровождаться приведением объекта в неконсистентное состояние, когда объект семантически мертв, а на деле им можно еще пользоваться и его придется умертвить еще раз(самостоятельно через delete или автоматически при выходе из скоупа). Это может приводить к ошибкам самого широкого спектра, в зависимости от логики работы объекта.

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

Stay safe. Stay cool.

#cppcore #fun #memory
Всем привет!

Мы проставили хэштеги на каждый пост
А также на канале появился закрепленный пост, где собраны все эти хэштеги
Поэтому теперь вы можете:

1️⃣ В один клик перейти в самое начало канала и читать подряд все посты, не тратя время на прокручивание ленты

2️⃣ Быстро искать посты по интересующим тематикам, просто жмакнув на нужный хэштег

Давно эта штука напрашивалась и вот она появилась!
Уверен, что ваш юзер экспириенс качественно улучшится с этим нововведением)

Всем замечательного дня!

Stay cool.
Порядок вычислений в С++

Рассмотрим простой вызов функции:

foo(std::unique_ptr{new A}, std::unique_ptr{new B});

Что может пойти здесь не так?

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

Что было до С++17?

Был бардак, одним словом. Нормальный человек подумает, что в начале вычисляем new A, потом конструктор unique_ptr, потом new B и второй конструктор. Но это же С++. Компилятор на самом деле мог сгенерировать код, который вычисляет это выражение в любом рандомном порядке. Именно поэтому существует эта известная проблема, что компилятор так переупорядочит вызовы, что первым будет вызов new A, а вторым будет вызов new B и дальше конструкторы. Прикол в том, что если new B бросит std::bad_alloc, то мы получим утечку памяти. Успешно отработавший new выдаст указатель на память, которая никогда не будет возвращена обратно системе. Это мог бы сделать unique_ptr, но его конструктор так и не был вызван.

Да и вообще там много приколов неприятных было. Самые знаменитые примеры:

i = i++ + i++;

std::string s = "but I have heard it works even if you don't believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don't"), 6, "");

В обоих примерах компилятор мог так переставить операции, что в первом случае i будет равно нулю, а во втором случае будет совсем каша.

Порядок вычислений неспецифицирован. Конечно, там много правил под капотом и все-таки какие-то гарантии там есть, но объяснение их всех явно влезет в этот пост.

Именно поэтому std::make_unique и std::make_shared были незаменимы до 17-х плюсов. Потому что содержали внутри себя аллокацию памяти и давали базовую гарантию безопасности исключений.

Что стало после С++17?

Порядок вычислений стал более понятен. Там ввели кучу ужесточающих правил, не буду их всех приводить, потому что они скучные и скорее всего вы с ними не пересечетесь в своей работе. Главное, что в самом первом примере с функций foo и умными указателями теперь не сможет случиться утечка памяти. Новый закон таков, что не указано, в каком порядке должны быть вычислены аргументы функций, но каждый из них должен быть полностью вычислен до того, как начнет вычисляться следующий. Если второй new бросит bad_alloc, то первый указатель уже будет обернут в unique_ptr и при разворачивании стека будет вызван его деструктор и память освободится.

Тема на самом деле не простая. Если кто хочет поподробнее ее разобрать, то можете почитать тут и тут.

Stay safe. Stay cool.

#cpp17 #memory
Remove-erase идиома

Удаление элементов из вектора - вещь не совсем тривиальная. Безусловно, в STL есть интерфейс для этого, бери да вызывай. Проблема в том, что удаление вызывает копирование всех элементов вектора, которые находятся правее от удаляемого элемента. То есть, в среднем, сложность удаления одного элемента контейнера будет линейной. А сложность очистки всего контейнера - квадратичная. Это все очень печальные результаты. И хоть для последовательности упорядоченных элементов нет стандартного алгоритма удаления из середины, для вектора, в котором порядок неважен, такой алгоритм есть.

Нам нужно лишь свопнуть удаляемый элемент с последним элементом в векторе и очистить последнюю с конца ячейку. Таким образом, удаление будет занимать всего 2 действия, а значит сложность этой операции - константная. Звучит намного лучше.

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

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

И такое есть. В плюсах это называется remove-erase idiom. Идиома удаления-очистки. Какой у меня прекрасный английский)
Решение хоть и стандартное, но не прям очень элегантное. Ну а что вы хотели? Это плюсы все-таки.
Судя по названию, надо просто скомбинировать стандартный алгоритм std::remove (или std::remove_if) и метод вектора std::vector::erase.
На картинке снизу можете увидеть, как конкретно эта комбинация решает задачу удаления элементов.

Stay cool.

#algorithms #STL #datastructures
Квантовая суперпозиция bool переменных

Наверно у многих в головах давно лежит прямая и четкая ассоциация, что тип данных bool всегда принимает значение true или false. Спешу развеять ваши убеждения!

На первый взгляд кажется, что такая ветка условия никогда не может быть выполнена:
bool condition;

if (condition != true && condition != false)
{
// Недостижимый код?
}

Но, неожиданно и к сожалению, у менять есть вот такой пример: https://compiler-explorer.com/z/jf7zE64eq

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

Небольшой экскурс в историю. Раньше в языке C такого типа как bool не существовало в принципе. Вместо него использовались целочисленные переменные, такие как int. Неявное приведение происходит по правилу: 0 -> false, иначе true. Приведу пример:
if ( 0) // false
if ( 1) // true
if ( 2) // true
if (-1) // true


Но как мы знаем, С++ во многом совместимым с С. Следовательно, он перенимает некоторые особенности своего прародителя, поэтому логическая переменная может скрывать под собой абсолютно любое целочисленное значение! И напротив, логические константы true и false однозначно определены, как 1 и 0 соответственно.

Получается, что на самом деле мы работаем с этим:
int condition; // Неинициализированное значение

if (condition != 1 && condition != 0)
{
// Вполне себе достимый код
}


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

Чисто теоретически можно создать другие, очень специфичные условия. Приведу другой пример, но напоминаю -- это UB: https://compiler-explorer.com/z/fn6YPvnzP

Да, просто под капотом сравнивается 10 != 1 - и никакой магии. Но увидеть это порой столь же неожиданно.

У вас могут появиться вопросы, зачем нам может понадобиться такое знание? Это ведь, фактически, UB, которое надо постараться воспроизвести!

Приведу практический пример из моей опыта. Я написал этот код несколько лет назад. Мне хотелось избежать лишних условных ветвлений в коде и написать что-то типа такого:
bool condition = ???; 
int position = index + static_cast<int>(condition);


А-ля, если логическая переменная condition == true (типа оно равно 1), значит index + 1, иначе index + 0. Так вот на самом деле нельзя с уверенностью сказать, какое целочисленное значение лежит под булем.

Пока что этот код не выстрелил :) Но я вижу его проблему... Так что перепроверяйте некоторые очевидные убеждения и пишите безопасный код!

#hardcore #cppcore #goodoldc
Всем привет!

Сегодня знаменательный день для нас всех(ну для админов точно) - мы достигли первой сотни подписчиков!

Один из методов продвижения, что мы используем - комментинг на других каналах. И после того, как я вчера в ночи оставил коммент на одном из каналов по плюсам, мне приснилось, что число подписчиков на этом канале стало 107. Проснувшись, я подумал, что слишком влажно мечтаю и что не нужно рассчитывать на такие цифры. Но удивлению не было предела, когда я увидел число 105. Это практически 20 живых людей за день, которым понравился контент и они решили подписаться, чтобы узнавать что-то новое и быть частью крутого сообщества. Жалко, что я так и не увидел круглую цифру, но это не так важно.

Спасибо всем-всем подписчикам за доверие!

То, что мы пока маленькие - ничего не значит. Это все временно. А статистика по охватам говорит, что вам реально нравится и вы вовлечены в то, что мы публикуем. Это самая главная метрика. Подписчикам должно быть интересно читать канал. И мы будем и дальше работать над этим.

Но и вы тоже можете сделать канал ещё более живым и интересным. Пишите комментарии. Любые, по сабжу и не по нему, «глупые» и «умные» вопросы, идеи для постов и тд. Комментариями вы можете помочь подснежникам втянуться в эту самую айтишечку, прояснить для себя вопрос, который долго мучал, потешить своё эго, что по вашему запросу написали пост, или ткнуть носом админа в его ошибку. Это будет полезно всему сообществу.

В любом случае, дальше - больше.

Всё, не буду отвлекать вас от важных государственных предновогодних дел.

Всем прекрасных выходных!!

Stay cool.
std::span

Все мы любим С++ за способность работать как на высоком уровне, так и на низком уровне абстракции. Ну ладно, за это не любим. За это ценим. Довольно много проблем зашито в самом языке из-за этой особенности, однако плюсы перевешивают(какой каламбур получился неожиданный). Одна из таких проблем - поддержка высокоуровневых контейнеров и низкоуровневых сишных массивов. Проектируя обобщенный код, нам нужно учитывать, что пользователь хотел бы оперировать и контейнерами, и массивами, при работе с этим кодом. Однако это не всегда удается сделать красиво, да и комон, какие сишные массивы? 2к23 наконец. Хочется писать в терминах С++, объектно-ориентированной модели и все такое. Однако с недавних пор у нас появился инструмент, который может нам помочь.
Я говорю в std::span.

template<class T,  std::size_t Extent = std::dynamic_extent>
class span;


Этот шаблонный класс, который введен в С++20. Он описывает объекты, являющиеся ссылкой на непрерывную последовательность объектов. В чем фишка этой фичи?

1️⃣ Первое. Она позволяет единообразно работать с динамическими контейнерами и со статическими массивами. Для статических последовательностей в конструктор передается указатель на первый элемент и количество элементов в массиве. Тогда типичная реализация будет содержать только указатель на первый элемент последовательности, и количество элементов будет содержаться в самом типе. А если передать в конструктор контейнер, то объект будет содержать 2 поля - указатель и размер последовательности.

2️⃣ Второе. Это по сути вьюха на существующий контейнер или массив, которая позволяет работать с ними целиком и даже с подпоследовательностями без передачи владения и главное без копирования. Мы можем проектировать свои апи так, чтобы пользователь был уверен, что ничего плохого не случится с его массивом в функции. Это повышает безопасность кода. А отсутствие копирования открывает нам двери к адекватным легковесным слайсам в с++. Метод subspan предоставляет интерфейс слайсинга. Теперь для задания отрезка массива необходимы просто 2 числа. Как во всех нормальных языках.

Фича на самом деле рабочая. Даже в кор гайдлайнах код написан с использованием std::span. Там не советуют его использовать. А просто используют его в листингах. Как бы предполагая, что все про это знают и это стандартный способ написания кода. А это уже о многом говорит.

Stay updated. Stay cool.

#cpp20 #STL
std::make_unique

В комментах под этим постом, @Zolderix предложил рассказать про плюсы-минусы использования std::make_unique и std::make_shared. Темы клевые, да и умные указатели, судя по всему, вам заходят. Но будем делать все по порядку и поэтому сегодня говорим про std::make_unique.

Нет ни одной ситуации, где я бы предпочел создать объект через new вместо того, чтобы воспользоваться какой-нибудь RAII оберткой, будь то smart pointer или, например, std::array. Бывает апи говно и по-другому просто нельзя. Но чтобы намеренно делать это - неа. Но и даже при работе с умными указателями, их можно создать с помощью сырого поинтера, возвращенного new. Нужно ли так делать или лучше воспользоваться специальными функциями?

Мне кажется, что в целом идея умных указателей - снять с разработчиков ответственность за работу с памятью(потому что они ее не вывозят) и семантически разграничить разные по предназначению виды указателей. И, как мне кажется, функции std:make_... делают большой вклад именно в полной снятии ответственности. Я большой фанат отказа от явного вызова new и delete. Со вторым умные указатели и сами хорошо справляются, а вот с первым сильно помогают их функции-фабрики. Программист в идеале должен один раз сказать: "создать объект", объект создастся и программист просто забудет о том, что за этим объектом надо следить. Уверен, что большую часть компонентов систем можно и нужно строить без упоминания операторов new и delete вообще. И если с delete все и так ясно, то ограничение использования new может привести к улучшению безопасности и читаемости кода.

Это было особенно актуально до С++17, когда гарантии для порядка вычисления выражений были довольно слабые. Использование new, даже в комбинации с умным указателем, в качестве аргумента функции могло привести к утечкам памяти. Об этом более подробно я рассказывал в этом посте. А введение std::make_unique в С++14 полностью решило это проблему! Эта функция дает базовую гарантию безопасности исключений и, даже в случае их появлений, никакие ресурсы не утекут. Уверен, что какие-то проекты до сих не апнулись до 17 версии по разным причинам, поэтому для них это будет особенно актуально. Но гарантии исключений std::make_unique остаются прежними для всех существующих версий плюсов. Поэтому, кажется, что сердцу будет все равно спокойнее при ее использовании. У меня каждый раз повышается алертность, когда я вижу new. А какой цикл жизни у объекта? А что с исключениями? Оно того не стоит.

Также std::make_unique улучшает читаемость кода. И на этом есть 2 причины.
Первая - она лучше выражает намерение. На канале мы много об этом говорим. Эта функция доносит в понятной человеку языковой форме, что сейчас идет создание объекта. Я считаю использование фабрик - хорошей идеей именно поэтому. Хотя ничего и не меняется, и в конструктор и фабрику мы передаем одни и те же аргументы. Но вот это человеческое сообщение "make" "create" воспринимается в несколько раз лучше, чем просто имя класса.
Вторая - вы избегаете повторения кода. Чтобы создать unique_ptr через new нужно написать что-то такое:

std::unique_ptr<VeryLongAndClearNameLikeItShouldBeType> ptr{ new VeryLongAndClearNameLikeItShouldBeType(...) };

И сравните во с этим:

auto ptr = std::make_unique<VeryLongAndClearNameLikeItShouldBeType>(...);

В полтора раза короче и намного приятнее на вид.

Еще std::make_unique разделят типы Т и Т[]. Здесь вы обязаны явно специфицировать шаблон с подходящим типом, иначе вы просто создадите не массив, а объект. Функция сделает так, чтобы при выходе из скоупа обязательно вызовется подходящий оператор delete или delete[]. А вот если работать с непосредственно с конструктором std::unique_ptr, то вот такая строчка

std::unique_ptr<int> ptr(new int[5]);

хоть и компилируется, но приводит к UB.

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

Stay in touch. Stay cool.

#cpp14 #cpp17 #STL #memory #goodpractice
std::make_unique. Part 2

Вчера мы поговорили о том, почему вам стоит всегда использовать std::make_unique вместо std::unique_ptr(new ...). Однако может вы и убедились, что фича крутая и ей надо пользоваться всегда, но, как бы я этого не хотел, это не всегда возможно. То, что фича крутая - это беспортно! Просто в некоторых ситуациях вы не сможете ее применить. Поэтому сегодня рассмотрим эти ограничения. Ситуации значит такие:

1️⃣ Вам нужен кастомный делитер. Например, для логирования. Или для закрытия файла, если в умный указатель вы положили файл. Делитер нужно передавать, как параметр шаблона класса, а std::make_unique не умеет принимать второй параметр шаблона. Поэтому вы просто не сможете с ее помощью создать объект с кастомным удалителем. Скорее всего такой дизайн функции был продиктован простотой ее использования и следованием более понятной модели владения и инкапсуляции ресурсов. Когда ответственность за владение и удаление ресурсов ложится целиком на класс указателя.

2️⃣ Если у вас уже есть сырой указатель и вы хотите сделать из него смарт поинтер. Дело в том, что std::make_unique делает perfect-forwarding своих аргументов в аргументы конструктора целевого объекта. И получается, что передавая в функцию Type *, вы говорите - создай новый объект на основе Type *. И в большинстве ситуаций это не то, что вы хотите. У вас уже есть существующий объект и вам хочется именно его обезопасить. С make_unique такого не получится.

3️⃣ Если у вашего класса конструктор объявлен как private или protected. По идее, make_unique - внешний код для вашего класса. И если вы не хотите разрешать внешнему коду создавать объекты какого-то класса, то нужно быть готовым, что объекты такого класса нельзя будет создать через std::make_unique. В этом случае придется пользоваться конструкцией std::unique_ptr(new Type(...)). Этот пункт довольно болезненный в проектах, где у многих классов есть фабричные методы.

4️⃣ std::make_unique плохо работает с initializer_list. Например, вы не сможете скомпилировать такой код:

make_unique<TypeWithMapInitialization>({})

мы бы хотели создать объект с пустой мапой, но не можем этого сделать вот таким элегантным образом. Придется делать вот так:

make_unique<TypeWithMapInitialization>(std::map<std::string, std::map<std::string, std::string>>({}))

или придется использовать new для простоты:

unique_ptr<TypeWithDeepMap>(new TypeWithDeepMap({}))

5️⃣ И наконец, не ограничение, а скорее отличие make_unique<Type>() от unique_ptr<Type>(new Type()). Первое выражение выполняет так называемую default initialization, а второе - value initialization. Это довольно сложнопонимаемые явления, может как-нибудь отдельный пост на это запипю. Но просто для базового понимания, например, int x; - default initialization, в х будет лежать мусор. А int x{}; - value initialization и в х будет лежать 0. Повторюсь, не все так просто. Но такое отличие есть и его надо иметь ввиду при выборе нужного выражения, чтобы получить ожидаемое поведение.

Закончить я хочу так. Как часто вам нужны кастомные делитеры, приватные конструкторы? Как часто нужно передавать список инициализации в конструктор или создавать пустые объекты? Думаю, что таких кейсов явно немного. А, если и много, то поспрашивайте у коллег, мне кажется, что у них не так)
Поэтому всем рекомендую пользоваться std::make_unique, несмотря на все эти редкие и мелкие ограничения.

Stay unique. Stay cool.

#cpp14 #cpp17 #STL #memory #goodpractice
Идиома NVI

В публикации под названием "C++ Coding Standards: 101 Rules, Guidelines, and Best Practices" есть такая строчка: Consider making virtual functions nonpublic, and public functions nonvirtual. По-русски - рассмотрите возможности сделать виртуальные функции непубличными, а публичные функции - невиртуальными. Для кого-то сейчас это предложение звучит, как минимум, странно. "Я ж всегда определял полиморфный интерфейс классов публичным. И зачем вообще делать виртуальные методы непубличными? " Уверяю вас, что эту строчку написали вполне уважаемые в международном коммьюнити люди - Андрей Александреску и Герб Саттер. Фигню они вряд ли скажут, поэтому сейчас постараюсь объяснить, какие идеи лежат за этими словами.

Иногда хорошие практики организации кода оформляют в виде идиом. И для выражения выше появилась такая идиома(хотя не знаю, что появилось раньше: курица или яйцоидиома или эта цитата). Называется она non-virtual interface idiom. Говорящее название)

Мотивация

Представьте себе какую-нибудь обычную иерархию классов. Очень часто полиморфные методы классов в ней выполняют какую-то общую работу. Это может быть обработка ошибок, логирование, сбор мониторингов, вызов какого-то общего метода, захват общего мьютекса, вы работаете с данными на низком уровне, а потом запаковываете в одинаковый формат и выдаете наружу и тд. Перечислять можно реально долго. Я бы даже так сказал: полифорфные методы с развитием проекта практически обречены обретать общую функциональность. То есть даже если ее не было, она с большой вероятностью появится. К чему это будет приводить? Желание изменить один аспект поведения объектов приводит к изменению кода по всей иерархии классов. Это увеличивает сложность внедрения новой или обслуживающей функциональности. А там еще и копипаст ошибки могут подьехать. Можно забыть в каком-то наследнике добавить то, что нужно. В общем беда.

А если нужно подкорректировать API без особого изменения кор-функциональности? Тогда опять придется править по всей иерархии.
А если надо подкорректировать кор-функциональнальность, без изменения апи и общей функциональности? Тоже самое.

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

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

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

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

Следование идиоме действительно дает гибкость в проектировании классов и интерфейсов. Однако эта гибкость идет вместе с ответственностью за возросшую сложность системы. Теперь вам нужно поддерживать 2 интерфейса: внутренний и внешний. Если не очень аккуратно проектировать базовый класс, то не так уж и сложно нарваться на проблемы.

Так что
Stay well-designed. Stay cool.

#design #goodpractice
std::make_shared

Недавно тут и тут мы поговорили про плюсы и минусы использования std::make_unique. Настала очередь его братишки std::make_shared.

Базового все pros and cons с предыдущих постов справедливы и для сегодняшнего разбора. Поэтому не будем на этом долго останавливаться.

Но шаренный указатель немного сложнее внутри устроен, чем уникальный. От этого идут и уникальные преимущества и недостатки. А связаны они вот с чем. Посмотрите на эту строчку:

std::shared_ptr<T>(new T(...));

Сколько раз память аллоцируется в результате выполнения этой строчки?

Многие скажут 1. А люди, знающие внутреннее устройство шареного уккзателя, скажут 2. И будут правы.

Первая аллокация, очевидно, происходит в new. А вот где вторая?
На выделении памяти для, так называемого, control block'а. Это внутренняя структура, которая хранит счетчики ссылок и еще пару приколюх. Она нужна для того, чтобы вести учет существующих объектов указателя, указывающих на данный объект. Естественно, эта структура должна быть общей для всех таких объектов. Поэтому в каждом объекте указателя хранится сырой указатель на этот самый контрол блок. То есть базово в классе std::shared_ptr 2 поля: указатель на объект и указатель на контрол блок. Ну и приняв указатель на объект, конструктор указателя дополнитель выделяет память для этого блока.

Чем в этом контексте отличается поведение std::make_shared?

Она вызывает всего одну аллокацию. Как? выделяет просто один блок памяти, который может содержать сразу и создаваемый объект, и control block, и кладет эти данные вместе. Это уменьшает статический размер программы, потому что код содержит всего 1 вызов аллокатора. И увеличивает скорость выполнения кода, потому что аллокация - довольно дорогостоящий вызов.

Перформанс - это уже серьезный аргумент отдать свое предпочтение в пользу make функции.

Однако эта фича ведет к одной проблеме. Для кого-то она совсем не проблемная, но об этом надо знать.

Дело в том, что может создаться такая ситуация, когда ни одного shared_pointer уже не существует, а память, выделенная для объекта и блока, все еще не отдана системе. Как такое может быть? Слабые ссылки.

Контрол блок помимо счетчика сильных ссылок(собственно сами shared_ptr'ы) хранит еще и счетчик слабых ссылок - для weak_ptr'ов. А деструктор control block'а и деаллокация памяти происходят только после того, как оба счетчика зануляться. Поэтому, если у вас есть хоть один висящий std::weak_ptr, то у вашего объекта хоть и будет вызван деструктор, но память так и не будет возвращена системе.

При создании больших объектов и при обильном использовании слабых ссылок это действительно может создавать проблему.

А если у вас не этот случай - смело используйте std::make_shared()

Stay efficient. Stay cool.

#cpp17 #cpp17 #STL #optimization #memory