Грокаем C++
6.04K subscribers
22 photos
3 files
314 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