Грокаем C++

@grokaemcpp


Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

Грокаем C++

22 Oct, 09:00


​​Разница инициализаций

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

Единые правила - хорошая вещь. И как многие хорошие вещи, они чего-то стоят. А в С++ есть такой девиз: "мы не платим за то, что не используем". Мне не всегда нужно задавать значение переменной. Иногда меня это вообще не интересует. Я могу создать неинициализированную переменную и передать ее в функцию, где ей присвоится конкретное значение.

int i;
FillUpVariable(i);


Чтобы ответить на вопрос из начала поста, давайте посмотрим, чем мы вообще платим за инициализацию в обоих случаях.

Рассмотрим локальные переменные.

В сущности, они являются просто набором байт на текущем фрейме стека. И программа интерпретирует эти байты, как наши локальные переменные.

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

Теперь глобальные переменные

Они инициализируются всего один раз при старте программы. Соответственно, стоимость - немножко кода 1 раз при старте программы.

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

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

Теперь представьте, что мы бы потребовали устанавливать валидное значение всегда. Это просто неэффективно. Да и не нужно.

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

Be effective. Stay cool.

#cppcore #compiler

Грокаем C++

21 Oct, 12:00


Квиз
#новичкам

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

У меня к вам всего один вопрос. Что будет в результате попытки компиляции и запуска этого кода?

#include <iostream>

int id;

int main()
{
std::cout << id;
}

Грокаем C++

21 Oct, 10:01


Бесплатное IT-образование в 2024

Отобрали для вас полезные телеграм-каналы, которые помогут освоить программирование и другие IT-направления

Выбирайте нужное и подписывайтесь:

👩‍💻 С/С++: @Cpportal
📱 GitHub: @git_developer
🤓 Книги айти: @portalToIT
👩‍💻 Frontend: @FrontendPortal
⚙️ Backend: @BackendPortal
👩‍💻 Python: @PythonPortal
👩‍💻 Java: @Java_Iibrary
👩‍💻 C#: @KodBlog
🖥 Базы Данных & SQL: @SQL
👩‍💻 Golang: @juniorGolang
👩‍💻 PHP: @PHPortal
👩‍💻 Моб. разработка: @MobDev
👩‍💻 Разработка игр: @GameDevgx
👩‍💻 DevOps: @loose_code
🖥 Data Science: @DataSciencegx
🤔 Хакинг & ИБ: @cybersecinform
🐞 Тестирование: @QAPortal
📱 Маркетинг: @MarketingPortal
🖥 Дизайн: @PortalToDesign

➡️ Сохраняйте себе, чтобы не потерять

Грокаем C++

20 Oct, 09:00


Материалы для обучения
#новичкам

В этом посте вы очень хорошо постарались и накидали много ресурсов. Сейчас мы их немного систематизируем.

Начнем с самого популярного запроса. Книги. Пдфки будут в комментах.

База:

Бьерн Страуструп. "Программирование: принципы и практика использования C++".

Стивен Прата. «Язык программирования C++»

Стенли Липпман. "Язык программирования C++. Базовый курс"

Эндрю Кениг. "Эффективное программирование на С++"

Брайан Керниган. «Язык программирования С»


Немножко компьютер сайенса:

Бхаргава Адитья. "Грокаем алгоритмы".

Кирилл Бобров. "Грокаем конкурентность".


Книжки по продвинутому С++. Накладываются уже на адекватные знания языка и навыки написания кода.

Скотт Майерс. "Эффективный и современный С++"

Бартоломей Филипек. "С++17 в деталях".

Энтони Уильямс. «С++. Практика многопоточного программирования»

Пикус Ф. «Идиомы и паттерны проектирования С++».

Можно еще вот сюда заглянуть. Там еще больше полезных книжек.



Курсы:

Пояса от Яндекса. Платный.

"Добрый, добрый ОПП С++" на Stepik. Совсем недорогой.

1 и 2 части курса программирования на C++ от Computer Science Center на платформе Stepik. Из всех курсов, которые я изучал, это лучший в рунете имхо.

Программирование на языке C++ на Stepik. Бесплатный.

Программирование на языке C++ (продолжение) на Stepik. Бесплатный.


Введение в программирование (C++)курс Яндекса на Stepik. Бесплатный

Базовый курс С++ от Хэкслет. Бесплатный

Бесплатный курс от Яндекса

C++ Tutorial . Бесплатно

Яндекс Практикум «Разработчик С++». Платно.


Ютуб:

Константин Владимиров обо всем

Илья Мещерин С++

Роман Липовский. Конкурентность. Лекции и семинары

TheCherno. Нужен английский.

Simple Code


Интернет ресурсы:

https://ravesli.com/uroki-cpp. Нужен впн

https://www.learncpp.com

https://metanit.com/cpp/tutorial

https://leetcode.com - решение алгоритмических задачек

Теперь отсебятина

У всех разная подходящая модель обучения. Не концентрируйтесь только на книгах или курсах. У всего есть свои плюсы. Надо попробовать все и найти подходящий ВАМ формат обучения. Но нужны какие-то начальные рекомендации. Я бы начал с одной из базовых книг и обязательно после каждой главы решал бы задачки(это самое главное, иначе не запомнится). "Чтобы научиться программированию, необходимо писать программы" - Брайан Керниган. Поэтому чуть освоившись с языком я бы пошел на какие-нибудь курсы из списка и просто начал бы писать код. Пройдите 3-4 из них и вы уже будете довольно хороши.

Дальше уже можете на эту базу наваливать и лекции, и специфику, и прочее.

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

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

Заслуженно помещаем этот пост в закреп. Теперь можно отправлять всех на него.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Upgrade yourself. Stay cool.

#digest

Грокаем C++

18 Oct, 09:09


Опасности автоматического вывода типов
#новичкам

C++17 дал нам замечательную фичу CTAD. Это автоматический вывод шаблонных параметров класса по инициализатору.

Теперь, если вы хотите создать например пару строки и числа, то вместо этого:

std::pair<std::string, int> pair{"Hello there!", 1};


Можно писать так:
std::pair pair{"Hello there!", 1};


Удобно? Безусловно! Только вот один вопросик есть.

Что будет, если я попытаюсь достать размер строки?

size_t size = pair.first.size();


А будет ошибка

error: request for member 'size' in 'a.std::pair<const char*, int>::first', 
which is of non-class type 'const char*'


Пара-то на самом деле не из строки и числа, а из указателя и числа. Но это и правильно. Компилятор не умеет читать мысли, а четко работает с тем, что ему предоставили. В данном случае "Hello there!" действительно преобразуется в тип const char*. Если вы хотите std::string, то нужно явно показать это компилятору:

std::pair pair{std::string("Hello there!"), 1};


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

Поэтому при использовании CTAD нужно тщательно следить за типами аргументов. Классы с не explicit конструкторами могут наделать большую невкусную кучу беспокойства.

Кстати, знаю адептов строгой типизации, которые даже auto не признают. А как вы относитесь к автоматическому выводу типов? Жду ваши мысли в комментах)

Be careful. Stay cool.

#cppcore #cpp17

Грокаем C++

17 Oct, 09:00


Так больше нельзя жить

Все. Нет сил больше игнорировать эту тему.

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

Хватит это терпеть!

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

Прошу активно участвовать в дискуссии. Мы все-таки новое поколение плюсовиков растим. Будущую гордость страны!

Мы вообще программисты или кто? Давайте автоматизируем процесс рекомендации.

Make life easier. Stay cool.

Грокаем C++

16 Oct, 09:00


​​Достаем элемент из последовательного контейнера

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

Я говорю о попе элементов. Не вот этой ( | ), а вот этом

void pop_back();

void pop_front();


Эти методы достают из контейнера элементы из зада или из переда соответственно.

"Какие тут проблемы?" - спросите вы.

И я вам отвечу.

Что произойдет, если я вызову эти методы на пустом контейнере? Если вы задумались, то это нормально, обычно такого не происходит. Но вот я такой Маша-растеряша и забыл проверить на пустоту перед вызовом. Будет UB.

Даже не исключение, которое можно обработать. Просто УБ. И можно УБиться в поисках бага, которая появится в следствии пропуска одной проверки.

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

Туда же идут и методы front() и back(). Они дают такое же UB, когда контейнер пуст.

Почему так сложилось? Вопрос сложный.

Но не в этом суть.

Суть в том, что не нужно делать похожий интерфейс у своих классов. Давайте пользователю сразу понять, что в функции может что-то пойти не так и эту ситуацию надо обработать.

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

Даже что-то подобное, на мой взгляд, куда более безопасный дизайн:

bool pop_back() {
if (data_.empty()) {
return false;
}
// remove element
return true;
}


А если его еще и аттрибутом nodiscard пометить, будет вообще щикарно(привет фанатам южного парка).

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

Но язык С++ никогда не славился своей безопасностью. И если вы можете своими силами обезопасить свой проект - нужно это делать. Даже таким несовершенным образом.

Stay safe. Stay cool.

#cppcore #STL

Грокаем C++

15 Oct, 10:00


​​Достигаем недостижимое

В прошлом посте вот такой код:

int main() {
while(1);
return 0;
}

void unreachable() {
std::cout << "Hello, World!" << std::endl;
}


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

Темная магия это или проделки ГосДепа узнаем дальше.

Для начала, этот код содержит UB. Согласно стандарту программа должна производить какие-то обозримые эффекты. Или завершиться, или работать с вводом-выводом, или работать с volatile переменными, или выполнять синхронизирующие операции. Это требования forward progress. Если программа ничего из этого не делает - код содержит UB.

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

Тут очень важно понять одну вещь. Компилятор следует не вашей логике и ожиданиям, как должна работать программа. У него есть фактически инструкция(стандарт), которой он следует.

По стандарту программа, содержащая бесконечные циклы без side-эффектов, содержит UB и компилятор имеет право делать с этим циклом все, что ему захочется.

В данном случае он просто удаляет цикл. Но он не только удаляет цикл. Но еще и удаляет инструкцию возврата из main.

В нормальных программах функция main в ассемблере представляет из себя следующее:

main:
// Perform some code
ret


ret - инструкция возврата из функции. И код функции main выполняется, пока не достигнет этой инструкции.

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

main:

unreachable():
push rax
mov rdi, qword ptr [rip + std::cout@GOTPCREL]
lea rsi, [rip + .L.str]
call std::basic_ostream<char, std::char_traits<char>>...


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

Справедливости ради стоит сказать, что в 19-м шланге поменяли это поведение и теперь таких неожиданностей нет.

Stay predictable. Stay cool.

#fun #cppcore #compiler

Грокаем C++

14 Oct, 09:00


Правда С++ замечательный язык?)

Грокаем C++

13 Oct, 10:00


​​Неименованные параметры функций

С++ позволяет не указывать имена параметров функций, если они не используются в коде.


void foo(int /no name here/);

void foo(int /no name here/)
{
std::cout << "foo" << std::endl;
}

foo(5);


Это можно делать и в объявлении функции, и в ее определении.

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

Но вот вопрос возникает тогда. Если параметр ничего не делает, нахрена он тогда вообще нужен?

На самом деле много кейсов, где неименованный параметр может пригодится.

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

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

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

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

💥 Знаменитая перегрузка постфиксного оператора инкремента/декремента. Есть 2 вида этих операторов: префикстный и постфиксный. Проблема в том, что это все еще вызов функции operator++. Как различить реализации этих функций? Правильно, нужна перегрузка. Вот здесь и приходит на помощь безымянный параметр: в коде он не нужен, но влияет на выбор конкретной перегрузки. Выглядит это так:

struct Digit
{
Digit(int digit=0) : m_digit{digit} {}
Digit& operator++(); // prefix has no parameter
Digit operator++(int); // postfix has an int parameter
private:
int m_digit{};
};


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

Stay useful. Stay cool.

#cppcore #design

Грокаем C++

11 Oct, 10:08


​​Как передать в поток ссылку на объект?

Глупый вопрос на первый взгляд. Ну вот есть у вас функция

void RunSomeThread(const & SomeType obj) {...}


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

std::thread thr(RunSomeThread, obj);


Запускаете прогу, все нормально работает, вы довольный пьете кофеек. Но решаете проверить логи. Так, на всякий случай. А вы очень не хотите лишних копирований объектов SomeFunckingType. Поэтому логируете создание этих объектов. И в логах обнаруживаете странную штуку: ваш объект скопировался. WTF???

Дело в том, что новосозданный поток копирует аргументы своего конструктора в свой внутренний сторадж. Зачем это нужно? Проблема в том, что параметры, которые вы передали, могут не пережить время жизни потока и удалиться до его завершения. Тогда обращение к ним по ссылке вело бы к неопределенному поведению. Но копирование выполняется только для тех параметров, которые переданы по значению. Если передавать параметр по ссылке, то ссылка передастся во внутренний сторадж потока без копирования. Это нужно делать только тогда, когда вы на 100% уверены, что ваш аргумент переживет цикл жизни потока.

"Но я же передал obj по ссылке!" Погоди....

Поскольку в плюсах так просто передать в функцию объект по ссылке - нужно просто указать имя этого объекта в параметрах функции, то конструктор std::thread сознательно предотвращает такие неосознанные манипуляции. Тут нужно явно показать, что мы знаем о проблеме и готовы идти на риск.

А сделать это очень просто. С помощью std::ref. Эта функция оборачивает ваш объект в другой шаблонный класс std::reference_wrapper, который хранит адрес вашего объекта. Теперь вы можете написать вот так:

std::thread thr(RunFuckingThread, std::ref(obj));


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

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

Stay conscious. Stay cool.

#concurrency #cppcore #memory

Грокаем C++

10 Oct, 09:00


​​Как узнать четное ли число

Вы сейчас подумали, типа "wtf, он шо нас за идиотов держит". Но погодите, щас все объясню.

Есть одно условие: нельзя использовать операции целочисленного деления и брать остаток от деления. Вот это уже задачка не для второклассников и обычный человек вряд ли с ней справится. Но программист может. Хотя и не любой, судя по моему небольшому опросу😆)

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

В общем. Нужно просто проверить последний бит. Если он ноль - число четное, если нет - число нечетное. Все очень просто. Делается это с помощью битового & с единичкой.

Но во время написания этого поста мне пришла идея задать эту задачку ChatGPT, в тему недавнего поста про него. Правда я попросил сгенерировать 3 примера. Чисто из интереса. И результат меня сильно удивил. Все 3 примера были правильные, среди них было решение из абзаца выше, но было и еще 2, о которых я и не думал. После этого попросил нагенерить еще 2 примера. И они тоже были верные. Конечно, все из них использовали битовые операции, но как филигранно!

Очень интересно решение с битовым умножением на -2. Дело в том, что -2 в памяти компьютера представляется как 111...1110. Поэтому умножение любого числа на -2 будет давать то же самое число, только если последний бит был выставлен в 0.

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

Stay amazed. Stay cool.

#fun

Грокаем C++

09 Oct, 10:00


Удаляем элемент из ассоциативного контейнера по значению

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

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

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

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

std::map<int, int> map{{1, 6}, {2, 7}, {3, 8}, {4, 9}, {5, 10}};
// вот так
auto it = std::find_if(map.begin(), map.end(), [](const auto& elem) {return elem.second == 10;});
map.erase(it);
//
std::for_each(map.begin(), map.end(), [](const auto& item){
std::cout << item.first << " " << item.second << std::endl;});
// OUTPUT
// 1 6
// 2 7
// 3 8
// 4 9


Две строчки на идейно очень простое и понятное действие. Ох, если бы был метод erase_if...

И вы знаете, в С++20 появились перегрузки свободной функции std::erase_if для каждого стандартного контейнера. Теперь можно написать просто вот так:

std::erase_if(map, [](const auto& elem) {return elem.second == 10;});


И результат вывода будет таким же.

У кого есть только древние плюсы - не переживайте. Для вас эти перегрузки реализовали в экспериментальной библиотеке. Просто сделайте так:

#include <experimental/map>
std::experimental::erase_if(map, [](const auto& elem) {return elem.second == 10;});


И все заработает.

Do things easier. Stay cool.

#STL #cpp20

Грокаем C++

08 Oct, 09:00


Шутейки от чатгпт

Почему С++ программисты не играют в прятки? Они задолбались искать баги, скрытые в своем коде!
(на картинке типа плюсовик в поте лица ищет баги. Так, что аж ноут запотел)

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

Грокаем C++

07 Oct, 09:00


​​Результаты ревью
#опытным

Понимаю, что код был, мягко говоря, не для новичков, хоть и иллюстрировал начальный пример работы с epoll. Поэтому критиков не очень было много. Но все же Максим и Михаил хорошо постарались с поиском проблем. Давайте им похлопаем 👏👏👏.

А теперь суммируем.

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

🔞 В функции read_data_and_count есть возвращаемое значение, но ничего не возвращается.

🔞 Повсюду утечки. Не освобождаются entries, не закрывается дескриптор epoll_fd.

🔞 Все три функции связаны и, если уж мы пишем на С++, то хочется все это дело обернуть в класс. А если не очень хочется, то хотя бы статиками пометить, чтобы скоуп не засорять.

🔞 Надоели указатели эти. Мы же С++, нам ссылки подавай!

🔞 Цикл, собирающий результаты мог бы быть более полюсовым и использовать стандартные функции из STL.

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

Из неочевидного:

😱 Использование VLA, то есть variable-length array, в строчке с epoll_event pending[N]. Это не стандарт языка С++, поэтому надо переделать на нормальный вектор.

😱 Не проверяются никакие ошибки. epoll_create1, epoll_wait, calloc, read - все эти системные функции не только говорят, что все летает и играет яркими красками, а еще и сигнализируют об ошибках. Здесь нигде их возвращаемые значения не проверяются, в надежде, что мы живем в мире розовых пони, где все всегда стабильно работает.

😱 Еполл может вернуть событие EPOLLERR в случае какой-то ошибки. А также он может прерваться из-за приема сигнала сгенерить ошибку EINTR. Опять же нет проверки.

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

😱 Не очень понятно, зачем обработчику событий знать о том, сколько файлов осталось, и влиять на это количество. Из process_epoll_event вполне можно возвращать индикатор, сигнализирующий о том, что мы прочитали данные из сокета до конца. Таким образом чисто внутренняя переменная files_left становится в единоличной власти своей материнской функции. Собственно, как и должно быть. Тогда и флаг done в структуре не нужен.

😱 Обо всех ошибках вызывающему коду хотелось бы знать, поэтому надо как-то сообщать ему о них. Можно по аналогии с сисколами использовать возвращаемое значение -1, как индикатор ошибки, но по плюсовому можно использовать std::optional. Ну или кидать исключения/возвращать объект ошибки, кому как нравится.

Фух, вроде все, что увидел, написал. Кто еще что-то заметит - не стесняйтесь, пишите в комменты. Исправленный код скину в комменты.

Let people critique your solutions. Stay cool.

Грокаем C++

06 Oct, 11:00


​​Ревью
#опытным

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

1️⃣ Предварительно за кадром зарегистрировали нужные события на еполле

2️⃣ Создали массив из эвентов, в который эполл будет записывать произошедшие события

3️⃣ Дожидаемся этих событий в epoll_wait и дальше как-то обрабатываем.

Если вы знакомы с select|poll, то здесь небольшие отличия в интерфейсе + еполл сам говорит нам на каких дискрипторах появился евент.

Цель всего этого добра - подсчитать общее количество байт, которое пришло на все дескрипторы.

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

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

Ну и чего ждем? Комментарии сами себя не напишут! Погнали хейтить чужой код!

Analyse solutions. Stay cool.

Грокаем C++

04 Oct, 09:09


​​Signed Integer overflow

Переполнение знаковых целых чисел - всегда было и остается болью в левой булке. Раньше даже стандартом не было определено, каким образом отрицательные числа хранились бы в памяти. Однако с приходом С++20 мы можем смело утверждать, что стандартом разрешено единственное представление отрицательных чисел - дополнительный код или two's complement по-жидоанглосаксонски. Казалось бы, мы теперь знаем наверняка, что будет происходить с битиками при любых видах операций. Так давайте снимем клеймо позора с переполнения знаковых интов. Однако не все так просто оказывается.

С приходом С++20 только переполнение знаковых чисел вследствие преобразования стало определенным по стандарту поведением. Теперь говорится, что, если результирующий тип преобразование - знаковый, то значение переменной никак не изменяется, если исходное число может быть представлено в результирующем типе без потерь.
В обратном случае, если исходное число не может быть представлено в результирующем типе, то результирующим значением будет являться остаток от деления исходного значения по модулю 2^N, где N - количество бит, которое занимает результирующий тип. То есть результат будет получаться просто откидыванием лишних наиболее значащих бит и все!

Однако переполнение знаковых интов вследствие арифметических операций до сих пор является неопределенным поведением!(возмутительно восклицаю). Однако сколько бы возмущений не было, все упирается в конкретные причины. Я подумал вот о каких:

👉🏿 Переносимость. Разные системы работают по разным принципам и UB помогает поддерживать все системы оптимальным образом. Мы могли бы сказать, что пусть переполнение знаковых интов работает также как и переполнение беззнаковых. То есть получалось бы просто совершенно другое неожиданное (ожидаемое с точки зрения стандарта, но неожиданное для нас при запуске программы) значение. Однако некоторые системы просто напросто не продуцируют это "неправильное значение". Например, процессоры MIPS генерируют CPU exception при знаковом переполнении. Для обработки этих исключений и получения стандартного поведения было бы потрачено слишком много ресурсов.

👉🏿 Оптимизации. Неопределенное поведение позволяет компиляторам предположить, что переполнения не произойдет, и оптимизировать код. Действительно, если УБ - так плохо и об этом знают все, то можно предположить, что никто этого не допустит. Тогда компилятор может заняться своим любимым делом - оптимизировать все на свете.
Очень простой пример: когда происходит сравнение a - 10 < b -10, то компилятор может просто убрать вычитание и тогда переполнения не будет и все пойдет, как ожидается.

Так что УБ оставляет некий коридор свободы, благодаря которому могут существовать разные сценарии обработки переполнения: от полного его игнора до включения процессором "сирены", что произошло что-то очень плохое.

Leave room for uncertainty in life. Stay cool.

#cpp20 #compiler #cppcore

Грокаем C++

03 Oct, 09:00


Как компилятор определяет переполнение

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

Сразу с места в карьер. То есть в ассемблер.

Есть функция

int add(int lhs, int rhs) {
int sum;
if (__builtin_add_overflow(lhs, rhs, &sum))
abort();
return sum;
}


Посмотрим, во что эта штука компилируется под гцц х86.

Все немного упрощаю, но в целом картина такая:

    mov %edi, %eax
add %esi, %eax
jo call_abort
ret
call_abort:
call abort

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

Инструкция jo выполняет прыжок, если выставлен флаг OF в регистре EFLAGS. То есть Overflow Flag. Он выставляется в двух случаях:

1️⃣ Если операция между двумя положительными числами дает отрицательное число.

2️⃣ Если сумма двух отрицательных чисел дает в результате положительное число.

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

127 + 127 = 0111 1111 + 0111 1111 = 1111 1110 = -2 (в дополнительном коде)

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

Для беззнаковых чисел тоже кстати есть похожий флаг. CF или Carry Flag. Мы говорили, что переполнение для беззнаковых - не совсем переполнение, но процессор нам и о таком событии дает знать через выставление carry флага.

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

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

Detect problems. Stay cool.

#base #cppcore #compiler

7,325

subscribers

23

photos

1

videos