Грокаем C++
Открыть в Telegram
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Больше9 383
Подписчики
-224 часа
+27 дней
+1030 день
Архив постов
9 383
Помогите Доре найти ошибку
#опытным
А у нас новая рубрика #бага, где мы пытаемся найти нетривиальные ошибки в коде. Коллективные усилия и жаркие обсуждения в комментариях приветствуются.
Вот такой код:
template <typename T>
class Vector {
private:
T *m_data;
T *m_endSize;
T *m_endCapacity;
public:
// Use a different type "U to support const and non-const
template <typename U>
class Iterator {
private:
U *m_ptr;
public:
Iterator(U ptr) : m_ptr{ptr} {}
U &operator() const { return *m_ptr; }
};
template <typename Self>
auto begin(this Self &&self) {
return Iterator(self.m_data);
}
};
Это доморощенная и обрезанная версия вектора. Понятное дело, что здесь многого не хватает и этим нельзя пользоваться. Но зато это уже компилируется.
Тем не менее даже в таком маленьком кусочке кода есть принципиальная бага.
Сможете найти? Пишите свои варианты в комментариях.
Правильный ответ с пояснениями и фиксом будет завтра.
Deduce the error. Stay cool.9 383
Одно значимое улучшение С++17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
f( g(expr1, expr2), h(expr3) );В каком порядке вызываются expr1, expr2, expr3, g, h и f? Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr2 -> h -> f. Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом. Что было до С++17? Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все! То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f. Полный бардак! И это приводило на самом деле к неприятным последствиям. Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(new SomeClass1{}, new SomeClass2{});
Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
new SomeClass1{} -> new SomeClass2{} -> std::unique_ptr<SomeClass1> -> std::unique_ptr<SomeClass2>
Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
bar(std::make_unique<SomeClass1>(), std::make_unique<SomeClass2>());
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
f(e(), g(expr1, expr2), h(expr3));До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2. Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен. Теперь такой код не будет проблемой:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(new SomeClass1{}, new SomeClass2{});
потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp179 383
Недостатки std::make_shared. Кастомные делитеры
#новичкам
Заходим на cppreference и видим там такие слова:
This function may be used as an alternative to std::shared_ptr<T>(new T(args...)).
Также видим ее сигнатуру:
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
И понимаем, что make_shared не предоставляет возможности указывать кастомный делитер. Все аргументы функции просто перенаправляются в конструктор шареного указателя.
Опустим рассуждения об оправданных кейсах применения кастомных делитеров для шареных указателей. Можете рассказать о своих примерах из практики в комментариях.
Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".
Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.
Получается и деаллокация для этих смежных частей одного отрезка памяти должна быть совместная, единая и через базовый оператор delete.
Если бы мы как-то хотели бы встроить делитер в эту схему, получился бы конфликт: делитер хочет удалить только объект, но им придется пользоваться и для освобождения памяти под контрольный блок. Это просто некорректное поведение.
Да и скорее всего, если вас устраивает помещать объект в шареный указатель вызовом дефолтного new, то устроит и использование дефолтного delete. Поэтому эта проблема тесно связана с проблемой из первой части серии, но не добавляет особых проблем сверх этого.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory9 383
Недостатки std::make_shared. Непубличные конструкторы
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
struct Class: public std::enable_shared_from_this {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};
Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
#cppcore #cpp119 383
🚀 Начинаете путь в C++ или хотите разобраться в основах надежной разработки? Приглашаем на открытый вебинар:
«Обработка ошибок в C++: исключения, ожидания и исключения из правил»
📅 28 августа в 19:00 (МСК)
Чем сложнее становится приложение, тем важнее правильно обрабатывать ошибки. На вебинаре от курса «C++ Developer. Basic» вы разберётесь, какие подходы существуют в C++ и когда стоит применять каждый из них:
- Классические коды ошибок - плюсы и минусы
- Современные инструменты: std::error_code, std::optional, std::variant, std::expected
- std::exception - особенности и паттерны использования
📌 Вебинар подойдёт начинающим C++ разработчикам, а также тем, кто хочет обновить или систематизировать знания.
По итогу вы получите рекомендации по выбору подхода к обработке ошибок в зависимости от проекта.
Начните разбираться в языке правильно - с опытом и поддержкой OTUS.
📲 Успейте зарегистрироваться - количество мест ограничено: https://otus.pw/RiaJ/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
9 383
Недостатки std::make_shared. Кастомный new и delete
#новичкам
В этой небольшой серии будем рассказывать уже о различных ограничениях при работе с std::make_shared.
И начнем с непопулярного.
Внутри себя она создает объект с помощью ::new. Это значит, что если вы для своего класса переопределяете операторы работы с памятью, то make_shared не будет учитывать это поведение, а вы будете гадать, почему не видите нужных спецэффектов:
class A {
public:
void *operator new(size_t) {
std::cout << "allocate\n";
return ::new A();
}
void operator delete(void *a) {
std::cout << "deallocate\n";
::delete static_cast<A *>(a);
}
};
int main() {
const auto a =
std::make_shared<A>(); // ignores overloads
//const auto b =
// std::shared_ptr<A>(new A); // uses overloads
}
// OUTPUT:
// Пусто!
В общем, если нужный кастомный менеджент памяти, то std::make_shared - не ваш бро.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory9 383
Преимущества std::make_shared
#новичкам
Попсовая тема, которая часто спрашивается на собеседованиях. Краем касались ее в других постах, но пусть будет и отдельно, для удобной пересылки.
Коротко о том, что это за функция.
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
Это по сути фабрика для создания шаред поинтеров из параметров конструктора разделяемого объекта. Внутри себя она производит аллокацию памяти и вызов конструктора с помощью переданных аргументов на этой памяти.
В чем же преимущества этой функции по сравнению с явным вызовом конструктора shared_ptr?
Ну для начала, она не предполагает явного использования сырых указателей. Никакого вызова new!
Сами по себе сырые указатели - это неплохо. Просто на душе спокойнее, когда их как можно меньше в современном С++ коде.
Но если new не вызывает программист, это не значит, что функция его не вызывает. Еще как вызывает. И в том, как она это делает кроется главное преимущество std::make_shared над явным вызовом конструктора.
ОООчень упрощенно внутреннее устройство std::shared_ptr выглядит вот так:
template <typename T>
struct shared_ptr {
T * obj_ptr;
ControlBlock * block_ptr;
}
Это два указателя: на сам объект и на контрольный блок, в котором находятся счетчики ссылок и некоторая другая информация.
Память под объекты, на которые указывают эти указатели, обычно выделяется раздельно:
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
Память под объект Foo выделяется при вызове new, а память под контрольный блок выделяется внутри конструктора shared_ptr.
При явном вызове конструктора невозможно по-другому: будет две аллокации.
Но когда make_shared забирает у пользователя возможность самому вызывать конструктор, у нее появляется уникальная возможность: за один раз выделить один большой кусок памяти, в который влезет и объект, и контрольный блок:
template <typename T, typename... Args>
shared_ptr<T> my_make_shared(Args&&... args) {
// Выделяем память для ControlBlock и объекта T одним блоком
char* memory = new char[sizeof(ControlBlock) + sizeof(T)];
// Инициализируем ControlBlock в начале памяти
ControlBlock* block = new (memory) ControlBlock();
// Инициализируем объект T после ControlBlock
T* object = new (memory + sizeof(ControlBlock)) T(std::forward<Args>(args)...); // Placement new
shared_ptr<T> ptr;
ptr.obj_ptr = object;
ptr.block_ptr = block;
return ptr;
Это очень упрощенная реализация, которая показывает главный принцип: выделяется один кусок памяти под два объекта.
Отсюда повышение производительности за счет уменьшения количества аллокаций и за счет большей локальности данных и кеш-френдли структурой.
Ну и на последок.
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
В этой записи два раза повторяется имя класса. В коде могут быть довольно длинные названия сущностей, даже при использовании алиасов. Получается в каком-то смысле явный вызов конструктора приводит к дублированию кода.
Это не происходит с std::make_shared, потому что у нас есть волшебное слово auto:
auto ptr = std::make_shared<Foo>(arg1, arg2);
Есть(было) и еще одно преимущество make_shared. Но его разберем уже отдельно, там ситуация непростая.
А на этом у нас все)
Make better tools. Stay cool.
#cppcore #memory9 383
🚀 YADRO приглашает C++ разработчиков в команду OpenBMC и встроенных систем!
Если вы хотите создавать сложное программное обеспечение для серверов и систем хранения данных, работать с передовыми технологиями Linux и участвовать в проектах open source, то эта возможность для вас.
📌 Кого мы ищем:
• Ведущего разработчика C++ (Linux/OpenBMC)
• Ведущего разработчика интерфейсов встроенных систем
• TeamLead разработки OpenBMC
🧰 Технологический стек и задачи:
• C++ (стандарты 17, 20, 23), STL, Boost
• Linux-среда, systemd, D-Bus, Yocto, bash, Python
• Работа с ядром прошивки OpenBMC, взаимодействие с UEFI/BIOS
• Разработка и поддержка сложных интерфейсов встроенных систем
💼 Условия работы:
• Гибкий формат: удалённо или в офисах в Москве, Санкт-Петербурге, Екатеринбурге, Нижнем Новгороде и Минске
• Работа с масштабными проектами в уникальной команде инженеров
• Возможность горизонтального и вертикального карьерного роста
💙 Узнайте больше и откликайтесь на вакансии прямо на сайте!
9 383
ssize_t
#новичкам
Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.
Зачем такой тип нужен?
В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:
ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void buf, size_t count);Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) { / Обработка ошибки */ }
Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.
Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.
Use the right tool. Stay cool.
#cppcore #goodoldc #NONSTANDARD9 383
Как итерироваться в обратном порядке?
#новичкам
Кто часто решал задачки на литкоде поймут проблему. Есть вектор и надо проитерироваться по нему с конца. Ну пишем:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = vec.size() - 1; i >= 0; --i) {
std::cout << i << ": " << vec[i] << '\n';
}
В чем проблема этого кода?
Бесконечный цикл и ub. auto определяет тип i беззнаковым, который физически не может быть меньше нуля. Происходит переполнение, i становится очень большим и происходит доступ к невалидной памяти.
В большинстве задач можно написать тип int и все будет работать. Но все-таки size() возвращает size_t и будет происходить сужающее преобразование. В реальных проектах нужно избегать этого и сегодня мы посмотрим, как безопасно итерироваться в обратном порядке.
✅ Использовать свободную функцию ssize() из C++20:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = std::ssize(vec) - 1; i >= 0; --i) {
std::cout << vec[i] << '\n';
}
Ее можно применить к вектору и она вернет значение типа std::ptrdiff_t. В первом приблежении это знаковый аналог std::size_t, который позволяет вычислять расстояние между двумя указателями, даже для очень больших массивов.
Так как тип знаковый и в большинстве реализаций его размер сопоставим с size_t, то можно не переживать по поводу возможной срезки длины вектора до меньшего типа.
✅ Использовать обратные итераторы:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto it = std::rbegin(vec); it != std::rend(vec); ++it)
std::cout << *it << '\n';
Тут все довольно очевидно и безопасно.
Однако cppcore гайдлайны говорят нам, что нужно предпочитать использовать range-based-for циклы обычным for'ам. Чтож, давайте пойдем в эту сторону.
✅ Написать свой легковесный адаптер для итерирования в обратном порядке:
template <typename T>
class reverse {
private:
T &iterable_;
public:
explicit reverse(T &iterable) : iterable_{iterable} {}
auto begin() const { return std::rbegin(iterable_); }
auto end() const { return std::rend(iterable_); }
};
std::vector vec{1, 2, 3, 4, 5};
for (const auto &elem : reverse(vec))
std::cout << elem << '\n';
Делаем тонкую обертку над любым итерируемым объектом(в рабочем коде нужно всяких концептов навесить, чтобы было прям по-красоте) и элегантно итерируемся по контейнеру.
✅ А ренджи для кого придумали? Они для этой задачи подходят идеально:
for (const auto& elem : vec | std::views::reverse)
std::cout << elem << '\n';
// или без пайпов
for (const auto& elem : std::ranges::reverse_view(vec))
std::cout << elem << '\n';
Рэнджи из C++20 предоставляют кучу удобных адаптеров для работы с контейнерами. В сущности std::views::reverse или std::ranges::reverse_view делает примерно то же самое, что и мы сами написали в третьем пункте.
Можно совсем упороться и применить алгоритмы ренждей:
std::ranges::copy(vec | std::views::reverse,
std::ostream_iterator<int>( std::cout,"\n" ));
// или c лямбдой
std::ranges::for_each(vec | std::views::reverse,
[](const auto& elem) {
std::cout << elem << '\n';
});
Бывает, что индексы элементов все-таки нужны внутри цикла. Но это решается с помощью std::ranges::iota_view. Оставляем реализацию этого решения для домашних изысканий.
Have a large toolkit. Stay cool.
#cppcore #cpp20 #STL9 383
🔁 Прыжок с C++ на Go за 3 недели. А вы так сможете?
Проект Яндекс Маркета переписан с C++ на Go за 3 недели и теперь обрабатывает 40k запросов в секунду. Что? Да! Команда использовала проверенные методы многозадачности и добавила всю мощь Go для быстрого масштабирования. Погнали читать, как всё было.
Реклама. ООО «ЯНДЕКС», ИНН 7736207543
9 383
Cпецификатор, модификатор, квалификатор и идентификатор
#новичкам
Когда люди учат иностранные языки, то после определенного уровня они начинают изучать идиомы языка, чтобы звучать как нейтив спикеры.
При описании С++ кода тоже можно пользовать определенные словечки, чтобы все понимали, что ты "про". Среди них выделяются
спецификатор, модификатор, квалификатор и идентификатор. Они очень похожи и непонятно, в какой ситуации применять эти слова. Сегодня разрушим эту лингвистическую преграду к высотам артикуляции кода.
Начнем с простого. Идентификатор. Это просто имя, которым "идентифицируется" сущность. Имя переменной, константы, функции, класса, шаблона - это идентификаторы. Такие себе id'шники сущностей.
Спецификатор. Это слово скрывает в себе самое большое разнообразие сущностей. В основном это ключевые слова, уточняющие, что это за сущность:
- Спецификаторы типа. Ключевые слова, использующиеся для определения типа или сущности. class и struct(при объявлении класса указываем что идентификатор является классом), enum, все тривиальные типы(char, bool, short, int, long, signed, unsigned, float, double, void), объявленный прежде имена классов, enum'ов, typedef'ов.
- Спецификаторы объявления. typedef, inline, friend, conetexpr, consteval, constinit, static, mutable, extern, thread_local, final, override.
- Спецификаторы доступа к полям классов: private, protected, public.
Модификатор
Модификатор типа - это ключевое слово, которое изменяет поведение стандартных числовых типов. Модификаторами являются: short, insigned, signed, long. Например, unsigned int - это уже беззнаковый тип, в short int - короткий тип инт, который обычно занимает 16 бит вместо 32.
Это слово редко используется, потому что все модификаторы - это спецификаторы. Так что это вносит только путаницу.
Квалификатор
Существует всего 4 квалификатора. cv-квалификаторы: const и volatile. И ref-квалификаторы: & и &&.
Все. Теперь вы native говоруны и можете speak как про С++ coders.
Know the meaning. Stay cool.
#cppcore9 383
Последний элемент enum
#новичкам
С enum'ами в С++ можно творить разное-безобразное. Можно легко конвертить элементы enum'а в числа и инициализировать их числом. Мы в это сейчас глубоко не будем погружаться, а возьмем базовый сценарий использования. Вам дано перечисление:
enum class Color {
kRed,
kGreen,
kBlue
};
И в каком-то месте программы вам нужно узнать размер этого перечисления. Вопрос: как в коде получить его размер?
В таком варианте, когда элементам enum'а явно не присвоены никакие числа, каждому из них присвоен порядковый номер, начиная с нуля. kRed - 0, kGreen - 1, kBlue - 2.
Соответственно, чтобы получить количество элементов перечисления нужно сделать такую операцию:
auto size = static_cast<int>(Color::kBlue) + 1;
Это работает, но выглядит что-то не очень. Читающий этот код конечно догадывается, что если мы хотим получить размер, то kBlue должен быть последним элементом. Но это вообще никем не гарантируется. Особенно, если в какой-то момент цветов станет больше:
enum class Color {
kRed,
kGreen,
kBlue,
kBlack
};
И все. Код получения размера поломался. И надо везде его исправлять теперь. В общем, подход не расширяемый и требует модификации большого количество кода.
На этот случай есть проверенный прием: заранее вставлять в enum фейковый последний элемент, порядковый номер которого и будет равен размеру перечисления:
enum class Color {
kRed,
kGreen,
kBlue,
kCount
};
auto size = static_cast<int>(Color::kCount);
В этом случае расширять enum нужно приписывая элементы перед kCount. А код получения размера не меняется.
Эта фишка повсеместно используется в реальных проектах, поэтому новичкам полезно будет это знать.
Create extendable solutions. Stay cool.
#goodpractice #cppcore9 383
🧐Слышали о контейнерах в C++, но не уверены, когда и как их правильно использовать?
На открытом уроке «Контейнеры C++» 20 августа в 20:00 МСК мы разберём, как эффективно использовать стандартные и сторонние контейнеры в C++. Мы рассмотрим популярные STL-контейнеры — std::vector, std::list, std::deque, а также контейнеры-адаптеры и библиотеки сторонних разработчиков, такие как folly, boost и libcuckoo. Поймём, в каких случаях использовать каждый из них, чтобы повысить производительность и улучшить архитектуру программ.
Вы получите конкретные знания, которые можно сразу применить в реальных проектах, и сможете выбирать оптимальные решения для работы с данными в C++.
⚡️Регистрируйтесь на вебинар и получите скидку на курс «C++ Developer. Professional»: https://otus.pw/OlBI/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
9 383
#include <filename> vs #include "filename"
#новичкам
Тот нюанс, который зеленые программисты С++ впитывают из окружающей среды, но зачастую не понимают его деталей.
Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?
Стандарт нам дает отписку, что поведение при обоих подходах implementation defined, поэтому надо смотреть на то, как ведут себя большинство компиляторов.
Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.
У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.
Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).
А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.
В конце концов, обычно, в обоих случаях все известные компилятору директории будут просмотрены на наличие подходящего файла, пока он не будет найден. Вопрос только в порядке поиска.
Так что в большинстве случаев разницы особо никакой не будет. Однако все равно есть определенные гайдлайны, которым следуют большинство разработчиков.
✅ Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.
#include <stdio.h> // Стандартный заголовочник
#include <curl/curl.h> // Заголовочник из системной директории
int main(void) {
CURL *curl = curl_easy_init();
if(curl) {
printf("libcurl version: %s\n", curl_version());
curl_easy_cleanup(curl);
}
return 0;
}
✅ Используем #include "filename" для включения заголовочных файлов своего проекта. Препроцессор будет сначала искать эти файлы там, где вы ему об этом явно укажите с помощью опций.
// include/mylib.hpp - объявляем функцию из нашего проекта
#pragma once
void print_hello();
// src/main.cpp - используем локальный заголовочник через " "
#include <iostream> // Системный заголовочник
#include "mylib.hpp" // Заголовочник локального проекта, ищем в указанных путях
int main() {
print_hello();
return 0;
}
void print_hello() {
std::cout << "Hello from my project!\n";
}
// компиляция: g++ -Iinclude/ src/main.cpp -o my_program -std=c++17
See the difference. Stay cool.
#cppcore #compiler9 383
or and not
#новичкам
В С/C++ всегда был не очень дружелюбный синтаксис операторов. Показать вот такой код человеку, который не в зуб ногой в программировании:
if (x > 0 && y < 10 || !z) {
// ...
}
есть вероятность, что он подумает, что его прокляли шаманы тумба-юмба.
Однако знали ли вы, что в С/C++ есть альтернативный синтаксис токенов? Символы операторов заменяются на короткие слова и код выше становится почти питонячьим:
if (x > 0 and y < 10 or not z) {
// ...
}
Выглядит свежо! Хотя было доступно еще с С++98.
Вот полный список альтернативных токенов:
&& - and
&= - and eq
& - bitand
| - bitor
~ - compl
! - not eq
| - or
|= - or eq
^ - xor
^= - xor eq
{ - <%
} - %>
[ - <:
] - :>
# - %:
## - %:%:
Последние токены для скобок - это конечно дичь. Но остальные - вполне интересные варианты.
В сях альтернативные токены были введены в С95, поэтому до этого момента токенов не было в языке. Но даже с их введением все продолжали использовать привычный синтаксис. Видимо поэтому мы так до сих пор и остались на уровне наскальной живописи.
А вы используете в продакшен коде альтернативные токены?
Evolve. Stay cool.
#fun #goodoldc9 383
В России можно посещать IT-мероприятия хоть каждый день: как оффлайн, так и онлайн
Но где их находить? Как узнавать о них раньше, чем когда все начнут выкладывать фотографии оттуда?
Переходите на канал IT-Мероприятия России. В нём каждый день анонсируются мероприятия со всех городов России
📆 в канале размещаются как онлайн, так и оффлайн мероприятия;
👩💻 можно найти ивенты по любому стеку: программирование, frontend-backend разработка, кибербезопасность, дата-аналитика, osint, devops и другие;
🎙 разнообразные форматы мероприятий: митапы с коллегами по цеху, конференции и вебинары с известными опытными специалистами, форумы и олимпиады от важных представителей индустрии и многое другое
А чтобы не искать по разным форумам и чатам новости о предстоящих ивентах:
🚀 IT-мероприятия России — подписывайся и будь в курсе всех предстоящих мероприятий!
9 383
Разбор ревью
#новичкам
Большое спасибо всем участникам ревью, которые проявили активность под предыщущим постом. Всем и каждому посылаем лучи благодарности!
Было непросто выбрать самый эффективный по критике комментарий, потому что некоторые люди предлагали странные решения. В итоге, мы выбрали @seweeex и вот его коммент. Давайте похлопаем ему 👏👏👏.
Теперь к сути. В этом коде не так уж и много проблем, просто они жирные и явно бросаются в глаза.
Поехали разбирать.
🔞 Зачем-то в очереди хранятся сырые указатели. Смысла в этом особого нет, кроме как подложить себе свинью на будущее и поджечь жёпы комментаторов. Тут даже умные указатели не нужны, зачем дополнительные аллокации? В очереди можно хранить сами объекты и никаких проблем с менеджментом памяти не будет.
🔞 Использование сырых указателей приводит например к тому, что при вылете исключения из метода push, произойдет утечка памяти. Да, элементов мы закидываем в очередь немного, но концептуально проблема есть. Решается это опять же через хранение обычных объектов.
🔞 Слон в посудной лавке здесь - это конечно отсутствие синхронизации на очереди. Это в принципе ub и дальше не о чем говорить. Нужна не стандартная, а потокобезопасная очередь.
Очередь должна быть блокирующей, чтобы не тратить активно ресурс CPU на ожидание нового сообщения. Это решается с помощью кондвара.
🔞 Ресивер может зашедулиться раньше сендера, увидит пустую очередь и выйдет из цикла, не обработав ни одной задачи. Поэтому нужна система сигнализации: очередь должна ждать прихода новых задач, пока ей не скажут, что больше задач нет.
🔞 Бесконечный цикл в ресивере выглядит не очень. Если можно не писать бесконечных циклов и не вставлять брейки, то лучше этого не делать. break и continue усложняют понимание кода. Лучше перенести забирание элемента из очереди прям в шапку цикла.
🔞 Гонка на потоконебезопасном логировании. Нужен мьютекс, чтобы сообщения не интерферировали.
По сути все. Главное изменение - вынесение блокирующей потокобезопасной очереди в отдельный шаблонный класс, который хранит объекты типа Т. С шаблонами можно долго играться и далеко зайти, поэтому приведем самую простую реализацию, которая справляется со своими задачами в данном кейсе, но может быть улучшена для корректной работы с самыми разными типами:
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <optional>
#include <print>
struct Message {
int data;
};
template<typename T>
class ThreadSafeQueue {
public:
void push(T msg) {
{
std::lock_guard lock(mutex_);
queue_.push(std::move(msg));
}
cv_.notify_one();
}
std::optional<T> pop() {
std::unique_lock lock(mutex_);
cv_.wait(lock, [this] { return !queue_.empty() || stopped_; });
if (stopped_ && queue_.empty()) {
return std::nullopt;
}
auto msg = std::move(queue_.front());
queue_.pop();
return msg;
}
void stop_receive() {
stopped_ = true;
cv_.notify_all();
}
private:
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cv_;
std::atomic<bool> stopped_ = false;
};
void println(const std::string& str) {
static std::mutex mtx;
std::lock_guard lock(mtx);
std::cout << str << std::endl;
}
void sender(ThreadSafeQueue<Message>& msgQueue) {
for (int i = 0; i < 20; ++i) {
auto msg = Message(i);
println("Sent: " + std::to_string(msg.data));
msgQueue.push(std::move(msg));
}
msgQueue.stop_receive();
}
void receiver(ThreadSafeQueue<Message>& msgQueue) {
while (auto msg = msgQueue.pop()) {
println("Received: " + std::to_string(msg.value().data));
}
}
int main() {
ThreadSafeQueue<Message> msgQueue;
std::thread t1(sender, std::ref(msgQueue));
std::thread t2(receiver, std::ref(msgQueue));
t1.join();
t2.join();
}
Пишите свои дополнения, если что-то забыли.
Make things better. Stay cool.9 383
Ревью
#новичкам
Пролистывал намедни один тг канал по С++, его название начинается на Senior и заканчивается на С++ Developer. Там обычно постится очень "качественный контент" и мне на глаза попался код, который я бы хотел закинуть вам на тряпкозакидательство.
В рамках #ревью мы приводим кусок кода, а вы в комментариях прожариваете его до полного well done: говорите, что вам не нравится, и как это исправить. Комментатора с самым большим количеством отмеченных проблем упомянем в завтрашнем посте с разбором.
struct Message {
int data;
};
std::queue<Message *> msgQueue;
void sender() {
for (int i = 0; i < 20; ++i) {
Message *msg = new Message();
msg->data = i;
msgQueue.push(msg);
std::cout << "Sent: " << msg->data << std::endl;
}
}
void receiver() {
while (true) {
if (msgQueue.empty()) {
break;
}
Message *msg = msgQueue.front();
msgQueue.pop();
std::cout << "Received: " << msg->data << std::endl;
delete msg;
}
}
int main() {
std::thread t1(sender);
std::thread t2(receiver);
t1.join();
t2.join();
return 0;
}
Вот такой код. Под оригинальным постом с этим кодом коллеги призвали руки рубить за такой код. Давайте сделаем так, чтобы он не вызывал испанского стыда, а только возвышенные чувства платонической любви к С++.
Раз, два, три, код в порядок приведи!
Critique your decisions. Stay cool.9 383
Офер в Яндекс за 48 часов: ищем бэкендеров
В команду нужны опытные бэкенд-разработчики на C++, Python, Java и Go. Приглашаем на Мультитрек — онлайн-программу быстрой адаптации.
Всего за 2 дня вы можете получить офер:
• До 18 августа подать заявку и пройти предварительный отбор
• 23 августа решить задачи на технических секциях
• 24 августа пройти финальное собеседование и получить офер
После этого будет возможность поработать с тремя командами и выбрать проект по душе.
Создаём технологии, которые меняют мир. Присоединяйтесь! Оставляйте заявку на сайте.
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
