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

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​sync_with_stdio
#опытным

std::cout, std::cerr и std::cin - стандартные объекты потоков для работы с вводом-выводом в С++.

Но из С++ программы мы также можем вызывать и сишное апи для работы с IO. Например scanfprintf. Они также могут читать из консоли и писать в нее.

Но как связаны С++ и С апи для работы с IO? Мешают ли они друг другу и скремблят результаты операций? Или все хитрее?

Все хитрее)

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

В базовой конфигурации std::cout и запись в stdout с помощью С апи синхронизированы. То есть следующие вызовы полностью эквивалентны(c- символьная переменная):

std::fputc(stdout, c);
// and
std::cout.rdbuf()->sputc(c);


Запись символа в буфер С++ объекта имеет тот же эффект, что и запись символа в буфер С потока.

На практике это означает, что синхронизированные потоки C++ не буферизуются, и каждая операция ввода-вывода на потоке C++ немедленно применяется к буферу соответствующего потока C. Это позволяет свободно смешивать C++ и C I/O операции.

Обращу внимание. Мы до сих пор говорим только об одном символе. И не спроста: для записи конкретного символа гарантируется синхронизация и потокобезопасность(thread-safety). Но записи в stdout из разных тредов могут перемешивать символы этих записей.

То есть при вызове:

std::cout << "Hello, World!" << std::endl;


по сути имплементация посимвольно записывает приветствие миру в stdout в цикле, вызывая std::putc(stdout, c). Обычно такие функции(типа putc) реализованы с помощью внутренних механизмов синхронизации, обеспечивая потокобезопасность.

И это предательски медленно! Поэтому дефолтные операции ввода-вывода в С++ такие медленные.

Синхронизируют запись каждого символа только трусы! Но мы-то с вами не трусы.

Можно отвязать С++ потоки от С потоков и сделать их независимыми.
Тогда у С++ потоков появляется свой буфер, который работает оптимальнее, чем посимвольная запись. Это может дать сильный буст к производительности стандартных IO операций и по скорости они могут сравниться с сишными.

Чтобы отвязать потоки нужно самой первой строчкой main вызывать следующую функцию. Например так

int main()
{
std::ios::sync_with_stdio(false);
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
}


Вывод может изменяться в разных ситуациях, но вот здесь получился такой вывод:

a
c
b


Видно, что разное апи работает независимо и непоследовательно.

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

Be fast. Stay cool.

#cppcore #optimization #goodoldc #compiler
123🔥12👍8🤔5
Конфликт в действии
#опытным

Спасибо, @Ivaneo, за любезно предоставленный примерчик.

Посмотрите на этот код:

#include <iostream>

int read;

int main()
{
std::ios_base::sync_with_stdio(false);
std::cin >> read;
}


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

А получите ошибку сегментации: прилетит сигнал SIGSEGV.

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

На самом деле сделали кое-что незаконное. Назвали переменную в честь системного вызова. Давайте по порядку.

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

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

Посмотрим на примере gcc.

В исходниках есть такое определение библиотечного вызова чтения:

/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (read, fd, buf, nbytes);
}

weak_alias (__libc_read, read)


Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak).

А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss).

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

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

Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например readopenclosewriteexit).

Avoid name clash. Stay cool.

#cppcore #goodoldc #OS
17🔥11👍7🤯4❤‍🔥1
​​Объявления функций vs объявление переменной или зачем нужен extern
#новичкам

В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины:

👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление.

👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp.

👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения.

👉🏿 Организация кода и разделение ответственности.

И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:

void foo(int a); // declaration

void foo(int a) {
std::cout << a << std::endl; // definition
}


С переменными все немного сложнее. Но начнем с легкого:

int i = 0;


Это определение хоть глобальной, хоть локальной переменной i и ее инициализация нулем. Все все понимают.

Если думать в аналогии с функциями, то легко дойти до мысли что:

int i;


это объявление переменной.

Но это не так!

Это в любом случае определение!

Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него.

Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте?

Ровно для этого и нужно ключевое слово extern.

extern int i; // declaration

int main() {
std::cout << i << std::endl;
}

int i = 42;

// OUTPUT
// 42


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

В этом случае строчка extern int i; является объявлением глобальной переменной i.

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

Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:

int main() {
extern int i;
std::cout << i << std::endl;
}


То вы опять объявите глобальную переменную i.

То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью extern.

Declare your intentions. Stay cool.

#cppcore
1🔥21👍117🤣3
​​Почему локальные переменные инициализируются мусором?
#новичкам

Изучать С++ больно по многим причинам. Одна из них - есть вещи, которые просто надо принять на веру. И чтобы понять, почему эти вещи работают так, а не иначе, нужно много времени за кодингом и качать сиплюсплюсные и computer science бицепсы.

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

int main() {
int i;
std::cout << i << std::endl;
}
// POSSIBLE OUTPUT:
// 64


Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить?

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

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

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

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

int main() {
int i;
...
}


Мы говорим, что хотим иметь переменную i и надо выделить под нее память, то есть просто передвинуть указатель на стек с позиции X на позицию X+4. Само значение мы при этом не задаем.

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

А кто-то из вас заранее знает, какие байтики там будут храниться?

Нет

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

Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции.

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

"Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?"

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

"Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?"

Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься.

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

Understand the root cause. Stay cool.

#cppcore #os
👍3511🔥10❤‍🔥3
​​Почему тогда глобальные переменные зануляются?
#новичкам

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

Можно конечно сказать, что такие двойные стандарты определены в стандарте С++ и на этом можно закончить.

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

int i;

int main() {
std::cout << i << std::endl;
}
// OUTPUT is always
// 0


Все пошло от языка С. Там глобальные переменные занулялись, поэтому для совместимости с С С++ также перенял эту особенность.

"Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь."

Это был важный переход, который и приведет нас к ответу.

Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык.

Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями.

И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись.

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

Вот так обнуление стало стандартом С, а затем и С++.

Understand the root cause. Stay cool.

#cppcore #os
8👍2910🔥9😱1
Если хотите повеселиться, то почитайте комменты в последнем подкасте подлодки про С++ с Антоном Полухиным.

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

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

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

Есть и проблемы, без них никуда. И их предостаточно. Но здесь я согласен с Антоном, что ошибки в программах в основном зашиты в логике, а не вызваны кривизной языка. Я пишу на userver'е и за последние несколько лет не встретил на проде бага, который бы появился из-за опасной фичи языка. Когда есть сильные тесты, они выявляют большинство проблем.

В общем смотрите https://youtu.be/ZYaBzFj3d4Y?si=HMxEeBONt6pL5VC7. И подкаст интересный, и комменты настоялись.

Ну и давайте в противовес тем комментам, напишите под этим постом, почему вам нравится С++?

Enjoy the tool. Stay cool.

#fun
👍319🔥6👎2😁2❤‍🔥1
​​Как запретить объекту создаваться на стеке? Классика
#новичкам

Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++.

Можно удалить все конструкторы с помощью пометки delete и тогда объект вообще нигде нельзя будет создавать. Но вопрос скорее всего про другое: как запретить объекту создаваться на стеке, но не на куче или статической памяти?

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

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

Если мы хотим создавать объект только на куче можно написать так:

class MyClass {
private:
MyClass() { std::cout << "MyClass created\n"; }
public:
~MyClass() { std::cout << "MyClass destroyed\n"; }
static std::unique_ptr<MyClass> create() {
return std::unique_ptr<MyClass>(new MyClass());
}
void hello() const {
std::cout << "Hello, MyClass!\n";
}
};

auto obj = MyClass::create();
obj->hello();


Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.

Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.

Такой подход может быть полезен, если:

👉🏿 размер объекта очень большой и просто не влезет в стек

👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.

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

👉🏿 вы хотите какой-то особый контроль времени жизни.

Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:

class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void foo() {}
private:
Singleton() = default;
~Singleton() = default;
};


Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.

Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик.

Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз.

Limit wrong uses. Stay cool.

#design #cppcore
23👍14🔥7
Как запретить объекту создаваться на стеке? Экзотика
#опытным

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

Начнем с конца. Жизни объекта, конечно. Давайте сделаем приватным не конструктор, а деструктор. Тогда внешний код не сможет разместить такой объект на стеке, ведь он не сможет его удалить потом.

Однако удалять объект хочется. Для этого сделаем публичный статический метод destroy и фабричный метод, возвращающий умный указатель с кастомным делитером:

class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }

private:
~MyClass() { std::cout << "MyClass destructor\n"; }

public:
static void destroy(MyClass* ptr) {
delete ptr;
}

static std::unique_ptr<MyClass, decltype(&destroy)> create() {
return std::unique_ptr<MyClass, decltype(&destroy)>(new MyClass(), destroy);
}
};


При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.

Публичный конструктор здесь не имеет большого смысла, но и так и так работает.

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

Можно(почти).

Давайте сделаем публичный деструктор и конструктор с параметром, который не может создать внешний код:

class Key {
private:
Key() = default;
friend class Factory;
};

class Object {
public:
explicit Object(const Key&) {} // требует ключ
};

class Factory {
public:
static std::unique_ptr<Object> create() {
return std::make_unique<Object>(Key());
}
};


Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.

Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.

Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.

И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.

// object.hpp
class Object;
std::unique_ptr<Object> createObject();

// object.cpp
#include "object.h"
class Object {
public:
Object() = default;
~Object() = default;
};

std::unique_ptr<Object> createObject() {
return std::make_unique<Object>();
}


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

Explore exotic things. Stay cool.

#design #cppcore
🔥177👍7🤯1
Как запретить объекту создаваться на куче?
#новичкам

Про стек поговорили, не будем и кучу обижать.

Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:

class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};


Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:

class OnlyStack {
OnlyStack() {
if (rand() % 2) {
std::cout << "ERROR" << std::endl;
}
}
public:
static std::optional<OnlyStack> Create() { return {}; }
};


"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"

Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:

class OnlyStack {
public:
OnlyStack() = default;
~OnlyStack() = default;

static void* operator new(std::size_t) = delete;
static void* operator new = delete;
};

OnlyStack obj; // OK
OnlyStack* p = new OnlyStack(); // ERROR


Вот так просто и без дополнительных приседаний.

Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:

class PooledObject {
static char pool[1024];
static size_t offset;
public:
void* operator new(size_t s) {
if (offset + s > 1024) throw std::bad_alloc();
void* ptr = pool + offset;
offset += s;
return ptr;
}
void operator delete(void*) noexcept {
// Complicated logic
// or just ignore freeing memory
}
};
PooledObject* obj = new PooledObject();


Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.

Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?

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

class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};

struct Container {
Container() : obj{OnlyStack::Create()} {}
OnlyStack obj;
};

Container* p = new Container(); // OK


Пишите в комментах, если знаете, как обойти эту проблему)

Don't be so radical. Stay cool.

#cppcore #memory #design
👍199🔥9
​​мьютекс vs семафор
#новичкам

Мьютексы довольно часто используют при разработке потокобезопасных модулей. Хоть их использование в высокопроизводительных приложениях нежелательно, они все равно являются базовым выбором для реализации наивной потокобезопасности. Более менее все про них знают.

А вот про семафоры - не все. В стандарт их добавили только в С++20 в виде std::counting_semaphore и std::binary_semaphore. Да и в принципе они не так часто используются.

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

Аналогия

Мьютекс - это дверь в очень маленькую туалетную комнату. Когда она свободна, любой может в нее войти. Любой, но только один. Как только кто-то вошел, все остальные начинают выстраиваться в очередь и ждать освобождения комнаты. А освободить комнату может только тот, кто в нее вошел.

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

Для чего используется

Мьютекс - судя из названия mutual exclusion - взаимное исключение. Применяется, когда только один поток в один момент времени может получить доступ к разделяемому ресурсу.

Семафор же просто контролирует количество ресурсов и не дает уйти в минуса.

Низкоуровневое представление

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

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

Владелец

У мьютекса есть владелец - тот, кто захватил замок, должен его отпустить. Если каким-то образом в коде это условие нарушается - сразу ub.

У семафора же нет никаких ограничений - любой поток может накручивать и скручивать счетчик.

Примеры

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

std::mutex mtx;
std::map<std::string, int> cache;
void update_cache(const std::string& key, int value) {
std::lock_guard<std::mutex> lock(mtx);
cache[key] = value;
}


Это все конечно нужно обернуть в класс, но суть понятна и так.

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

template<typename T, size_t N>
class BoundedQueue {
std::queue<T> queue_;
std::counting_semaphore<N> empty_slots_{N};
std::counting_semaphore<N> filled_slots_{0};
std::mutex mtx_;
public:
void push(T value) {
empty_slots_.acquire();
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(value));
}
filled_slots_.release();
}
T pop() {
filled_slots_.acquire();
T value;
{
std::lock_guard<std::mutex> lock(mtx_);
value = std::move(queue_.front());
queue_.pop();
}
empty_slots_.release();
return value;
}
};


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

Лайк, если понравилось. Да и если не понравилось, тоже ставьте.

Compare things. Stay cool.

#concurrency #cpp20 #cpp11
50👍32🔥7😁3
make_unique и приватный конструктор
#новичкам

Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями std::make_unique и std::make_shared у вас могут быть проблемы.

struct Type {
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>();
}
private:
Type() = default;
};

int main()
{
auto obj = Type::Create();
}


Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.

Это можно обойти, просто использовав явный вызов new:

struct Type {
static std::unique_ptr<Type> Create() {
return std::unique_ptr<Type>(new Type);
}
private:
Type() = default;
};

int main()
{
auto obj = Type::Create();
}


Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!

Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:

class Type {
class PrivateKey { // private struct of Type
PrivateKey() = default;
friend Type;
};
public:
Type(PrivateKey) {}

static std::unique_ptr<Type> Create() {
return std::make_unique<Type>(PrivateKey{});
}
};

int main() {
auto obj = Type::Create(); // OK

auto obj2 = Type(PrivateKey{}); // ERROR: PrivateKey is private
}


По сути, это та же идиома passkey из этого поста. Только здесь она раскрывается с еще одной стороны.

Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!

Know your enemies. Stay cool.

#cppcore #design #memory
👍22🔥97
​​WAT
#новичкам

Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Посмотрите на этот код и скажите, что выведется на экран:

std::string_view crop_string_view(std::string_view str_view)
{
return std::string_view{str_view.begin() + 5};
}

int main()
{
const char* str = "some super mega long string";
std::string_view str_view = {str, 10};
std::cout << crop_string_view(str_view);
}


Складывается довольно уверенное ощущение, что мы берем первые 10 символов строки str и после этого отрезаем от этой подстроки первые 5 символов. И в итоге выведется "super".

Однако ваш компилятор думает иначе и выведется на самом деле вот что:

super mega long string

ЧЗХ? str_view же содержит обезанную строку! Откуда там изначальная последовательность символов?

Дело в том, что str_view конечно не содержит никакую строку. Этот объект грубо говоря лишь ссылается на оригинальную строку с ограничениями на длину, которую мы задали в конструкторе.

И конечно вполне естественно на первый взгляд подумать, что std::string_view{str_view.begin() + 5} здесь обрезается сама подстрока. Но это не так.

Конструктор string_view от одного аргумента формирует вьюху от переданного итератора на начало строки и идет дальше прям до символа конца строки. str_view.begin() и str.begin() ничем не отличаются, это фактически тот же самый указатель на начало супер длинной строки. Поэтому и остановится конструктор в конце этой строки и на консоль выведется "super mega long string".

Поэтому если вы создаете std::string_view не от строкового литерала, то указывайте в конструкторе либо длину, либо итератор на конец последовательности.

Specify your boundaries. Stay cool.

#cpp17
126👍10🔥5🥱4❤‍🔥31
​​Loop unrolling. Мотивация.
#новичкам

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

Компилятор позволяет нам думать о логике, в то время как сам заботится о перформансе.

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

Сегодня и дальше мы поговорим про одну из таких оптимизаций - loop unrolling или развертывание цикла.

В чём проблема обычного цикла?

Возьмём простейшую задачу: просуммировать элементы массива.

int sum = 0;
for (int i = 0; i < 1024; ++i) {
sum += arr[i];
}


На каждый проход цикла процессор делает:

1️⃣ Загружает arr[i]
2️⃣ Прибавляет к sum
3️⃣ Увеличивает i
4️⃣ Сравнивает i с 1024
5️⃣ Если не конец — прыгает обратно

Можно было просуммировать элементы вот так:

int sum = 0;
sum += arr[0];
sum += arr[1];
sum += arr[2];
...
sum += arr[1023];


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

Так вот шаги 3–5 — это чистые накладные расходы (overhead) на использование этой абстракции. На 1024 итерациях мы теряем 1024 сравнения и 1024 условных перехода. Все это вносит свой, да мизерный, но вклад, в просадку перфа.

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

Полностью получить все плюшки и убрать все минусюшки не получится. Но получится смешать два подхода. Будем делать цикл не по каждой итерации i, а через 4 числа:

int sum = 0;
for (int i = 0; i < 1024; i += 4) {
sum += arr[i];
sum += arr[i+1];
sum += arr[i+2];
sum += arr[i+3];
}


Теперь на 256 итерациях мы делаем те же 1024 сложения, но сравнений и переходов в 4 раза меньше.

Круто? Круто. Это и называется loop unrolling.

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

Получается такой трейдоф: ускоряем код за счет красоты кода и увеличения бинаря. Классический баланс используемой памяти и скорости кода.

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

Optimize your performance. Stay cool.

#compiler #performance
👍3317🔥6🥱2
Ты такой стараешься, оптимизируешь код, а компилятор просто плюет на него и делает то, что ему вздумается..
Чертова железяка!
46😁39🔥8👍3
Особый день

Сегодня очень важный и теплый для нашей страны праздник - День Победы.

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

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

Любой знаковый день - повод сделать что-то. Накидываем беспроигрышный вариант.

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

С праздником, дорогие подписчики! Благодарность свернет горы.

Tip your hat to your ancestors. Stay cool.
112❤‍🔥31🔥17👎7🤪2
​​joinable
#опытным

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

В деструкторах классов, которые в качестве поля содержат поток, часто можно найти такую конструкцию:

if (worker_.joinable())
worker_.join();


Полноценно это может выглядеть так:

class BackgroundWorker {
std::atomic<bool> stop_{false};
std::thread worker_;
public:
BackgroundWorker() {
worker_ = std::thread([this] {
while (!stop_.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// do something important
}
});
}
~BackgroundWorker() {
stop_ = true;
if (worker_.joinable())
worker_.join();
}
BackgroundWorker(const BackgroundWorker&) = delete;
BackgroundWorker& operator=(const BackgroundWorker&) = delete;
};


Зачем нужен этот joinable? Как это поток может не присоединиться? К чему может привести присоединение не joinable потока? На все эти вопросы сейчас ответим.

Проверка joinable() перед вызовом join() в деструкторе класса - это защита от ошибок, связанных с некорректным состоянием потока. Если вызвать join() для не joinable потока, будет сгенерировано исключение std::system_error, чего нам явно не хочется получить в такой важный момент.

Какой поток нельзя присоединять?


1️⃣ Созданный дефолтным конструктором. std::thread thd; - объект потока конечно есть, но никакого реального потока запущено не было. Поток должен быть успешно запущен, чтобы быть в состоянии joinable.

2️⃣ Перемещенный объект. В ту же степь. Перемещенный объект std::thread не связан больше ни с каким потоком, поэтому и находится в неприсоединяемом состоянии.

3️⃣ Ранее присоединенный объект. Если тебя уже присоединили, то ты уже не можешь бы joinable.

4️⃣ Отсоединенный. Если у объекта вызвали метод detach() и отсоединили поток исполнения от этого объекта, то присоединить его совсем не получится.

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

Но все-таки это идиоматическая конструкция, которую просто надо запомнить и использовать "на всякий случай".

В будущем вы скорее всего будете менять этот класс. И, например, добавите в апи метод Wait(), который явно дожидается завершения потока. Или вообще отсоедините поток.

Никто не застрахован от изменений в будущем, поэтому уже заранее нужно заложить безопасное разрушение объекта.

Ну или используйте C++20 std::jthread и забудьте про эту конструкцию)

Be safe. Stay cool.

#concurrency
🔥2111👍7
Одна из проблем решения задачек с литкода – отсутствие учителя

Либо ты гений, нарешавший кучу задач, и сам придумываешь решение, либо застреваешь часами в попытке выработать оптимальный алгоритм, либо просто смотришь готовое решение. Не самые завидные варианты.

Но не одним литкодом едины. В свое время Яндекс создал для своих разработчиков онлайн-тренажер, а потом открыл доступ для всех желающих попрактиковаться в решении задач. Недавно в CodeRun появилась новая фича в виде AI-помощника на базе SourceCraft – Кодерун AI.

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

Чтобы попробовать, заходите в задачи на CodeRun и открывайте вкладку «Кодерун AI». Пока фича в бета-режиме, нужна авторизация, а лимит — 20 запросов в сутки.
👍17🔥75👎4
​​std::terminate
#опытным

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

Большинство из вас скорее всего знает, что вызов std::terminate приводит к завершению программы из-за какой-то ошибки.

Но как именно приводит? И почему это очень плохо? - На эти вопросы постараемся сегодня ответить.

Термин "ошибка" - слишком неоднозначный. На самом деле есть как минимум 2 категории ошибок: от которых можно восстановиться и от которых нельзя.

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

Ситуации же из второй категории намного серьезней. Вы в принципе нарушаете правила исполнения программы. Выход за границы массива - сегфолт. Слишком глубокая рекурсия - переполнение стека.

Таких ситуаций можно придумать много, и в каком-то небольшом их подмножестве вызывается функция std::terminate.

[[noreturn]] void terminate() noexcept;


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

Внутренний механизм вызова std::terminate() достаточно общий для такого рода функций. std::terminate() не выполняет всю работу сама. Вместо этого она вызывает обработчик события "уничтожения программы". Обработчик настраивается через функцию std::set_terminate и по умолчанию там стоит вызов std::abort. А это уже функция которая немедленно посылает сигнал SIGABRT текущему процессу. Стандартная реакция на этот сигнал — аварийное завершение программы.

В чем проблемы такого завершения программы? Ну помимо того, что мы уже довольно сильно накосячили, что вызвался std::terminate.

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

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

Recover from your mistakes. Stay cool.

#cppcore
👍259🔥5🤣1