Грокаем 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
​​Почему тогда глобальные переменные зануляются?
#новичкам

Внимательные читатели 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
Если хотите повеселиться, то почитайте комменты в последнем подкасте подлодки про С++ с Антоном Полухиным.

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

Естественно, в комментах порвались неженки. Их неустраивает 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
👍258🔥5🤣1