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

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

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

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

Тогда мы идем к вам!

Здесь не будет бесполезных 30 IQ постов, сгенеренных ChatGPT, накрученных подписчиков и активности.

Канал ведут два сеньора, Денис и Владимир, которые искренне хотят делится своими знаниями по С++ и создать самое уютное коммьюнити позитивных прогеров в телеге!
(ну вы поняли, да? с++, плюс плюс, плюс типа
позитивный?.. ай ладно)

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

ГАЙДЫ:

Мини-гайд по собеседования
Гайд по категория выражения и мув-семантике
Гайд по inline

Дальше пойдет список хэштегов, которыми вы можете пользоваться для более удобной навигации по каналу и для быстрого поиска группы постов по интересующей теме:
#algorithms
#datastructures
#cppcore
#stl
#goodoldc
#cpp11
#cpp14
#cpp17
#cpp20
#commercial
#net
#database
#hardcore
#memory
#goodpractice
#howitworks
#NONSTANDARD
#interview
#digest
#OS
#tools
#optimization
#performance
#fun
#compiler
#multitasking
#design
#exception
#guide
#задачки
#base
#quiz
#concurrency
Указатель на void

Тот случай, когда тема сишная, но знать это надо и это довольно часто встречается в плюсовых проектах.
Естественно, это все про сишные интерфейсы и C-style функции. Мы все с этим сталкивается, ничего не поделать.

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

Дело в том, что любой указатель - просто 8-мибайтное число, указывающее на какую-то точку в памяти. Но объекты и структуры - это не точки. Это отрезки. От и до. Поэтому компиллятору надо знать размер области памяти, чтобы достать оттуда информацию. Отсюда и правило, что разыменовывать void* нельзя. Отсюда и второе правило, что адресная арифметика неприменима к void*. Поэтому нужны типизированные указатели. Любой определенный тип имеет вполне конкретный размер, известный компилятору. И когда мы храним указатель на этот тип, мы говорим условному gcc "тут лежит объект и, чтобы откопать его, копай на 4 байта вправо от указателя".

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

Первый - дженерик функции, работающие с любым типом. Главное знать размер этого типа. Типичные примеры: стандартный memcpy или какой-нибудь кастомный bytesToHexString

Второй - передача параметров callback функциям, когда типы параметров неизвестны вызывающему коду. Для примера, посмотрите функцию qsort. Она принимает компаратор с двумя параметрами - void*. Это необходимо, так как сама qsort не знает, с какими данными она работает и передает их в callback как есть, в виде void*.

Stay cool.

#cppcore #goodoldc #memory
Копаемся в маллоке

Короче. Попробуем новый формат на канале - статьи. Вот пожалуйста ссылочка https://telegra.ph/Nahodim-razmer-bloka-malloc-11-30. Это продолжение песни, начатой здесь. Инфы там многовато, поэтому пришлось в немного более длинном формате ее упаковать.

Было прикольно немного поисследовать вопрос, лично я кайфанул во время писания статьи.

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

You are the best.

#fun #howitworks #memory #goodoldc #hardcore
Variable length array

У большинства разработчиков есть стереотип, что С++ - это надмножество Си. Плюсовики же знают, что это не так, но зачастую на вопрос о различиях ничего ответить не могут. Так и что же есть такого в Си, чего нет в С++? Сделаю оговорку, что сейчас речь пойдет только о стандартах языков. Так как любой кастомный крестовый компилятор может поддерживать те или иные фичи языка Си. Это называется расширения компилятора. Мы всё-таки говорим о стандарте.

Сегодня мы рассмотрим только один из примеров. Механизм называется VLA или Variable Length Array. Или массивы переменной длины. В сущности он позволяет создавать массивы, размер которых не известен на момент компиляции, а память под них выделяется в автоматической области, то есть на стеке. Синтаксис ничем не отличается от статических массивов.

int n = 10;
int array[n];

Во всех учебниках по С++ написано, что создание динамических массивов на стеке запрещено и код выше запрещен стандартом (у значение переменной n нет квалификатора const). Однако в Си это часть стандарта, начиная с С99.

Фича довольно полезная в контексте простоты написания кода, не нужно городить дополнительных конструкций с выделением динамической памяти. Да и само выделение на стеке быстрее и операции с его памятью тоже происходят ощутимо быстрее. Однако всегда есть опасность выделить слишком много памяти и словить переполнение. Из-за этого о фиче мнение неоднозначно. В самом сишном стандарте то ограничивают ее, то вновь вводят поддержку в С23. А в один момент времени она даже была в драфте плюсового стандарта 14 года. Но на момент релиза ее убрали оттуда. Из-за этого кстати в некоторых компиляторах, например гцц, есть поддержка VLA. И код выше там скомпиляруется. Как-то я и сам неосознанно ею пользовался для написания небольшой библиотечки. А потом мне на ревью сказали, что вместо динамических массивов на стеке в плюсах принято пользоваться вектором. Так бы и не узнал, что использую запрещенку.

Но то, что в gcc есть поддержка vla, не значит, что она реализована так, как это предполагается по сишному стандарту. vla - лишь одна из граней variable length types. И в контексте этого понятия поведение кода, написанном на чистом С и на плюсах под гцц, будет разным. Не будем углубляться в детали. Просто надо понимать, что в данном случае лучше не использовать это расширение gcc, да и в принципе стараться придерживаться стандарта.

Stay cool.

#goodoldc
Как использовать 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
Квантовая суперпозиция 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
Сравниваем циклы

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

Берем довольно солидный массивчик на 1кк элементов и в циклах будем просто инкрементировать его элементы. Если вам 1кк кажется небольшим числом и результаты будут неточными, вы правы. Результаты в любом случае будут неточные, потому что моя тачка не заточена под перформанс тестирование. Однако флуктуации можно убрать: надо запустить изменение времени много-много раз и затем усреднить результаты. При достаточном количестве запусков, результатам можно будет верить. Я выбрал его равным 100 000. Просто имперически. Не хотел ждать больше 10 мин выполнения кода)

Дальше дело техники. Шаблонная функция для измерения времени, по циклу на сбор статистики и вывод результатов.

Шо по цифрам. В целом, все довольно ожидаемо. С++-like циклы прикурили у старых-добрых Си-style циклов. За комфорт, лаконичность и объектно-ориентированность приходится платить самым дорогим, что у программиста есть - процессорными клоками. Однако не совсем ожидаемо, что разница будет ~50%. Эт довольно много, поэтому все мы на заметочку себе взяли.

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

Ну и соболезнования для for_each. Им как бы и так никто не пользуется, еще и здесь унизили.

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

Measure your performance. Stay cool.

#performance #fun #goodoldc
Оптимизации компилятора

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

Компьютер - очень сложная штука. Людей, которые реально понимают, что происходит внутри него, и, исходя из этого, знают, как писать эффективные программы - ну если не по пальцам пересчитать, то их числа явно недостаточно, чтобы закрыть мировой спрос на программистов. Умные дяди думали-думали над этой проблемой и придумали одно решение. "А напишем-ка мы программу, которая будет знать, что происходит внутри машины, позволит людям писать удобный код и, на основе своих знаний, поможет им этот код ускорить!" Это и есть компилятор. Отсюда еще одной задачей компилятора является изменение наивного пользовательского кода так, чтобы его функционал не изменился, а время работы скоратилось. Причем делать такие изменения только по запросу программиста. Так появились оптимизации компилятора и соотвествующие опции, включающие их.

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

Но я не дал компилятору проявить себя во всей красе. Умные люди в комментариях сразу указали на эту проблему. Код компилировался без оптимизаций. И тот пост был подводкой к теме оптимизаций компилятора и как они могут аффектить наш код. Просто так рассказать про это было бы не очень интересно. А так чуть ли не скандал разразился и вы сильнее вовлеклись в тему😆. А я не устаю убеждаться, что в нашем коммьюнити много крутых и внимательных специалистов с критическим мышлением)

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

Существует дохренальен флагов оптимизации, но сегодня мы обратим внимание на группу флагов с префиксом -О. -О0, -О1, -О2, -О3. Это такие удобные верхнеуровневые рычажки, дергая которые вы включаете целый набор оптимизаций. Пока не будем углубляться из чего он состоит. Важно знать, что -О0 - дефолтный флаг(нет оптимизаций), и что чем больше чиселка при букве О, тем больше компилятор изменяет ваш код, чтобы он работал быстрее. Не факт, что у него получится что-то ускорить, но в среднем выигрыш будет. Какого характера может быть выигрыш?

Продолжение в комментах

#optimization #compiler #goodoldc #performance
std::signbit

В прошлом посте мы уже упоминали std::signbit. Сегодня мы посмотрим на эту сущность по-подробнее.

По сути, это самый говорящий и плюсовый чтоли способ узнать знаковый бит числа, который появился в нашем арсенале с приходом С++11. Причем не только целого, но и числа с плавающей точкой. Хотя на самом деле даже наоборот.

bool signbit( float num );  
bool signbit( double num );
bool signbit( long double num );


вот такие перегрузки мы имеем для floating-point чисел. А вот такую:

template< class Integer >  
bool signbit( Integer num );


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

В чем особенность целочисленной перегрузки. В том, что число, которое туда попадает трактуется, как double. Поэтому выражение std::signbit(num) эквивалентно std::signbit(static_cast<double>(num)).

Также эта функция детектирует наличие знакового бита у нулей, бесконечностей и NaN'ов. Да, да. У нуля есть знак. Так что 0.0 и -0.0 - не одно и то же. И если вы внимательные, то заметили даже у NaN есть знак. И std::signbit - один из двух возможных кроссфплатформенных способов узнать знак NaN. Этот факт еще больше мотивирует использовать эту функцию(в ситуациях, где это свойство решает).

Начиная с 23 стандарта функция становится constexpr, что не может не радовать любителей compile-time вычислений.

Для языка С тоже кстати есть похожая сущность. Только там это макрос

#define signbit( arg ) /* implementation defined */


И для него гарантируется такое поведение: для положительных чисел возвращаем ноль, а для отрицательных - ненулевое целое число.

Мне кажется, что в повседневной разработке(там где не нужно выжимать все возможные такты и кода) плюсовое решение будет более предпочтительным, по сравнению с аналогами. Говорящее название и поддержка стандрата - наши главные друзья.

Look for signs in life. Stay cool.

#cpp23 #cpp11 #goodoldc
Еще одно отличие С от С++

Это вот прям такое, мажорное отличие. Скажете его на собесе - все охренеют, вам руку пожмут через вебку и возьмут вас на работу сразу же(но это не точно).

В языке С нет проблемы Static initialization order fiasco!

Как же так? В С тоже есть статики и тоже есть разные единицы трансляции. Почему так?

All the expressions in an initializer 
for an object that has static storage
duration or in an initializer list for an
object that has aggregate or union type
shall be constant expressions.


Таким образом, в С статическая переменная со скалярным типом может быть инициализирована только константным выражением. Это не constexpr, а просто выражение, которое компилятор в состоянии вычислить во время компиляции. Если тип переменной представляет собой массив со скалярным типом элемента, то каждый инициализатор должен быть константным выражением и так далее. Поскольку такие выражения не могут ни вызывать побочных эффектов, ни зависеть от побочных эффектов, вызванных любыми другими вычислениями, изменение порядка вычисление константных выражений не влияет на результат. А значит и никакого фиаско нет!

Единственные неконстантные выражения, которые могут быть вычислены перед main, - это те, которые вызываются из среды выполнения C
. Вот почему объекты FILE, на которые указывают stdin, stdout и stderr, уже доступны для использования сразу же после начала main.

Стандартный C не позволяет пользователям регистрировать свой собственный код запуска перед основным, хотя GCC предоставляет расширение под названием constructor (возможна массонская связь с конструкторами из C++), которое вы можете использовать для воссоздания SIOF в C. Но это, как говорится, НЕСТАНДАРТ и у каждого свой путь в могилу.

Целью Страуструпа было сделать пользовательские типы пригодными для использования везде, где есть встроенные типы. Это означало, что C++ должен был разрешать глобальным переменным быть кастомными типами, что означает, что их конструкторы будут вызываться во время запуска программы. Поскольку в начале C++ не было функций constexpr, такие вызовы конструкторов никогда не могли быть постоянными выражениями. И так, родилось чудовище, погубившее много наших ребят - Static initialization order fiasco.

В процессе стандартизации C++ вопрос о порядке выполнения статической инициализации был спорной темой. Я думаю, что вы согласитесь с тем, что идеальная ситуация - это когда каждая статическая переменная была инициализирована до ее использования. К сожалению, для этого требуется технология компоновки, которой в те дни не существовало (и, вероятно, до сих пор не существует?). Инициализация статической переменной может включать вызовы функций, и эти функции могут быть определены в другой TU, что означает, что вам нужно будет выполнить анализ всей программы, чтобы успешно отсортировать статические переменные в порядке зависимостей. Стоит отметить, что даже если бы C++ мог быть разработан таким образом, он все равно не полностью предотвратил бы проблемы с порядком инициализации. Представьте, если бы у вас была какая-то библиотека, где предварительным условием функции использования было то, что функция init() была вызвана в какой-то момент в прошлом и повлияла на нужную для инициализацию переменную. Компилятор не может увидеть такие зависимости, которые есть только у программиста в голове. По коду этого совсем не видно. Поэтому, думаю, что даже полноценный анализ кода не помог бы решить проблему.

В конечном счете, ограниченные гарантии порядка инициализации, которые мы получили в C++98, были лучшими, которые мы могли получить в данных обстоятельствах. С помощью народного "а вот сделали бы по-человечески", возможно, многие из нас высказали пару ласковых о том, что тот стандарт не был полным без функций constexpr и что статические переменные должны иметь только константную инициализацию. Но такого рода размышления надо оставить это нытикам и нюням. А настоящие программисты прогают на том, что есть. В тех условиях, в которых возможно.

Don't complain to your life. Work on it and stay cool.

#cppcore #goodoldc