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

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

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

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

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

Здесь не будет бесполезных 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
База алгоритмов STL

Когда я только изучал плюсы, меня приводило в ступор обилие и разнообразие в стандартной библиотеке. Контейнеры еще куда ни шло. Их довольно немного, а, при знании их устройства, с ними довольно комфортно работать per se.
Но вот алгоритмы…

Их дохрена, хрен их запомнишь и хрен поймешь еще, когда их использовать.

А что, если я скажу, что в бэкэнд разработке в 99% случаев будут использоваться только три алгоритма?
Естественно, в этот список не входят пустышки, типа
std::swap и прочей тривиальщины.

В следующих постах буду раскрывать каждый из этих
алгоритмов.

Stay Cool.

#STL #algorithms
В продолжение к предыдущему посту

Хочу пояснить пометку «в бэкэнд разработке».
C++ - очень мощная инструмент. Так или иначе большинство современных языков написаны с его использованием (первичный компилятор, интерпретатор), почти все системное ПО написано на связке С/С++, браузеры, нейросети, дата сайенс инструменты.

Плюсы везде

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

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

Есть такой мем, что люди учатся в Бауманке, потом жестко ботают языки и алгоритмы, чтобы наконец устроиться в компанию мечты - Яндекс. А потом всю карьеру перекладывают джейсоны да эксемельки из одного места в другое.
Ну да. А что вы хотели от бэкэнда? Если бизнесу нужно перекладывать джейсоны, ты будешь их перекладывать. И это нормально. Далеко не все проекты rocket science и это нужно принять.

Так вот. Если вам не подходят методы стандартных контейнейров, то скорее всего вам нужна какая-то кастомная нетривиальная обработка. С которой алгоритмы STL, при всей их обобщенности, не справляются.

Stay cool.
Ссылки vs Указатели

Давайте раз и навсегда разберем, чем отличаются эти две сущности.

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

Ссылка

📎 Является псевдонимом переменной
📎 Нельзя декларировать, только определять. Ссылка не может быть неинициализированной
📎 Один раз инициализируется, переприсвоить ее нельзя
📎 Объект инициализации должен иметь адрес
📎 Не имеет своего адреса. Попытка взять адрес от ссылки вернёт адрес объекта, на который ссылка указывает
📎 Не может иметь невалидное значение, так как инициализируются существующим обьектом на стеке
📎 Разыменовывается автоматически при использовании
📎 Не бывает константной. В этом нет смысла, так как ссылка и так неизменяема. То есть нельзя перенаправить ссылку на другой объект.
📎 Является обёртка над указателем с лимитированы функционалом (над константным указателем строго говоря, из этого и следует предыдущий пункт)

Указатель

👉🏼 Является переменной на стеке, содержащей типизированный адрес куска памяти
👉🏼 Для получения значения по указателю его нужно разыменовать, что довольно неприятно и неудобно
👉🏼 Может иметь сколько угодно уровней индирекции. То есть я могу определить указатель на указатель на указатель на инт. Ссылки никого не могут
👉🏼 Можно объявить массив указателей. Объявить массив ссылок нельзя
👉🏼 Самое практичное отличие - у указателей есть своя арифметика. Для них определены + и -. Ссылки в такое не могут
👉🏼 Имеет определённое невалидное значение nullptr

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

Stay cool.

#cppcore
Сортировка

std::sort - самый нужный и наиболее часто используемый алгоритм из библиотеки STL. Не зря мир серьезных алгоритмов маленькие хакеры начинают познавать с сортировки. Конечно после изучения всех базовых концепций языка и программных сущностей. Это реально нужная на практике вещь.

Давайте на пальцах.

Есть у вас вектор для каких-то объектов. Вы заполняете этот вектор объектами просто с помощью push_back. Здесь есть 2 варианта.

Первый вариант - порядок элементов неважен, работаем со всеми элементами по очереди или только с крайними(ну например очередь задач или список слушателей евентов класса). Тут все просто, испотльзуй методы контейнера и живи счастливо.

Второй вариант - вы хотите переупорядочить элементы в контейнере или сделать вставку/удаление/поиск/изменение элемента из середины контейнера. Тут все сложнее. Для начала надо подумать, потому что вы скорее всего выбрали неправильный контейнер. Алгоритмы find, remove и тому подобное - от Иблиса. Может показаться, что линейная сложность не проблема, не так уж это и много. Но нет. Вряд ли вам нужно один раз найти какой-то элемент к контейнере и скорее всего там будет квадратичная сложность. Про удаление я вообще молчу, внутреннее устройство вектора даст вашему первомансу отдохнуть.
Присмотритесь к ассоциативным контейнерам, когда вам нужны такие операции. И если нужно переупорядочивать элементы, то тоже проверьте себя еще раз.
Проверили? Идем дальше. У нас остается вектор и вы хотите его переупорядочить. Переупорядочить - назначить отношение порядка. Значит можно сказать, какой элемент идет за каким. Значит можно сказать, какой элемент "меньше" другого. Так и приходим к сортировке.

По факту, сортировка - единственный алгоритм, который реально можно применить для последовательных контейнеров. Ну ладно не единственный. Но это уже тема для другого поста.

Stay cool.

#STL #algorithms
Small string optimization

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

Дело в том, что в основе плюсовой строки лежит обычный сишный массив. Точнее не обычный, а выделенный в куче. Так вот, когда вы пишите std::string var = "Like this post", по идее на куче должен выделяться блок памяти, равный размеру этой строки. Но. Зачастую пользователь класса std::string хочет присвоить переменной какую-нибудь короткую строку, которую он менять не собирается. Зачем тогда тратить драгоценные клоки проца и делать дорогостоящий вызов, когда можно поместить эту короткую строку в маленький внутренний буффер самого объекта строки и жить счастливо? Не дергать аллокатор, не фрагментировать память. Хорошо звучит? Вот и индусы так же подумали и запилили small string optimization.

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

Давайте все быть такими же умными, как индусы.

Stay cool.

#optimization #datastructures
struct vs class

Если ты перед каждым объявлением класса думаешь над тем, какое ключевое слово использовать - ты по адресу.

Я лично всегда триггерился, когда полноценный класс объявляют структурой. Да где это виданно?!? Ты объявляешь класс, в языке есть очень похожее ключевое слово, которое говорит, что началось объявление класса. И ты, как полностью психически здоровый человек, пишешь struct. Круто че. Мои полномочия тут все.

И вот ты хочешь сделать "по правильному" и объявить класс классом. А потом понимаешь, что половина проекта использует struct, да еще в коде, для которого твой класс предназначен, тоже используются структуры. И закрадываются сомнения. Может это не мир неправильный, а я?

Но потом пришло принятие и понимание. Структуры проще писать. Может я один такой, конечно, невнимательный. Но насколько часто вы встречаете ошибку с использованием приватного метода вне класса? У меня это ошибка постоянно всплывает, когда я использую class. Меня это ужасно раздражало, поэтому сдался и начался просто всегда писать struct.

Все потому, что struct обладает двумя преумуществами в реальной разработке по сравнению с class. Все поля публичные и наследование тоже публичное.
Вспомните, сколько раз вам было нужно приватное наследование? Если хоть раз и оправданно, то вы бриллиант. А челяди эта фича не нужна. То есть уже на одно слово меньше нужно писать.
А исходя из концепции ООП, нам обычно нужно скрыть данные от чужих глаз. Я не думаю над тем, как открыть мой публичный интерфейс. Я думаю над тем, что мне нужно скрыть. Поэтому очень органично после публичных методов я пишу private: и дальше пишу всю запрещенку. Да и принято так, что публичный интерфейс описывается всегда первым. Поэтому я могу не писать лишний раз public:.

Экономится целых 1 слово на класс и еще одно на наследуемый класс. Это ли не причина перейти на struct?)

Stay cool.

#cppcore
CTAD - Class Template Argument Deduction

Одним из приятных нововведений C++ 17 стандарта стало автоматическое выведение типа классов при объявлении шаблонных объектов, функций и т.д.

Этот механизм позволяет вам явно не указывать тип аргументов шаблонных классов там, где они могут быть выведены компилятором. Пример:
// vector declaration before C++17
std::vector<double> v1 = {1.0, 2.0, 3.0};

// CTAD vector declaration since C++17
std::vector v2 = {1.0, 2.0, 3.0};


Давайте посмотрим на живой пример. Обратите внимание, что в строке параметров компилятора указан 17 стандарт -std=c++17. Если его попробовать переключить на более ранний стандарт -std=c++14, то возможность компиляции будет утрачена.

Ключевым моментом CTAD для компилятора является возможность вывести этот тип. Если при попытке выведения появляется несколько кандидатов, то попытка выведения типа приведет к ошибке. Пример:
// Error: no viable constructor or deduction guide for deduction of template arguments of 'vector'
std::vector data = {1.0, 2.0, 3, 4};

В данном случае тип передаваемых значений различен: double и int. Компилятор не будет брать на себя ответственность выбирать какой-то тип самостоятельно. Вдруг это принципиально важно для корректности исполнения ваших программ?

Однако, так же стоит и помнить про неожиданные эффекты CTAD. При инициализации из одного аргумента типа, который является специализацией рассматриваемого шаблона класса, вывод копирования обычно предпочтительнее переноса по умолчанию:
// The type is std::vector<int>
std::vector v1{1, 2};

// The type is std::vector<int>
std::vector v2{v1};

// The type is std::vector<std::vector<int>>
std::vector v3{v1, v2};


Компилятор дает большее предпочтение конструкторам initializer-list, но при list-initialization остается неизменным:
// The type is std::vector<int>
std::vector v1{1, 2};

// The type is std::vector<int>
std::vector v2(v1.begin(), v1.end());

// The type is std::vector<std::vector<int>::iterator>
std::vector v3{v1.begin(), v1.end()};


Подробнее: cppreference

#cpp17
# Комментарии в коде

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

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

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

Пишите комменты только для действительно нетривиальных вещей. Когда у потенциального читателя может возникнуть вопрос: "а как получается, что эта последовательность действий приводит к этому результату?" - вот то место, куда нужно вставить коммент.

Stay cool.

#goodpractice
Циклы

Было время, работал я датасайентистом. Написал я там на питоне один раз функцию, которая в цикле перебирает весь датасет и что-то делает с каждой строчкой. Тогда мне мой ментор сказал: "Циклы в чистом питоне, как мои нейроны, после разговора с феминиской - очень медленные. Используй pandas.apply". Дело в том, что питон - сам по себе очень медленный язык, а библиотеки под него, зачатую написаны на cython или вообще на плюсах. Поэтому они быстрые.

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

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

Не придумывайте велосипеды. Оставьте это импортозаметителям.

Stay cool.

#goodpractice
Инициализация глобальных объектов

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

Ладно, если в разных cpp файлах у вас весь глобальный или статический код иниациализируется независимо друг от друга. Куда ни шло. Но если объекты создаются с зависимостями в другой единице трансляции - ждите беды.

// file1.c

extern const X x;

const Y y = f(x); // read x; write y

// file2.c

extern const Y y;

const X x = g(y); // read y; write x

Смотрим на код. Поскольку x и y находятся в разных единицах трансляции, порядок вызовов f() и g() не определен; В любом из случаев здесь будет доступ к неинициализированной константе. Это показывает, что проблема порядка инициализации глобальных объектов не ограничивается глобальными переменными. Таким образом и статические объекты могут зафакапиться.

А если код конкурентный... Даже продолжать не буду.

Старайтесь по максимуму избегать глобальных объектов. Это типа антипаттерн и может привести к разным проблемам.
И старайтесь по максимуму использовать constexpr. Это упрощает инициализацию кода. А вы будете крутыми типами, которые используют фишки modern c++.

Stay cool.

#cppcore
Магические числа

А что мы, программисты, знаем о магии? Правильно, магия - это зло, с которым нужно бороться и искоренять!

Сегодня речь пойдет о магических константах и переменных. Так называют разные числовые значения, смысл которых очень трудно понять. Типовые ситуации:
1. Готовый результат каких-то промежуточных операций
2. Прямая подстановка значений в вычисления

Главная проблема всего магического и загадочного - это отсутствие понимания, как и что работает у тех, кто должен внести изменения. Зачастую эта проблема причиняет большие страдания вовсе не автору кода, а его наследникам. Так получается, потому что автор на момент написания программы обладает большим количеством знаний. Он же это пишет, черт возьми, для него это вовсе не магия. Но у других людей, которые видят готовое решение впервые, этих знаний нет. Им приходится их приобрести путем прочтения этого кода. Хорошо, когда есть у кого можно спросить, а если нет? А автор кода готов рассказывать об алгоритме своего решения каждый раз, когда кто-то другой редактирует это место?

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

Я вам посоветую:

1. Проявляйте эмпатию и поберегите своих коллег.
Это универсальный совет! Поставьте себя на место своих коллег и попробуйте представить, будет ли им понятно, что написано в коде при первом прочтении?
Если вы учтёте все нюансы, напишите комментарии, то ваш код полюбят и будут беречь долгие годы.

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

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

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

#goodpractice
Почему РКН не сможет полностью заблокировать VPN

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

За свою профессиональную карьеру плюсовика мне довелось плотно поработать с DPI. Работа как раз заключалась в анализе трафика сервиса, нахождении паттернов в полезной нагрузке пакетов или в метаданных и детектировании сервиса по этим паттернам. Это позволяло с высокой точностью определить протокол, сервис и даже пользовательскую активность в сервисе. То есть тема реально рабочая.

Однако есть и проблемы. Шаблоны-то не динамические. Если ты думаешь, что один раз нашел паттерн и он всегда будет работать - подумай еще раз. Ни что не вечно, а мир Айти - тем более. Проприетарные протоколы общения сервисов меняются в худшем случае с каждым обновлением аппки. Поэтому приходилось периодически перепроверять эти шаблоны и вносить изменения в систему, чтобы она снова работала с нужной точностью. Теперь представьте обилие ВПН протоколов и приложений, которые их используют. Теперь представьте, какое количество человекочасов нужно, чтобы проанализировать трафик, исправить паттерны в КОДЕ и провалидировать это дело. А теперь представьте количество программистов в компаниях, предоставляющих DPI решения (ведь РКН сам ничего не разрабатывает). И наконец, представьте обилие бюрократии, коммуникаций и времени, которое необходимо для того, чтобы запрос РКН на обновление сервиса выполнился.

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

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

Оставайся на связи. Stay cool.

#net #howitworks
Указатель на void

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

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

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

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

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

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

Stay cool.

#cppcore #goodoldc #memory
Что будет, если malloc'нуть 100 Гигабайт?

У одного моего друга недавно на ревью спросили этот вопрос. Вопрос интересный, и если не знать нюансов, то результат можно только предполагать. Друг накидал вариантов, но ревьюер ответа тоже не знал, поэтому это была задачка на подумать и порассуждать. А я решил не думать, а проверить. Так и родился этот пост.

У меня получилось 2 варианта развития событий: попробовать это провернуть на тачке с привычными характеристиками и на мощном сервере, где RAM много больше 100 Гб. Так уж вышло, что я имею доступ и к одному и к другому. Поэтому раскрою оба варианта.

Чтобы вдруг не заруинить сервер и свою тачку, я попробовал запустить код в онлайн компилляторе wandbox.
В мэйне было 2 строчки:

void *p = malloc(100000000000L); // да да 100 миллиардов байт это не 100 гб, знаю знаю... Jesus Christ...
printf("got %pn", p);


Запустил я это. И даже получил результат. Ничего не упало. Вывод: got (nil). То есть ОС просто отказала маллоку в выдаче такого большого объема памяти. И маллок вернул нулевой указатель. Это кстати одна из причин, почему всегда стоит проверять указатель, возвращенный маллоком, на равенство нулю. Иначе получите segfault.

Далее попробовал на своей локальной машинке. Результат тот же. Никто не хочет мне давать память😭. Но хотя бы комп не сломал.

Ну и наконец, на серваке получил валидный адрес: got 0x7f1e4f2e1010.

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

Оставайся на связи. Stay cool.

#fun