Обзор книжки #1
Сегодня довольно необычный пост. Много людей любят читать книжки и часто спрашивают, что же им такого почитать, чтобы преисполниться познанием. Прислушиваемся к болям людей. И поэтому мы будем иногда делать разборы на клевые книжки по плюсовой тематике.
Но начнем не с базовых книжек. Есть один очень крутой фундаментальный труд по многопоточке в плюсах - "С++. Практика многопоточного программирования" Энтони Уильямса. Та самая знаменитая "Concurrency in Action".
Для большинства новичков(да и для опытных тоже, что уж скрывать) многопоточка - это какой-то отдельный мир. Опасный и неизведанный мир. И они начинают пробираться сквозь густые заросли мьютексов, атомиков и потоков совсем одни, вооружившись какими-то огрызками видосов и статей.
Нужен нормальный, опытный гид, который сможет провести за ручку по всем ухабам, обучить человека всем азам и вооружить мощными практиками, чтобы он дальше сам мог жить в этом конкурентном мире.
Так вот Энтони Уильямс - как раз такой гид. Все в книге разложено четко и выверено.
1️⃣ Сначала погружаемся в саму концепцию параллелизма и многозадачности. Понимаем ее преимущества, способы организации и требования. И наконец-то понимаем разницу между потоками и процессами.
2️⃣ Изучаем самый простой механизм обеспечения безопасности - мьютекс.
3️⃣ Изучаем более сложные инструменты синхронизации - кондвары, фьючи, промисы.
4️⃣ Только после этого всего, когда вы уже наловчились думать в многопотоке и выявлять потенциальные проблемы, он переходит к модели памяти и атомикам. Тут конечно, все не просто и эта тема очень сложна для понимания. Возможно придется перечитывать несколько раз.
После этого можно сказать, что вы знаете про все инструменты из threading STL. И вас можно отпускать. Но нет!
Знание инструментов не равно способности их применять. И вот остальная часть книжки посвящена проектированию потокобезопасных структур данных, лок-фри структур данных и, в целом, проектированию многопоточных систем. Там вы научитесь именно что применять инструменты, которые вы выучили.
Хотите знать, что делать с проблемами обработки исключений в многопотоке? А с нюансами проектирования интерфейса потокобезопасных структур данных? Вот такие тонкости там разбираются.
Кому подойдет эта книга? Да вообще всем. Вкатунам в многопоточку, чтобы просто уметь читать и понимать этот код. Даже не обязательно всю книгу читать. Джунам, которые хотят нарабатывать уже практические навыки и что-то писать самим. Миддлам и сеньорам - для углубления своих знаний и познания тех самых деталей, в которых дьявол скрывается.
Я сам начинал познание многопоточки именно с нее. Тогда была версия только для 14-х плюсов(сейчас уже для 17-х). Есть такие книги, которые читаешь снова и снова. И с каждым прочтением переосмысливаешь свои знания и по-другому смотришь на мир. Вот это одна из таких книжек. Но вот реально, каждый раз читаю и что-то новое узнаю. Моя любимая часть - модель памяти, лок-фри программы. Там всегда взрыв мозга и новые инсайты)
Знаю, знаю. У вас в голове уже вопрос: "Куда деньги нести? Дайте мне уже это золото!"
Не спешите. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу разыграть его среди подписчиков.
Все, что вам надо сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом слово "Конкурс". Повторные комментарии будут удаляться.
Знаю, что много наших читателей читают посты не сразу, а скопом раз в недельку. Поэтому шанс залететь у всех будет еще ровно 7 дней, начиная с этого момента. На 8 день выйдет пост с результатами.
Победителя естественно выберем рандомайзером.
Если вы уже читали эту книжку, то оставьте свои впечатления о ней в комментах.
Be lucky. Stay cool.
#books
Сегодня довольно необычный пост. Много людей любят читать книжки и часто спрашивают, что же им такого почитать, чтобы преисполниться познанием. Прислушиваемся к болям людей. И поэтому мы будем иногда делать разборы на клевые книжки по плюсовой тематике.
Но начнем не с базовых книжек. Есть один очень крутой фундаментальный труд по многопоточке в плюсах - "С++. Практика многопоточного программирования" Энтони Уильямса. Та самая знаменитая "Concurrency in Action".
Для большинства новичков(да и для опытных тоже, что уж скрывать) многопоточка - это какой-то отдельный мир. Опасный и неизведанный мир. И они начинают пробираться сквозь густые заросли мьютексов, атомиков и потоков совсем одни, вооружившись какими-то огрызками видосов и статей.
Нужен нормальный, опытный гид, который сможет провести за ручку по всем ухабам, обучить человека всем азам и вооружить мощными практиками, чтобы он дальше сам мог жить в этом конкурентном мире.
Так вот Энтони Уильямс - как раз такой гид. Все в книге разложено четко и выверено.
1️⃣ Сначала погружаемся в саму концепцию параллелизма и многозадачности. Понимаем ее преимущества, способы организации и требования. И наконец-то понимаем разницу между потоками и процессами.
2️⃣ Изучаем самый простой механизм обеспечения безопасности - мьютекс.
3️⃣ Изучаем более сложные инструменты синхронизации - кондвары, фьючи, промисы.
4️⃣ Только после этого всего, когда вы уже наловчились думать в многопотоке и выявлять потенциальные проблемы, он переходит к модели памяти и атомикам. Тут конечно, все не просто и эта тема очень сложна для понимания. Возможно придется перечитывать несколько раз.
После этого можно сказать, что вы знаете про все инструменты из threading STL. И вас можно отпускать. Но нет!
Знание инструментов не равно способности их применять. И вот остальная часть книжки посвящена проектированию потокобезопасных структур данных, лок-фри структур данных и, в целом, проектированию многопоточных систем. Там вы научитесь именно что применять инструменты, которые вы выучили.
Хотите знать, что делать с проблемами обработки исключений в многопотоке? А с нюансами проектирования интерфейса потокобезопасных структур данных? Вот такие тонкости там разбираются.
Кому подойдет эта книга? Да вообще всем. Вкатунам в многопоточку, чтобы просто уметь читать и понимать этот код. Даже не обязательно всю книгу читать. Джунам, которые хотят нарабатывать уже практические навыки и что-то писать самим. Миддлам и сеньорам - для углубления своих знаний и познания тех самых деталей, в которых дьявол скрывается.
Я сам начинал познание многопоточки именно с нее. Тогда была версия только для 14-х плюсов(сейчас уже для 17-х). Есть такие книги, которые читаешь снова и снова. И с каждым прочтением переосмысливаешь свои знания и по-другому смотришь на мир. Вот это одна из таких книжек. Но вот реально, каждый раз читаю и что-то новое узнаю. Моя любимая часть - модель памяти, лок-фри программы. Там всегда взрыв мозга и новые инсайты)
Знаю, знаю. У вас в голове уже вопрос: "Куда деньги нести? Дайте мне уже это золото!"
Не спешите. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу разыграть его среди подписчиков.
Все, что вам надо сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом слово "Конкурс". Повторные комментарии будут удаляться.
Знаю, что много наших читателей читают посты не сразу, а скопом раз в недельку. Поэтому шанс залететь у всех будет еще ровно 7 дней, начиная с этого момента. На 8 день выйдет пост с результатами.
Победителя естественно выберем рандомайзером.
Если вы уже читали эту книжку, то оставьте свои впечатления о ней в комментах.
Be lucky. Stay cool.
#books
Telegram
Грокаем книги или TL;DR
Здесь мы говорим о:
- Новинках компьютерной литературы
- Новостях издательства "Питер"
- Котиках. В Питере любят котиков
- Мемах
Обустраивайся, будет интересно!
- Новинках компьютерной литературы
- Новостях издательства "Питер"
- Котиках. В Питере любят котиков
- Мемах
Обустраивайся, будет интересно!
🔥36👍13❤10🤩3👎1😁1
Мапа и оператор[]
#новичкам
Не так редко можно увидеть код примерно такого вида:
Типа если в мапе нет ключа, то создаем там объект со значением 1, если есть, то просто инкрементируем значение.
Не знаю, почему многие боятся написать просто вот так:
Наверное не знают пары секретиков. Сегодня я о них поведаю.
Первый - оператор[] для мапы создает объект, если его не было в мапе. То есть неважно, был ли ключ в мапе или нет, вы можете вызвать operator[] и ничего плохого не произойдет. Просто в словаре будет еще один ключ, если до этого не было.
Посмотрим на пример:
Простая оберточка, чтобы показать все наглядно. Создаем пустую мапу и инкрементируем значение по ключу, которого в ней нет. И выводим значение после инкремента.
Вывод:
То есть для значения нового элемента мапы вызывается конструктор по умолчанию. Объект создается налету.
Одно беспокойство убрали.
"Но тут в примере в дефолтном конструкторе вы явно инициализируете поле i в ноль. Почему реальное интовое значение в мапе будет выставляться в ноль?"
Да. Потому что гарантируется value initialization при создания объектов в ассоциативных контейнерах.
Value инициализация для интовой переменной может выглядеть вот так:
Эффект value initialization на тривиальных типах - они инициализируются нулем.
Вывод:
Этих двух "секретных знаний" достаточно, чтобы не бояться изменять значения в словарях даже если желаемого ключа в нем нет.
Know secrets. Stay cool.
#cppcore #STL
#новичкам
Не так редко можно увидеть код примерно такого вида:
if (map_.count(token)) {
map[token]++;
} else {
map[token] = 1;
}Типа если в мапе нет ключа, то создаем там объект со значением 1, если есть, то просто инкрементируем значение.
Не знаю, почему многие боятся написать просто вот так:
map[token]++;
Наверное не знают пары секретиков. Сегодня я о них поведаю.
Первый - оператор[] для мапы создает объект, если его не было в мапе. То есть неважно, был ли ключ в мапе или нет, вы можете вызвать operator[] и ничего плохого не произойдет. Просто в словаре будет еще один ключ, если до этого не было.
Посмотрим на пример:
struct CLASS {
CLASS() {std::cout << "default" << std::endl; i = 0;}
void operator++(int) {std::cout << "increment" << std::endl; i++; }
int i;
};
int main() {
std::map<std::string, CLASS> map;
map["qwe"]++;
std::cout << map["qwe"].i << std::endl;
}Простая оберточка, чтобы показать все наглядно. Создаем пустую мапу и инкрементируем значение по ключу, которого в ней нет. И выводим значение после инкремента.
Вывод:
default
increment
1
То есть для значения нового элемента мапы вызывается конструктор по умолчанию. Объект создается налету.
Одно беспокойство убрали.
"Но тут в примере в дефолтном конструкторе вы явно инициализируете поле i в ноль. Почему реальное интовое значение в мапе будет выставляться в ноль?"
Да. Потому что гарантируется value initialization при создания объектов в ассоциативных контейнерах.
Value инициализация для интовой переменной может выглядеть вот так:
int();
int{};
int object{};
new int();
new int{};
Эффект value initialization на тривиальных типах - они инициализируются нулем.
std::cout << int() << std::endl;
std::cout << int{} << std::endl;
int object{};
std::cout << object << std::endl;
std::cout << *(new int()) << std::endl;
std::cout << *(new int{}) << std::endl;
Вывод:
0
0
0
0
0
Этих двух "секретных знаний" достаточно, чтобы не бояться изменять значения в словарях даже если желаемого ключа в нем нет.
Know secrets. Stay cool.
#cppcore #STL
🔥37👍24❤6🎉3🤯2❤🔥1
Наследование? От лямбды? Ч1
#опытным
Наследованием в языке С++ никого не удивить. Все постоянно его видят в коде и используют. Но что, если я вам скажу, что вы можете наследоваться от лямбды! Как? Давайте разбираться.
Как это вообще возможно?
Ну это не удивительно для тех, кто знает, чем на самом деле являются лямбды. Это по сути своей классы, чей тип знает только сам компилятор, с перегруженным оператором(). То, что ее называют безымянной функцией, это не совсем правда. Все у нее есть.
То есть это класс, у которого есть вполне конкретный метод и даже поля(тут зависит от того, что захватила лямбда).
Значит это вполне легальный кандидат на наследование!
Придется конечно немного поколдовать вокруг отсутствия имени, но для С++ это не проблема.
Ничего особенного. Мы просто создали класс-обертку над каким-то функциональным объектом и используем его оператор(), как свой.
Дальше создаем лямбду и создаем объект обертки, просто передавая лямбду в конструктор. Мы специально не указываем явно шаблонный параметр DerivedFromLambda, потому что мы не знаем настоящего имени лямбды. Мы даем возможность компилятору самому вывести нужный шаблонный тип на основании инициализатора. Это возможно благодаря фиче С++17 Class Template Argument Deduction.
Но даже и на С++11-14 можно написать подобное. Ведь у нас есть оператор decltype, который возвращает в точности тип того выражения, которое мы в него передали. Тогда мы бы создавали объект так:
Зачем это нужно только? К этому мы будем потихоньку подбираться следующие пару постов.
Do surprising things. Stay cool.
#template #cppcore #cpp11 #cpp17
#опытным
Наследованием в языке С++ никого не удивить. Все постоянно его видят в коде и используют. Но что, если я вам скажу, что вы можете наследоваться от лямбды! Как? Давайте разбираться.
Как это вообще возможно?
Ну это не удивительно для тех, кто знает, чем на самом деле являются лямбды. Это по сути своей классы, чей тип знает только сам компилятор, с перегруженным оператором(). То, что ее называют безымянной функцией, это не совсем правда. Все у нее есть.
То есть это класс, у которого есть вполне конкретный метод и даже поля(тут зависит от того, что захватила лямбда).
Значит это вполне легальный кандидат на наследование!
Придется конечно немного поколдовать вокруг отсутствия имени, но для С++ это не проблема.
template<class Lambda>
struct DerivedFromLambda : public Lambda
{
DerivedFromLambda(Lambda lambda) : Lambda(std::move(lambda)) {}
using Lambda::operator();
};
int main(){
auto lambda = []{return 42;};
DerivedFromLambda child{lambda};
std::cout << child() << std::endl;
}
// OUTPUT:
// 42
Ничего особенного. Мы просто создали класс-обертку над каким-то функциональным объектом и используем его оператор(), как свой.
Дальше создаем лямбду и создаем объект обертки, просто передавая лямбду в конструктор. Мы специально не указываем явно шаблонный параметр DerivedFromLambda, потому что мы не знаем настоящего имени лямбды. Мы даем возможность компилятору самому вывести нужный шаблонный тип на основании инициализатора. Это возможно благодаря фиче С++17 Class Template Argument Deduction.
Но даже и на С++11-14 можно написать подобное. Ведь у нас есть оператор decltype, который возвращает в точности тип того выражения, которое мы в него передали. Тогда мы бы создавали объект так:
auto lambda = []{return 42;};
DerivedFromLambda<decltype(lambda)> child{lambda};Зачем это нужно только? К этому мы будем потихоньку подбираться следующие пару постов.
Do surprising things. Stay cool.
#template #cppcore #cpp11 #cpp17
👍30❤21🔥12🤯10💯2
Наследование? От лямбды? Ч2
#опытным
Лямбды - это функциональные объекты по своей сути. Объекты классов с перегруженным оператором(). Зачем вообще от такой сущности наследоваться? Можно же просто сделать обычный класс с такими же перегруженным оператором и расширять его сколько влезет.
Ну как будто бы да. От одной лямбды наследоваться особо нет смысла. А что насчет множественного наследования?
Идейно - наш наследник будет наследовать публичный интерфейс всех своих родителей. То есть в наследнике будет много перегруженных операторов() для разных входных параметров. Вот это уже чуть удобнее. Мы можем находу создавать объект, который единообразно с помощью перегрузок будет обрабатывать какие-то вещи.
Покажу чуть подробнее:
Логика и механика те же, что и в прошлом посте. Только теперь мы налету конструируем объект, который умеет в 2 перегрузки функции. Если эти перегрузки действительно небольшие, то не особо понятно, зачем мне определять их как отдельные перегрузки отдельной функции. Это нужно будет их потом по коду искать. А тут все рядышком и смотреть приятно.
Только не наследуйтесь от нескольких лямбд, которые принимают одинаковый набор параметров. Компилятор не сможет разрезолвить, от какого конкретно родителя вы хотите вызвать перегрузку и билд упадет.
Дальние ряды уже начали догадываться зачем такая конструкция реально может быть нужна. Но все объяснения в следующий раз.
Have a sense. Stay cool.
#template #cppcore #cpp17
#опытным
Лямбды - это функциональные объекты по своей сути. Объекты классов с перегруженным оператором(). Зачем вообще от такой сущности наследоваться? Можно же просто сделать обычный класс с такими же перегруженным оператором и расширять его сколько влезет.
Ну как будто бы да. От одной лямбды наследоваться особо нет смысла. А что насчет множественного наследования?
Идейно - наш наследник будет наследовать публичный интерфейс всех своих родителей. То есть в наследнике будет много перегруженных операторов() для разных входных параметров. Вот это уже чуть удобнее. Мы можем находу создавать объект, который единообразно с помощью перегрузок будет обрабатывать какие-то вещи.
Покажу чуть подробнее:
template<class Lambda1, class Lambda2>
struct DerivedFromLambdas : public Lambda1, Lambda2
{
DerivedFromLambdas(Lambda1 lambda1, Lambda2 lambda2)
: Lambda1(std::move(lambda1))
, Lambda2{std::move(lambda2)} {}
using Lambda1::operator();
using Lambda2::operator();
};
int main(){
DerivedFromLambdas child{[](int i){return "takes int";}, [](double d){return "takes double";}};
std::cout << child(42) << std::endl;
std::cout << child(42.0) << std::endl;
return 0;
}
// OUTPUT:
// takes int
// takes double
Логика и механика те же, что и в прошлом посте. Только теперь мы налету конструируем объект, который умеет в 2 перегрузки функции. Если эти перегрузки действительно небольшие, то не особо понятно, зачем мне определять их как отдельные перегрузки отдельной функции. Это нужно будет их потом по коду искать. А тут все рядышком и смотреть приятно.
Только не наследуйтесь от нескольких лямбд, которые принимают одинаковый набор параметров. Компилятор не сможет разрезолвить, от какого конкретно родителя вы хотите вызвать перегрузку и билд упадет.
Дальние ряды уже начали догадываться зачем такая конструкция реально может быть нужна. Но все объяснения в следующий раз.
Have a sense. Stay cool.
#template #cppcore #cpp17
🔥31👍10❤6🤯3❤🔥1⚡1
Наследование? От лямбды? Ч3
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
template<typename ... Lambdas>
struct DerivedFromLambdas : Lambdas...
{
DerivedFromLambdas(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{
}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
int main(){
std::vector<var_t> vec = {10, 1.5, "hello"};
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
10 1.500000 "hello"
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
❤22👍14🔥9🤯3⚡2👏1
Итоги конкурса
Мы долго ждали и, наконец, дождались. Вчера я честно взял генератор случайных чисел и нашел победителя и будущего счастливого обладателя книжки "С++. Практика многопоточного программирования" Энтони Уильямса. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете моей непредвзятости)
Но перед оглашение результатов хочу поблагодарить всех участников этого экспериментального формата. Спасибо вам, что поддержали инициативу! Так много новых лиц увидел в комментах. Будем надеяться, что все больше людей будет присоединяться к коммьюнити и делиться свои профессиональным опытом.
Ну а победителем стал Илья Морозов давайте похлопаем ему👏👏👏. Илья, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.
Будем работать, чтобы таких розыгрышей было больше. Поэтому мягко напоминаю, что у Питера самый качественный перевод зарубежных книг. А там сами все найдете.
Be lucky. Stay cool.
Мы долго ждали и, наконец, дождались. Вчера я честно взял генератор случайных чисел и нашел победителя и будущего счастливого обладателя книжки "С++. Практика многопоточного программирования" Энтони Уильямса. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете моей непредвзятости)
Но перед оглашение результатов хочу поблагодарить всех участников этого экспериментального формата. Спасибо вам, что поддержали инициативу! Так много новых лиц увидел в комментах. Будем надеяться, что все больше людей будет присоединяться к коммьюнити и делиться свои профессиональным опытом.
Ну а победителем стал Илья Морозов давайте похлопаем ему👏👏👏. Илья, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.
Будем работать, чтобы таких розыгрышей было больше. Поэтому мягко напоминаю, что у Питера самый качественный перевод зарубежных книг. А там сами все найдете.
Be lucky. Stay cool.
👏65🔥9🎉7❤3😭2🤔1
Считаем единички
#задачки
Меня всегда забавляли задачки на бинарное представление чисел. Всегда можно найти занимательные и новые для себя подходы.
Вроде бы простая и популярная задача: посчитать количество единиц в битовом представлении числа. Уверен, что большинство из вас решали эту задачу.
Однако популярный подход - не самый эффективный, элегантный и интересный.
Благо существует множество непопулярных, но очень интересных решений! О них я расскажу в завтра в ответном посте.
А пока предлагаю вам порешать эту задачку. Если вы знаете какие-то нетривиальные решения, то расскажите о них в комментах тоже.
А чтобы умудренным опытом людям было немного интереснее, давайте ограничим условия. Задачу надо решить либо за константное время(в среднем), либо за наименьшее количество строчек. Выражения вне цикла for разделенные символом
Challenge yourself. Stay cool.
#задачки
Меня всегда забавляли задачки на бинарное представление чисел. Всегда можно найти занимательные и новые для себя подходы.
Вроде бы простая и популярная задача: посчитать количество единиц в битовом представлении числа. Уверен, что большинство из вас решали эту задачу.
Однако популярный подход - не самый эффективный, элегантный и интересный.
Благо существует множество непопулярных, но очень интересных решений! О них я расскажу в завтра в ответном посте.
А пока предлагаю вам порешать эту задачку. Если вы знаете какие-то нетривиальные решения, то расскажите о них в комментах тоже.
А чтобы умудренным опытом людям было немного интереснее, давайте ограничим условия. Задачу надо решить либо за константное время(в среднем), либо за наименьшее количество строчек. Выражения вне цикла for разделенные символом
; считаются разными строчками.int count_ones(unsigned num) {
// Here your code
}Challenge yourself. Stay cool.
😁29❤19👍9🔥3⚡2
Считаем единички. Решения
Давайте быстро пробежимся через самое банальное решение. Нужно в цикле проверять по маске последний бит числа и сдвигать его вправо, пока число не превратится в ноль.
Алгоритмическая сложность этого решения - О(log(num)).
Что может быть интереснее? Например, знали ли вы, что выражение num & (num - 1) лишает число его самой правой единички? Посмотрите сами:
Поэтому в цикле, вместо сдвига числа вправо можно просто бинарно умножать число на это же число, уменьшенное на единицу. Даже считать отдельно ничего не нужно, количество итераций цикла определять число единичек. Это кстати в среднем в 2 раза эффективнее, чем просто каждый раз смотреть последний бит числа, но ассимптотическую сложность не меняет. Ну и для любителей кода покороче, все это можно написать так:
А что насчет самого короткого решения? Зачем писать велосипед, если можно просто воспользоваться встроенной функцией компилятора(или С++20 фичей):
А что, если мы хотим константную сложность? Такое вообще возможно?
Конечно. Нам потребуется всего sizeof(num)* 8 итераций цикла и проверки последнего бита, чтобы найти нужное число. Константа? Да. Эффективно ли это? Это даже медленнее, чем самое первое решение.
Однако давайте подумаем еще чуть-чуть. Комбинаций битов в инте на самом деле не такой уж и и много. Всего 2^32. Можно создать массив байтов на 2^32 элементов и в каждой ячейке хранить количество единичек для числа равного индексу этой ячейки. Мы это как-то можем заранее нагенерить(или при первом вызове функции) и потом все вызовы функции count_ones будут занимать константное время. Правда памяти сожрется на это предостаточно.
Кстати полезный ход. Иногда из-за сильных ограничений по входным данным задачи ее можно решить намного более оптимальным способом.
Если боитесь больших массивов, то можно немного схитрить. Мы можем запомнить в таблице количество единиц для каждого возможного байта, разбить число на 4 части, найти для этих частей количество хранящих в них единичек по таблице и сложить это дело. Получится, что нужно всего 256 байт доп памяти и 4 итерации цикла.
Но чтобы было прям наглядно понятна логика, то массив можно сделать еще меньше, если брать по 4 бита(тетрад). Различных тетрадов всего 16 штук, поэтому и нужно будет всего 16 байт доп памяти и 8 итераций цикла. Спасибо, @tutralex, за решение)
В общем, вы поняли. Чем меньше массив, тем больше итераций цикла и наоборот. Выбирайте, что вам больше подходит.
Если вы еще не устали, то у меня для вас есть banger. Нахождение количества единичных бит в числе - это не просто задача с литкода. У нее есть практическое применение. Есть такая штука, как расстояние Хемминга для двоичных чисел. Это количество символов, которое отличает данную строку битов от строки, заполненной нулями. То есть это и есть наше количество единичек. Эта штука используется во многих дисциплинах, в том числе и криптографии. Не удивительно, что много народу совершенствовало решение этой задачи. На мой взгляд, самое мозгодробительное решение выглядит примерно так:
Давайте быстро пробежимся через самое банальное решение. Нужно в цикле проверять по маске последний бит числа и сдвигать его вправо, пока число не превратится в ноль.
int count_ones(unsigned num) {
int result = 0;
while(num > 0) {
result += num & 1;
num >>= 1;
}
return result;
}Алгоритмическая сложность этого решения - О(log(num)).
Что может быть интереснее? Например, знали ли вы, что выражение num & (num - 1) лишает число его самой правой единички? Посмотрите сами:
10 = 1010
1010 & (1010 - 1) = 1010 & 1001 = 1000
118 = 111011
111011 & (111011 - 1) = 111011 & 111010 = 111010
Поэтому в цикле, вместо сдвига числа вправо можно просто бинарно умножать число на это же число, уменьшенное на единицу. Даже считать отдельно ничего не нужно, количество итераций цикла определять число единичек. Это кстати в среднем в 2 раза эффективнее, чем просто каждый раз смотреть последний бит числа, но ассимптотическую сложность не меняет. Ну и для любителей кода покороче, все это можно написать так:
int count_ones(unsigned num) {
int result = 0;
for(; num > 0; num &= (num - 1), ++result);
return result;
}А что насчет самого короткого решения? Зачем писать велосипед, если можно просто воспользоваться встроенной функцией компилятора(или С++20 фичей):
int count_ones(unsigned num) {
return std::popcount(num);
// or compiler extension
// return __builtin_popcount(num);
}А что, если мы хотим константную сложность? Такое вообще возможно?
Конечно. Нам потребуется всего sizeof(num)* 8 итераций цикла и проверки последнего бита, чтобы найти нужное число. Константа? Да. Эффективно ли это? Это даже медленнее, чем самое первое решение.
Однако давайте подумаем еще чуть-чуть. Комбинаций битов в инте на самом деле не такой уж и и много. Всего 2^32. Можно создать массив байтов на 2^32 элементов и в каждой ячейке хранить количество единичек для числа равного индексу этой ячейки. Мы это как-то можем заранее нагенерить(или при первом вызове функции) и потом все вызовы функции count_ones будут занимать константное время. Правда памяти сожрется на это предостаточно.
static std::array<uint8_t, std::numeric_limit<uint32_t>::max()> ones;
// somehow fill array
int count_ones(unsigned num) {
return ones[num];
}
Кстати полезный ход. Иногда из-за сильных ограничений по входным данным задачи ее можно решить намного более оптимальным способом.
Если боитесь больших массивов, то можно немного схитрить. Мы можем запомнить в таблице количество единиц для каждого возможного байта, разбить число на 4 части, найти для этих частей количество хранящих в них единичек по таблице и сложить это дело. Получится, что нужно всего 256 байт доп памяти и 4 итерации цикла.
Но чтобы было прям наглядно понятна логика, то массив можно сделать еще меньше, если брать по 4 бита(тетрад). Различных тетрадов всего 16 штук, поэтому и нужно будет всего 16 байт доп памяти и 8 итераций цикла. Спасибо, @tutralex, за решение)
int count_ones(unsigned num)
{
static unsigned char c[16]={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int count=0;
while (num)
{
count+=c[num&0x0F];
num>>=4;
}
return count;
};
В общем, вы поняли. Чем меньше массив, тем больше итераций цикла и наоборот. Выбирайте, что вам больше подходит.
Если вы еще не устали, то у меня для вас есть banger. Нахождение количества единичных бит в числе - это не просто задача с литкода. У нее есть практическое применение. Есть такая штука, как расстояние Хемминга для двоичных чисел. Это количество символов, которое отличает данную строку битов от строки, заполненной нулями. То есть это и есть наше количество единичек. Эта штука используется во многих дисциплинах, в том числе и криптографии. Не удивительно, что много народу совершенствовало решение этой задачи. На мой взгляд, самое мозгодробительное решение выглядит примерно так:
❤10👍7⚡4🔥2🤔1😱1
int count_ones(unsigned num)
{
num = (num & 0x5555555555555555 ) + ((num >> 1) & 0x5555555555555555 );
num = (num & 0x3333333333333333 ) + ((num >> 2) & 0x3333333333333333 );
num = (num & 0x0f0f0f0f0f0f0f0f ) + ((num >> 4) & 0x0f0f0f0f0f0f0f0f );
num = (num & 0x00ff00ff00ff00ff ) + ((num >> 8) & 0x00ff00ff00ff00ff );
num = (num & 0x0000ffff0000ffff ) + ((num >> 16) & 0x0000ffff0000ffff);
return num;
}
Выглядит все так, как будто кот случайно блеванул на экран, но естественно у этой каши есть логичное объяснение, которое можете найти тут.
Кому интересно, что же все-таки быстрее, то наш подписчик Леонид оставил по этому поводу интересный комментарий. Но помните, что все сильно зависит от конекретной архитектуры процессора. Поэтому всегда тестируйте решения на таргетном железе.
Honorable mentions:
Питонисты часто в этой задаче упоминают, что они одной левой могут превратить число в бинарное представление, потом в строку и просто посчитать одной функцией количество вхождений единички. А мы вообще-то ничем не хуже! И даже лучше! Нам не нужно конвертировать бинарное представление в строку. Достаточно std::bitset и его метода count:
int count_ones(unsigned num) {
return std::bitset<32>{num}.count();
}Solve your problems. Stay cool.
❤29👍20🤯7😱2⚡1
Header only либы. Pros and cons
#новичкам
Мы живем в мире С++, где почти нет ничего общепринятого. Часто даже не для очень специфичной задачи, типа сериализации, приходится искать какие-то сторонние решения и сравнивать их. Давайте отложим в сторону вопросы о качестве кода и дизайне и сконцентрируемся вот на чем. Какую библиотеку выбрать: уже скомпилированную или полностью header-only? Будем рассматривать вопрос с точки зрения header-only либ. Какие у них преимущества и недостатки?
Преимущества:
✅ Упрощенный процесс сборки. Вам не нужно ничего компилировать библиотеку и в кишках симейка указывать кучу зависимостей. Подключил хэдэр, указал к нему путь и вперед!
✅ Упрощенная поддержка. Если у вас есть скомпилированная библиотека, вы, вероятно, захотите создать несколько ее версий: одну скомпилированную с включенной отладкой, другую с включенной оптимизацией и, возможно, еще одну без символов. И, возможно, даже больше для мультиплатформенного приложения. Все это довольно сильно усложняет интеграцию библиотеки и будущее обновление.
✅ Можно нормально дебажить. Большинство бинарных либ распространяются именно в релизной версии и их функции невозможно нормально дебажить. Я знаю, что большинство из вас дебажатся принтами и ваша хата с краю, но иногда очень полезно было бы иметь дебажную версию.
✅ Бо'льшая переносимость. Не все компиляторы одинаково компилируют одни и те же сущности даже на одной и той же платформе. Ваша ддлка или сошник может просто по ABI вам не подходить. Хэдэр-онли либы приходится компилировать вместе с проектом, что убирает проблему несовместимости ABI.
✅ Возможность использования нестандартной стандартной либы. Оксюморон? Может быть. Но иногда вам может понадобиться использовать либо свою, либо пропатченную стандартную библиотеку. Возможно, в вашем проекте запрещены вызовы каких-то стдшных функций или вы сами подкручиваете и оптимизируете код в узких местах. Тогда ваше единственное спасение - хэдэр онли либы. Только они позволят вашим изменениям отразиться на скомпилированном коде библиотечных сущностей.
Недостатки:
⛔️ Крупные объектные файлы. Раз нам доступно определение сущностей, то мы можем попробовать их встроить. Это само по себе увеличивает размер бинарного файла. Так еще и на каждую единицу трансляции будет свое определение слабого символа этой сущности. Да, в конце они объединятся, но в объектные файлы все еще будут повторения.
⛔️ Более длинная компиляция. Раз код еще не скомпилирован, то надо его скомпилировать. Причем даже те сущности, которые используются в нескольких единицах трансляции, придется компилировать в каждой из них отдельно. Большое обилие компилируемых сущностей и слабых символов сильно тормозят работу компилятора и линкера.
⛔️ Перекомпиляция. При изменении исходников вам скорее всего будет нужно перекомпилировать весь проект. С бинарными либами такой проблемы нет. Перекомпилировать нужно только те единицы трансляции, которые непосредственно используют код библиотеки.
⛔️ Кожаным труднее читать. Даже с лучшей документацией пользователям библиотеки часто приходится прибегать к чтению заголовков библиотеки. Заголовки в хэдэр-онли либах заполнены деталями реализации, которые мешают пониманию интерфейса. С скомпилированной библиотекой все, что вы видите, это интерфейс и краткий комментарий о том, что делает реализация, и это обычно все, что вам нужно. Даже так, ничего кроме этого вам не должно быть нужно! Нафиг детали реализации, концентрируемся на интерфейсе.
В каждом конкретном случае разные ключевые факторы играют роль, поэтому нельзя дать универсальный ответ на вопрос "какой тип либы исопльзовать?". Контекст вам сам подскажет ограничения. Надо просто уметь метчить ограничения с типом библиотеки.
Make the right choice. Stay cool.
#новичкам
Мы живем в мире С++, где почти нет ничего общепринятого. Часто даже не для очень специфичной задачи, типа сериализации, приходится искать какие-то сторонние решения и сравнивать их. Давайте отложим в сторону вопросы о качестве кода и дизайне и сконцентрируемся вот на чем. Какую библиотеку выбрать: уже скомпилированную или полностью header-only? Будем рассматривать вопрос с точки зрения header-only либ. Какие у них преимущества и недостатки?
Преимущества:
✅ Упрощенный процесс сборки. Вам не нужно ничего компилировать библиотеку и в кишках симейка указывать кучу зависимостей. Подключил хэдэр, указал к нему путь и вперед!
✅ Упрощенная поддержка. Если у вас есть скомпилированная библиотека, вы, вероятно, захотите создать несколько ее версий: одну скомпилированную с включенной отладкой, другую с включенной оптимизацией и, возможно, еще одну без символов. И, возможно, даже больше для мультиплатформенного приложения. Все это довольно сильно усложняет интеграцию библиотеки и будущее обновление.
✅ Можно нормально дебажить. Большинство бинарных либ распространяются именно в релизной версии и их функции невозможно нормально дебажить. Я знаю, что большинство из вас дебажатся принтами и ваша хата с краю, но иногда очень полезно было бы иметь дебажную версию.
✅ Бо'льшая переносимость. Не все компиляторы одинаково компилируют одни и те же сущности даже на одной и той же платформе. Ваша ддлка или сошник может просто по ABI вам не подходить. Хэдэр-онли либы приходится компилировать вместе с проектом, что убирает проблему несовместимости ABI.
✅ Возможность использования нестандартной стандартной либы. Оксюморон? Может быть. Но иногда вам может понадобиться использовать либо свою, либо пропатченную стандартную библиотеку. Возможно, в вашем проекте запрещены вызовы каких-то стдшных функций или вы сами подкручиваете и оптимизируете код в узких местах. Тогда ваше единственное спасение - хэдэр онли либы. Только они позволят вашим изменениям отразиться на скомпилированном коде библиотечных сущностей.
Недостатки:
⛔️ Крупные объектные файлы. Раз нам доступно определение сущностей, то мы можем попробовать их встроить. Это само по себе увеличивает размер бинарного файла. Так еще и на каждую единицу трансляции будет свое определение слабого символа этой сущности. Да, в конце они объединятся, но в объектные файлы все еще будут повторения.
⛔️ Более длинная компиляция. Раз код еще не скомпилирован, то надо его скомпилировать. Причем даже те сущности, которые используются в нескольких единицах трансляции, придется компилировать в каждой из них отдельно. Большое обилие компилируемых сущностей и слабых символов сильно тормозят работу компилятора и линкера.
⛔️ Перекомпиляция. При изменении исходников вам скорее всего будет нужно перекомпилировать весь проект. С бинарными либами такой проблемы нет. Перекомпилировать нужно только те единицы трансляции, которые непосредственно используют код библиотеки.
⛔️ Кожаным труднее читать. Даже с лучшей документацией пользователям библиотеки часто приходится прибегать к чтению заголовков библиотеки. Заголовки в хэдэр-онли либах заполнены деталями реализации, которые мешают пониманию интерфейса. С скомпилированной библиотекой все, что вы видите, это интерфейс и краткий комментарий о том, что делает реализация, и это обычно все, что вам нужно. Даже так, ничего кроме этого вам не должно быть нужно! Нафиг детали реализации, концентрируемся на интерфейсе.
В каждом конкретном случае разные ключевые факторы играют роль, поэтому нельзя дать универсальный ответ на вопрос "какой тип либы исопльзовать?". Контекст вам сам подскажет ограничения. Надо просто уметь метчить ограничения с типом библиотеки.
Make the right choice. Stay cool.
👍25❤5😁5🔥2⚡1
Как header only либы обходят ODR
#новичкам
В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.
Действительно, если будут 2 функции с одинаковыми названиями, но разной реализацией, то непонятно, какую из них выбрать для линковки с использующим функцию кодом.
Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.
В чем подвох?
Подвоха нет. Даже так, чисто заголовочная природа библиотеки это не совсем цель, а возможно простое следствие. Следствие того, что часто библиотеки напичканы шаблонами по самые гланды. А шаблоны просто вынуждены находиться в хэдэрах, ничего уж тут не поделаешь. У нас даже целый пост про это есть.
Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.
А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .
В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.
Bypass the rules. Stay cool.
#compiler #design
#новичкам
В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.
Действительно, если будут 2 функции с одинаковыми названиями, но разной реализацией, то непонятно, какую из них выбрать для линковки с использующим функцию кодом.
Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.
В чем подвох?
Подвоха нет. Даже так, чисто заголовочная природа библиотеки это не совсем цель, а возможно простое следствие. Следствие того, что часто библиотеки напичканы шаблонами по самые гланды. А шаблоны просто вынуждены находиться в хэдэрах, ничего уж тут не поделаешь. У нас даже целый пост про это есть.
Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.
А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .
В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.
Bypass the rules. Stay cool.
#compiler #design
🔥19👍10❤4👏1
Смешиваем std::visit и std::apply
#опытным
Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.
И такую штуку можно сделать для tuple-like объектов, среди которых std::pair, std::tuple и std::array. Функция std::applyможет распаковать нам элементы этих коллекций и вызвать для них функцию, которая принимает в качестве аргументов все эти элементы по отдельности. Это же то, что нам нужно!
Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.
Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.
В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.
Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.
В общем, делаем себе укол пары кубиков метапрограммирования.
Выглядит клёво!
Опять же оставляю ссылочку на cppinsights с этим примером, там подробно показаны сгенеренные сущности и их их взаимодействие.
Look cool. Stay cool.
#template #cpp17
#опытным
Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.
И такую штуку можно сделать для tuple-like объектов, среди которых std::pair, std::tuple и std::array. Функция std::applyможет распаковать нам элементы этих коллекций и вызвать для них функцию, которая принимает в качестве аргументов все эти элементы по отдельности. Это же то, что нам нужно!
Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
int main(){
std::array<var_t, 3> arr = {1.5, 42, "Hello"};
Visitor vis{[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } };
std::apply([&](auto&&... args){(std::visit(vis, std::forward<decltype(args)>(args)), ...);}, arr);
}
Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.
В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.
Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.
В общем, делаем себе укол пары кубиков метапрограммирования.
Выглядит клёво!
Опять же оставляю ссылочку на cppinsights с этим примером, там подробно показаны сгенеренные сущности и их их взаимодействие.
Look cool. Stay cool.
#template #cpp17
👍20🔥11😁11❤4⚡1
Потокобезопасный интерфейс
#новичкам
Не для всех очевидная новость: не всегда можно превратить класс из небезопасного в потокобезопасный, просто по уши обложившись лок гардами. Да, вызов конкретного метода будет безопасен. Но это не значит, что классом безопасно пользоваться.
Возьмем максимально простую реализацию самой простой очереди:
Она конечно потокоНЕбезопасная. То есть ей можно адекватно пользоваться только в рамках одного потока.
Как может выглядеть код простого консьюмера этой очереди?
И вот мы захотели разделить обязанности производителя чисел и их потребителя между разными потокам. Значит, нам надо как-то защищать очередь от многопоточных неприятностей.
Бабахаем везде лок гард на один мьютекс и дело в шляпе!
Все доступы к очереди защищены. Но спасло ли реально это нас?
Вернемся к коду консюмера:
А вдруг у нас появится еще один консюмер? Тогда в первом из них мы можем войти условие, а в это время второй достанет последний элемент. Получается, что мы получим доступ к неинициализированной памяти в методе front.
То есть по факту в многопоточном приложении полученный стейт сущности сразу же утрачивает свою актуальность.
Что делать? Не только сами методы класса должны быть потокобезопасными. Но еще и комбинации использования этих методов тоже должны обладать таким свойством. И с данным интерфейсом это сделать просто невозможно.
Если стейт утрачивает актуальность, то мы вообще не должны давать возможность приложению получать стейт очереди. Нам нужны только команды управления. То есть push и pop.
Внутри метода
Теперь консюмер выглядит так:
Можно конечно было использовать кондвары и прочее. Но я хотел сфокусироваться именно на интерфейсе. Теперь реализация просто не позволяет получать пользователю потенциально неактульные данные.
Stay safe. Stay cool.
#concurrency #design #goodpractice
#новичкам
Не для всех очевидная новость: не всегда можно превратить класс из небезопасного в потокобезопасный, просто по уши обложившись лок гардами. Да, вызов конкретного метода будет безопасен. Но это не значит, что классом безопасно пользоваться.
Возьмем максимально простую реализацию самой простой очереди:
struct Queue {
void push(int value) {
storage.push_back(value);
}
void pop() {
storage.pop_front();
}
bool empty() {
return storage.empty();
}
int& front() {
return storage.front();
}
private:
std::deque<int> storage;
};Она конечно потокоНЕбезопасная. То есть ей можно адекватно пользоваться только в рамках одного потока.
Как может выглядеть код простого консьюмера этой очереди?
while(condition)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}
И вот мы захотели разделить обязанности производителя чисел и их потребителя между разными потокам. Значит, нам надо как-то защищать очередь от многопоточных неприятностей.
Бабахаем везде лок гард на один мьютекс и дело в шляпе!
struct Queue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
void pop() {
std::lock_guard lg{m};
storage.pop_front();
}
bool empty() {
std::lock_guard lg{m};
return storage.empty();
}
int& front() {
std::lock_guard lg{m};
return storage.front();
}
private:
std::deque<int> storage;
std::mutex m;
};Все доступы к очереди защищены. Но спасло ли реально это нас?
Вернемся к коду консюмера:
while(true)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}
А вдруг у нас появится еще один консюмер? Тогда в первом из них мы можем войти условие, а в это время второй достанет последний элемент. Получается, что мы получим доступ к неинициализированной памяти в методе front.
То есть по факту в многопоточном приложении полученный стейт сущности сразу же утрачивает свою актуальность.
Что делать? Не только сами методы класса должны быть потокобезопасными. Но еще и комбинации использования этих методов тоже должны обладать таким свойством. И с данным интерфейсом это сделать просто невозможно.
Если стейт утрачивает актуальность, то мы вообще не должны давать возможность приложению получать стейт очереди. Нам нужны только команды управления. То есть push и pop.
struct ThreadSafeQueue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
std::optional<int> pop() {
std::lock_guard lg{m};
if (!storage.empty()) {
int elem = storage.front();
storage.pop_front();
return elem;
}
return nullopt;
}
private:
std::deque<int> storage;
std::mutex m;
};Внутри метода
pop мы можем использовать проверять и получать стейт очереди, так как мы оградились локом. Возвращаем из него std::optional, который будет хранить фронтальный элемент, если очередь была непуста. В обратном случае он будет пуст.Теперь консюмер выглядит так:
while(true) {
auto elem = queue.pop();
if (elem)
process_elem(elem.value());
}Можно конечно было использовать кондвары и прочее. Но я хотел сфокусироваться именно на интерфейсе. Теперь реализация просто не позволяет получать пользователю потенциально неактульные данные.
Stay safe. Stay cool.
#concurrency #design #goodpractice
10👍42🔥11❤7😁2🤔1
Сколько вам было лет, когда вы узнали, что название бинарного файла, который по умолчанию генерирует GCC - a.out - это сокращение от assembler output(вывод ассемблера)? 🤯
Я вот узнал в толькочтогодиков.
Я вот узнал в толькочтогодиков.
😁130🤯47🔥17☃4👎4🤣2
Бросаем число
#новичкам
Мы привыкли, что исключения имеют какую-то свою иерархию и каждый класс имеет свое конкретное назначение в контексте отображения ошибки.
А что если мы попытаемся бросить что-то совсем несвязанное с иcключениями? Например, какой-нибудь тривиальный тип вроде int. Это вообще законно?
Абсолютно законно. В С++ можно бросать все, что угодно, кроме объектов неполных типов, абстрактных классов и указателей на неполный тип. Даже указатель на void можно.
Как и число.
Поймать число примерно также просто, как его бросить:
Это кстати один из любимых вопросов у интервьюеров.
"А можно ли кидать число вместо исключения?"
Теперь вы с полной уверенностью ответите "можно".
Но вот зачем это может быть нужно? Оставьте ваши мысли в комментариях
Make sense. Stay cool.
#interview #cppcore
#новичкам
Мы привыкли, что исключения имеют какую-то свою иерархию и каждый класс имеет свое конкретное назначение в контексте отображения ошибки.
А что если мы попытаемся бросить что-то совсем несвязанное с иcключениями? Например, какой-нибудь тривиальный тип вроде int. Это вообще законно?
Абсолютно законно. В С++ можно бросать все, что угодно, кроме объектов неполных типов, абстрактных классов и указателей на неполный тип. Даже указатель на void можно.
Как и число.
Поймать число примерно также просто, как его бросить:
void foo() {
throw 1;
}
int main() {
try {
foo();
}
catch(int i) {
std::cout << i << std::endl;
}
}
// OUTPUT: 1Это кстати один из любимых вопросов у интервьюеров.
"А можно ли кидать число вместо исключения?"
Теперь вы с полной уверенностью ответите "можно".
Но вот зачем это может быть нужно? Оставьте ваши мысли в комментариях
Make sense. Stay cool.
#interview #cppcore
❤24👍15😁7🔥3⚡1
Сколько минимально нужно мьютексов, чтобы вызвать дедлок?
#опытным
Мы уже рассматривали похожий вопрос и на собесах правильными ответом будет 2. 2 потока локают по одному мьютексу и пытаются захватить тот замок, который уже находится во владении другого потока. Они будут пытаться бесконечно исполнение в них перестанет двигаться вперед.
Но это скорее для всех ЯП некий универсальный ответ. У всех языков немного разные подходы к многопоточности и немного отличающиеся инструменты для работы с ней.
А что если мы не будем ориентироваться на общую универсальную для всех языков теорию многопоточного программирования и сфокусируется чисто на С++ и средствах, которые он предоставляет?
Какой тогда будет ответ?
#quiz
#опытным
Мы уже рассматривали похожий вопрос и на собесах правильными ответом будет 2. 2 потока локают по одному мьютексу и пытаются захватить тот замок, который уже находится во владении другого потока. Они будут пытаться бесконечно исполнение в них перестанет двигаться вперед.
Но это скорее для всех ЯП некий универсальный ответ. У всех языков немного разные подходы к многопоточности и немного отличающиеся инструменты для работы с ней.
А что если мы не будем ориентироваться на общую универсальную для всех языков теорию многопоточного программирования и сфокусируется чисто на С++ и средствах, которые он предоставляет?
Какой тогда будет ответ?
#quiz
👍7❤2🔥1🤔1
Сколько минимально нужно мьютексов, чтобы вызвать дедлок?
Anonymous Poll
3%
3
43%
2
35%
1
8%
0
11%
Мнимая единица!
🤔16👍4❤🔥2🔥2❤1🤬1
Ответ
#опытным
Правильный ответ на квиз из предыдущего поста- 0. Вообще ни одного мьютекса не нужно.
Много можно вариантов придумать. И даже в комментах написали несколько рабочих способов.
Вообще говоря, в определении дедлока не звучит слово "мьютекс". Потоки должны ждать освобождения ресурсов. А этими ресурсами может быть что угодно.
Для организации дедлока достаточно просто, чтобы 2 потока запустились в попытке присоединить друг друга. Естественно, что они будут бесконечно ждать окончания работы своего визави.
Однако не совсем очевидно, как это организовать. Вот мы определяем первый объект потока и его надо запустить с функцией, которая ждем еще не существующего потока.
Заметьте, что мы пытаемся сделать грязь. Так давайте же применим самые опасные вещи из плюсов и у нас все получится! Надо лишь добавить 50 грамм указателей и чайную ложку глобальных переменных. Получается вот такая каша:
Все просто. Вводим глобальный указатель на поток. В функции первого потока мы даем время инициализировать указатель и присоединяем поток по указателю. А тем временем в main создаем динамический объект потока и записываем его по указателю t_ptr. Таким образом первый поток получает доступ ко второму. В функцию второго потока передаем объект первого потока по ссылке и присоединяем его. Обе функции после инструкции join выводят на консоль запись.
Чтобы это все дело работало, нужно продлить существование основных потоков. В обратном случае, вызовутся деструкторы неприсоединенных потоков, а эта ситуация в свою очередь стриггерит вызов std::terminate. Поэтому делаем бесконечный цикл, чтобы иметь возможность посмотреть на этот самый дедлок.
И действительно. При запуске программы ничего не выводится. Более того, пока писался этот пост, программа работала и ничего так и не вывела. Учитывая, что потоки особо ничего не делают, то логично предположить, что ситуация и не поменяется.
Естественно, что потоков может быть больше и кольцо из ожидающих потоков может быть больше. Но это такой минимальный пример.
Если вы думаете, что это какая-то сова в вакууме, то подумайте еще раз. Владение потоками можно передавать в функции. Могут быть довольно сложные схемы организации взаимодействия потоков. И если вы присоединяете поток не в его родителе, то возникает благоприятные условия для возникновения такого безлокового дедлока.
Поэтому лучше избегать такого присоединения, или быть супервнимательным, если вы уж решились вступить на эту дорожку.
Stay surprised. Stay cool.
#concurrency #cppcore
#опытным
Правильный ответ на квиз из предыдущего поста- 0. Вообще ни одного мьютекса не нужно.
Много можно вариантов придумать. И даже в комментах написали несколько рабочих способов.
Вообще говоря, в определении дедлока не звучит слово "мьютекс". Потоки должны ждать освобождения ресурсов. А этими ресурсами может быть что угодно.
Для организации дедлока достаточно просто, чтобы 2 потока запустились в попытке присоединить друг друга. Естественно, что они будут бесконечно ждать окончания работы своего визави.
Однако не совсем очевидно, как это организовать. Вот мы определяем первый объект потока и его надо запустить с функцией, которая ждем еще не существующего потока.
Заметьте, что мы пытаемся сделать грязь. Так давайте же применим самые опасные вещи из плюсов и у нас все получится! Надо лишь добавить 50 грамм указателей и чайную ложку глобальных переменных. Получается вот такая каша:
std::thread * t_ptr = nullptr;
void func1() {
std::this_thread::sleep_for(std::chrono::seconds(1));
t_ptr->join();
std::cout << "Never reached this point1" << std::endl;
}
void func2(std::thread& t) {
t.join();
std::cout << "Never reached this point2" << std::endl;
}
int main() {
std::thread t1{func1};
t_ptr = new std::thread(func2, std::ref(t1));
while(true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
Все просто. Вводим глобальный указатель на поток. В функции первого потока мы даем время инициализировать указатель и присоединяем поток по указателю. А тем временем в main создаем динамический объект потока и записываем его по указателю t_ptr. Таким образом первый поток получает доступ ко второму. В функцию второго потока передаем объект первого потока по ссылке и присоединяем его. Обе функции после инструкции join выводят на консоль запись.
Чтобы это все дело работало, нужно продлить существование основных потоков. В обратном случае, вызовутся деструкторы неприсоединенных потоков, а эта ситуация в свою очередь стриггерит вызов std::terminate. Поэтому делаем бесконечный цикл, чтобы иметь возможность посмотреть на этот самый дедлок.
И действительно. При запуске программы ничего не выводится. Более того, пока писался этот пост, программа работала и ничего так и не вывела. Учитывая, что потоки особо ничего не делают, то логично предположить, что ситуация и не поменяется.
Естественно, что потоков может быть больше и кольцо из ожидающих потоков может быть больше. Но это такой минимальный пример.
Если вы думаете, что это какая-то сова в вакууме, то подумайте еще раз. Владение потоками можно передавать в функции. Могут быть довольно сложные схемы организации взаимодействия потоков. И если вы присоединяете поток не в его родителе, то возникает благоприятные условия для возникновения такого безлокового дедлока.
Поэтому лучше избегать такого присоединения, или быть супервнимательным, если вы уж решились вступить на эту дорожку.
Stay surprised. Stay cool.
#concurrency #cppcore
👍28❤8🔥5🤯3😱3😁1
Дедлокаем один поток
#опытным
Мы привыкли, что для дедлоков нужно несколько потоков. Не удивительно. Давайте прочитаем определение дедлока по Коффману. Там речь про процессы, но если поменять слово "процесс" на "поток" ничего не изменится. Ну и перевод будет вольный.
Дедлок - это ситуация в коде, когда одновременно выполняются все следующие условия:
А ну, мальчики, играем поочереди. Только один поток может получить доступ к ресурсу в один момент времени.
У меня уже есть красный паровозик, но я хочу синий!. Поток в настоящее время хранит по крайней мере один ресурс и запрашивает дополнительные ресурсы, которые хранятся в других потоках.
Я тебя захватил, я тебя и отпущу. Ресурс может быть освобожден только добровольно потоком, удерживающим его.
Все: Я хочу твой паровозик! Каждый поток должен ждать ресурс, который удерживается другим потоков, который, в свою очередь, ожидает, когда первый поток освободит ресурс. В общем случае ждунов может быть больше двух. Важно круговое ожидание.
Судя по этому определению, минимальное количество потоков, чтобы накодить дедлок - 2.
Но это такая общая теория работы с многозадачностью в программах.
Определение оперирует общим термином ресурс. И не учитывает поведение конкретного ресурса и деталей его реализации. А они важны!
Возьмем пресловутый мьютекс. Что произойдет, если я попытаюсь его залочить дважды в одном потоке?
Стандарт говорит, что будет UB. То есть поведение программы неопределено, возможно она заставит Ким Чен Ира спеть гангам стайл.
Возможно, но обычно этого не происходит. Программа в большинстве случаев ведет себя по одному из нескольких сценариев.
1️⃣ Компилятор имплементировал умный мьютекс, который может задетектить double lock и, например, кинуть в этом случае исключение.
2️⃣ Мьютекс у нас обычный, подтуповатый и он делает ровно то, что ему говорят. А именно пытается залочить мьютекс. Конечно у него ничего не получится и он вечно будет ждать его освобождения. Результат такого сценария - дедлок одного потока одним мьютексом!
Результат не гарантирован стандартом, но мой код под гццшкой именно так себя и повел. Поэтому теперь у вас есть еще один факт, которым можно понтануться перед коллегами или на собесах.
Be self-sufficient. Stay cool.
#concurrency #cppcore #compiler
#опытным
Мы привыкли, что для дедлоков нужно несколько потоков. Не удивительно. Давайте прочитаем определение дедлока по Коффману. Там речь про процессы, но если поменять слово "процесс" на "поток" ничего не изменится. Ну и перевод будет вольный.
Дедлок - это ситуация в коде, когда одновременно выполняются все следующие условия:
А ну, мальчики, играем поочереди. Только один поток может получить доступ к ресурсу в один момент времени.
У меня уже есть красный паровозик, но я хочу синий!. Поток в настоящее время хранит по крайней мере один ресурс и запрашивает дополнительные ресурсы, которые хранятся в других потоках.
Я тебя захватил, я тебя и отпущу. Ресурс может быть освобожден только добровольно потоком, удерживающим его.
Все: Я хочу твой паровозик! Каждый поток должен ждать ресурс, который удерживается другим потоков, который, в свою очередь, ожидает, когда первый поток освободит ресурс. В общем случае ждунов может быть больше двух. Важно круговое ожидание.
Судя по этому определению, минимальное количество потоков, чтобы накодить дедлок - 2.
Но это такая общая теория работы с многозадачностью в программах.
Определение оперирует общим термином ресурс. И не учитывает поведение конкретного ресурса и деталей его реализации. А они важны!
Возьмем пресловутый мьютекс. Что произойдет, если я попытаюсь его залочить дважды в одном потоке?
std::mutex mtx;
mtx.lock();
mtx.lock();
Стандарт говорит, что будет UB. То есть поведение программы неопределено, возможно она заставит Ким Чен Ира спеть гангам стайл.
Возможно, но обычно этого не происходит. Программа в большинстве случаев ведет себя по одному из нескольких сценариев.
1️⃣ Компилятор имплементировал умный мьютекс, который может задетектить double lock и, например, кинуть в этом случае исключение.
2️⃣ Мьютекс у нас обычный, подтуповатый и он делает ровно то, что ему говорят. А именно пытается залочить мьютекс. Конечно у него ничего не получится и он вечно будет ждать его освобождения. Результат такого сценария - дедлок одного потока одним мьютексом!
Результат не гарантирован стандартом, но мой код под гццшкой именно так себя и повел. Поэтому теперь у вас есть еще один факт, которым можно понтануться перед коллегами или на собесах.
Be self-sufficient. Stay cool.
#concurrency #cppcore #compiler
👍17🔥8❤5😁5🤣4⚡3
No new line
Оказывается, чтобы получить неопределенное поведение даже необязательно писать какой-то плохой код. Достаточно просто не добавить перенос строки в конце подключаемого файла!
Небольшой пример:
А теперь вспоминаем, что препроцессор вставляет все содержимое хэдера на место инклюда И(!) не вставляет после него символ конца строки. То есть спокойно может получится следующее:
То есть включение baz.hpp может быть полностью заэкранировано.
Учитывая, сколько всего препроцессор может делать с кодом, комбинации вариантов развития событий могут быть абсолютно разными.
Стандарт нам говорит:
Так что ub без кода - вполне существующая вещь.
Или уже нет?
На самом деле приведенная цитата была из стандарта 2003 года.
С++11 пофиксил эту проблему и обязал препроцессоры вставлять new line в конце подключаемых файлов:
Так что теперь проблемы нет.
Решил написать об этом, просто потому что очень весело, что в плюсах можно было такими неочевидными способами отстрелить себе конечность.
Ну и хорошо, что стандарт все-таки не только новую функциональность вводит, а фиксит вот такие вот недоразумения.
Fix your flaws. Stay cool.
#compiler
Оказывается, чтобы получить неопределенное поведение даже необязательно писать какой-то плохой код. Достаточно просто не добавить перенос строки в конце подключаемого файла!
Небольшой пример:
Файлик foo.hpp:
// I love code
// I love C++<no newline>
Файлик bar.cpp:
#include "foo.hpp"
#include "baz.hpp"
А теперь вспоминаем, что препроцессор вставляет все содержимое хэдера на место инклюда И(!) не вставляет после него символ конца строки. То есть спокойно может получится следующее:
// I love code
// I love C++#include "baz.hpp"
То есть включение baz.hpp может быть полностью заэкранировано.
Учитывая, сколько всего препроцессор может делать с кодом, комбинации вариантов развития событий могут быть абсолютно разными.
Стандарт нам говорит:
... If a source file that is not empty does not end in a new-line character,
or ends in a new-line character immediately preceded by a backslash
character before any such splicing takes place, the behavior is undefined.
Так что ub без кода - вполне существующая вещь.
Или уже нет?
На самом деле приведенная цитата была из стандарта 2003 года.
С++11 пофиксил эту проблему и обязал препроцессоры вставлять new line в конце подключаемых файлов:
A source file that is not empty and that does not end in a new-line character,
or that ends in a new-line character immediately preceded by a backslash
character before any such splicing takes place, shall be processed
as if an additional new-line character were appended to the file.
Так что теперь проблемы нет.
Решил написать об этом, просто потому что очень весело, что в плюсах можно было такими неочевидными способами отстрелить себе конечность.
Ну и хорошо, что стандарт все-таки не только новую функциональность вводит, а фиксит вот такие вот недоразумения.
Fix your flaws. Stay cool.
#compiler
👍49❤10🔥9🤯3⚡2