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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Удобно превращаем enum в число
#опытным

В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.

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

Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:

enum class ColorMask : std::uint32_t
{
red = 0xFF,
green = (red << 8),
blue = (green << 8),
alpha = (blue << 8)
};

// std::cout << ColorMask::red << std::endl; // ERROR
std::cout << static_cast<int>(ColorMask::red) << std::endl;


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

Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.

С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:

enum e1 {};
enum class e2 {};
enum class e3 : unsigned {};
enum class e4 : int {};

constexpr bool e1_t = std::is_same_v<std::underlying_type_t<e1>, int>;
constexpr bool e2_t = std::is_same_v<std::underlying_type_t<e2>, int>;
constexpr bool e3_t = std::is_same_v<std::underlying_type_t<e3>, int>;
constexpr bool e4_t = std::is_same_v<std::underlying_type_t<e4>, int>;

std::cout
<< "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n';

// OUTPUT
// underlying type for 'e1' is non-int
// underlying type for 'e2' is int
// underlying type for 'e3' is non-int
// underlying type for 'e4' is int


Соответственно, для каста нужно сделать такую штуку:

auto num = static_cast<std::underlying_type_t<ColorMask>>(ColorMask::red);


Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:

auto num = std::to_underlying(ColorMask::red);


Красота!

Know your type. Stay cool.

#cpp11 #cpp23
👍21🔥1610🥱1
Мувать не всегда дешево
#новичкам

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

Но мир не такой уж солнечный и приветливый. Это очень опасное...

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

Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.

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

void Process(Data data);


Что выбрать?

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

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

Но это не так! Перемещение - это поверхностное копирование.

Возьмем простой пример:

struct Data {
int a;
double b;
};

Data obj1{3, 3.14};
Data obj2 = std::move(obj1);


Что будет происходить при перемещении obj1? Копирование a и b.

Чуть сложнее:

struct Data {
std::array<int, 5> arr;
};

Data obj1{.arr = {1, 2, 3, 4, 5}};
Data obj2 = std::move(obj1);


Что будет при перемещении obj1, а значит и arr? Тоже копирование! std::array - это массив, фиксированного размера, расположенный на стеке. Как вы собираетесь его перемещать в другой объект? Под другой объект уже выделена своя память на стеке, вы не можете один кусок стека переместить в другой. Вы можете только скопировать значения.

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

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

struct Data {
std::string * str;
// member functions for making it work properly
};

Data obj1{.str = new std::string("Hello, World!")};
Data obj2 = std::move(obj1);


obj2 теперь имеет такое же значение указателя str, как и obj1, но сама строка оказалась нетронутой.

Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.

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

Теперь снова актуализируем вопрос: мувать или копировать?

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

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

Think logically. Stay cool.

#cppcore #cpp11
3👍4117🔥7😎5
​​Передача владения
#новичкам

Захотелось совсем немного развить тему предыдущего поста.

В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.

void bar(std::vector<int>&& vec) {
// do nothing
}
void foo() {
std::vector<int> vec = {1, 2, 3};
bar(std::move(vec));
}


Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.

Другой пример:

std::vector<int> double_elements(std::vector<int> vec) {
for (auto& elem: vec) {
elem *= 2;
}
return vec;
}

void foo() {
std::vector<int> vec = {1, 2, 3};
{
auto doubled = double_elements(vec);
std::println("{}", doubled);
}
vec.push_back(4);
{
auto doubled = double_elements(std::move(vec));
std::println("{}", doubled);
}
}


Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.

Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.

Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.

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

Give away what you don't need. Stay cool.

#cppcore #cpp11
19👍13🔥6
​​Время жизни и range-based for
#новичкам

Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?

Перед ответом экскурс в стандарт. Есть у вас range-based for:

for(const auto& item: <range>) {
process(item);
}


Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:

auto&& range = <range>;
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
process(item);
}


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

std::vector<int> generate() {
return {1, 2, 3, 4, 5};
}
for (const auto& item: generate()) {
...
}
// range-based for transforms into
auto&& range = generate();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
...
}

В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.

А вот во что преобразуется цикл из самого первого примера поста:

auto&& range = generateData().items();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
int x = *it;
process(x);
}


range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.

Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

auto data = generateData();
for (int x : data.items()) {
process(x);
}


Другие решения рассмотрим в следующих постах.

Avoid dangling references. Stay cool.

#cppcore #cpp11
34🔥23👍8😁5
​​Предотвращаем висячие ссылки
#опытным

Давайте снова взглянем на этот пример:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:

auto& vec = generateData().items();


Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.

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

Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.

Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.

Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.

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

И это действительно решает проблему.

Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}

~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.

Это также прекрасно решает проблему.

Prevent misuse. Stay cool.

#cpp11 #cpp20 #cpp23 #goodpractice
🔥2116👍9🤯3
​​Как получить длину строкового литерала?
#опытным

Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод size:

size_t length = std::string("Hello, subscribers!").size();


Ну или на худой конец вызовем strlen:

size_t length = strlen("Hello, subscribers!");


Но я считаю, это не по-современному.

С++ давно идет в сторону расширения возможностей вычислений в compile-time. Поэтому если что-то можно вычислить во время компиляции, то это нужно сделать именно там! Ни грамма лишнего времени вычислений не потратим.

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

1️⃣ Кастомщина. Хочешь что-то сделать хорошо, сделай это сам. Не факт, что получится хорошо, но ты старался:

template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return N - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");


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

2️⃣ Используем sizeof. Этот оператор возвращает длину массива во время компиляции. Единственное, что он считает терминирующий символ, поэтому все равно вокруг него надо обертку писать, чтобы единичку нигде не потерять:

template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return sizeof(str) - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");


Эх, а так хотелось готового get-to-go решения. Погодите...

3️⃣ Обернуть не в строку, а в string_view и вызвать метод size(). Конструкторы вьюхи изначально с С++17 были constexpr, как и сам метод size(), поэтому просто берем и пишем:

constexpr size_t len = std::string_view("Hello, subscribers!").size();


Просто, работает из коробки и знакомо всем.

4️⃣ Да зачем что-то менять в коде, это для слабаков. Поменяем стандарт и все заработает в compile-time! Ну точнее конструктор std::string и метод size() в С++20 теперь тоже constexpr:

constexpr size_t len = std::string("Hello, subscribers!").size();


Пысы: я не просто вызываю какие-то функции в надежде, что они выполнятся в compile-time. Тот факт, что len - constexpr переменная, требует, чтобы компилятор вычислил выражение справа во время компиляции.

5️⃣ Тот пункт, который и вдохновил на написание этого поста. Все пункты выше либо надо было самим реализовывать, либо вот какие-то обертки, чтобы хакнуть систему и на самом деле не работать с литералами.

Но не так плюсы бедны на стандартные решения. Есть стандартная С++17 функция std::char_traits<char>::length. Она может работать в compile-time, имеет явную семантику вычисления длины и работает чисто с c-style строками:

constexpr size_t len = std::char_traits<char>::length("Hello, subscribers!");


Красиво? Ну а что вы от плюсов хотели?) Зато из коробки работает.

6️⃣ Пользовательские литералы. Еще один неординарный способ. С С++11 мы имеем возможность превращать численные и строковые литералы в пользовательские объекты с помощью дописывания суффикса. Прикольно же писать:

constexpr auto length = "Hello, subscribers!"_len;


Коротко и понятно. Для этого нужно лишь определить оператор преобразования:

constexpr size_t operator"" _len(const char* str, size_t n) {
return n;
}


и теперь вы свободны от угнетения оберток.

Если есть еще идеи, кидайте в комменты, будет интересно.

Don't be oppressed. Stay cool.

#cpp11 #cpp17 #cpp20
🔥45👍189🤯1
Пользовательские литералы
#новичкам

Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.

В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:

👉🏿 Целочисленные: 5, 42, 0xFF.

👉🏿 С плавающей точкой: 3.14, 6.02e23.

👉🏿 Символьные: 'a', '\n'.

👉🏿 Строковые: "Hello, world!".

👉🏿 Логические: true, false.

👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя - nullptr.

Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.

Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.

А мы живем все-таки в мире объектов
. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.

Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.

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

Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!

Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:

class Temperature {
private:
double kelvin; // for precicion and consistency

explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};

Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}

{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}


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

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

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

Extend your capabilities. Stay cool.

#cppcore #cpp11
25👍14🔥11🤯2🤷‍♂1🤪1
​​Пользовательские литералы. А зачем?
#опытным

В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.

Поехали:

🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:

auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;


Меньше деталей, больше фокуса на происходящем.

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

double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree


Получится неожиданный результат, даже если функция работает верно.

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

class Radian {...};

Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}

SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error


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

Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:

using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char *>);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);


Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.

🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:

template<size_t N>
struct FixedString {
char data[N];

constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}

constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};

template <FixedString str>
class Class {};

Class<"Hello World!"> cl;


И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.

В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.

Be useful. Stay cool.

#cppcore #cpp11 #cpp20
👍1610🔥7😁1🤯1
Забавный факт про std::unordered_map
#опытным

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

А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранит связный список элементов, у которых одинаковый ключ.

При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):

std::unoredered_map<std::string, int> map = ...;
for (const auto& [key, value]: map) {
...
}


Но это не единственный способ итерироваться по мапе!

У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:

local_iterator begin( size_type n );
local_iterator end( size_type n );


Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!

std::unordered_map<std::string, int> word_count = {
{"AI", 5}, {"evil", 7}, {"banana", 3},
{"date", 2}, {"elderberry", 4}
};

// Iterate over backets
for (size_t i = 0; i < word_count.bucket_count(); ++i) {
std::cout << "Bucket " << i << " ("
<< word_count.bucket_size(i) << " elements): ";

// Iterate inside certain backet
for (auto it = word_count.begin(i); it != word_count.end(i); ++it) {
std::cout << "[" << it->first << ":" << it->second << "] ";
}
std::cout << std::endl;
}


Вывод:

Bucket 0 (0 elements): 
Bucket 1 (0 elements):
Bucket 2 (2 elements): [date:2] [evil:7]
Bucket 3 (0 elements):
Bucket 4 (0 elements):
Bucket 5 (2 elements): [elderberry:4] [banana:3]
Bucket 6 (0 elements):
Bucket 7 (0 elements):
Bucket 8 (0 elements):
Bucket 9 (0 elements):
Bucket 10 (0 elements):
Bucket 11 (1 elements): [AI:5]
Bucket 12 (0 elements):


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

Inspect your solutions. Stay cool.

#cpp11
🔥41😁179👍7🤯3❤‍🔥1
​​Стандартные пользовательские литералы. Строковые
#новичкам

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

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

Вторая особенность - нужно обязательно указывать using namespace std::literals помимо включения нужных хэдэров. Кастомный оператор - это по сути обычная функция. И при вызове функции из какого-то пространства имен(а все стандартное лежит как минимум в неймспейсе std) мы должны перед именем функции указать это пространство. Но как вы это сделаете с оператором? Да никак. Поэтому явно нужно использовать в своем коде неймспейс. Он общий для всех стандартных операторов, но есть еще и подпространства под конкретные их группы.

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

Строковые кастомные литералы

Интересно, что для них операторы принимают 2 параметра: указатель и длину:

( const char*, std::size_t )


Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.

Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:

1️⃣ std::string:
constexpr std::string operator""s(const char* str, std::size_t len);

using namespace std::literals;
auto str = "Hello, World!"s;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string>);


2️⃣ std::string_view:
constexpr std::string_view
operator ""sv(const char* str, std::size_t len) noexcept;

using namespace std::literals;
auto str = "Hello, World!"sv;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string_view>);


Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.

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

void print_with_zeros(const auto note, const std::string& s) {
std::cout << note;
for (const char c : s)
c ? std::cout << c : std::cout << "₀";
std::cout << " (size = " << s.size() << ")\n";
}
int main() {
using namespace std::string_literals;

std::string s1 = "abc\0\0def";
std::string s2 = "abc\0\0def"s;
print_with_zeros("s1: ", s1);
print_with_zeros("s2: ", s2);
}

// OUTPUT:
// s1: abc (size = 3)
// s2: abc₀₀def (size = 8)


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

Для s1 вызывается конструктор от одного аргумента:

basic_string( const CharT* s, const Allocator& alloc = Allocator() );


Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки.

Для s2 вызывается конструктор от двух аргументов:

basic_string( const CharT* s, size_type count,
const Allocator& alloc = Allocator() );


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

Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.

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

See the difference. Stay cool.

#cpp11 #cpp17
34🔥13👍8😁7🤯2🤔1💯1
​​WAT
#новичкам

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

Ответ на квиз из поста выше - на экран выведется 8.

WAT? Строковые литералы конкатенируются? Да еще и пользовательский суффикс между двух литералов применяется к конкатенации?

Вообще, да. Сейчас во всем разберемся.

Для начала. Да, c-style строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.

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

Можно это делать с помощью символов экранирования, например так:

auto str = "Suuuuuuuuuuuuuuupppeeeeeeeeeeeeeeeeeeeeeeeeeeerrrr
loooooooooooooooooooooooooooooong \ striiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";


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

Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:

auto str = "Hello "
// void
"World!";
std::cout << str << std::endl;

// OUTPUT
// Hello World!


Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.

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

Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.

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

int num1 = 123; // OK
int num2 = 12 23 // ERROR
int num3 = 1'234; // if you want to logicaly devide large number


Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).

Don't break into pieces. Be whole. Stay cool.

#cppcore #cpp11
🔥26👍86🤯6❤‍🔥1
​​union class
#опытным

В прошлом посте мы упомянули, что union - это такой специальный класс. Это что значит, объединение может иметь методы?

Представьте себе, да!

Начиная с С++11 union'ы могут иметь полноценные конструкторы, деструкторы и другие методы.

Но есть ограничения:

👉🏿 не должно быть виртуальных методов

👉🏿 юнион не может быть наследником

👉🏿 юнион не может быть базовым классом

👉🏿 юнион не может хранить ссылочные типы

Во всем остальном - такой же класс!

Но вот как-то не можется мне придумать юзкейсы методов объединения.

Конструкторы и деструкторы нужны, чтобы union мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.

Например:

union U {
int i;
float f;
std::string s;
};

U u;


Попытка скомпилировать это дело приведет к ошибкам:
error: use of deleted function 'U::U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string()
requires is_default_constructible_v<_Alloc>

error: use of deleted function 'U::~U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::~basic_string()


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

union U {
U() {}
~U() {}
int i;
float f;
std::string s;
};

U u; // ОК


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

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

Expand your horizons. Stay cool.

#cppcore #cpp11
19👍8🔥8
alignof
#опытным

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

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

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

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

Ну размер типа узнать просто - используем оператор sizeof, это все знают.

А требования к выравниваю как узнать?

Для этого есть C++11 оператор alignof. Он возвращает требуемое выравнивание для типа в байтах. Это значит, что адрес начала объекта данного типа был кратен результату alignof. Выравнивание должно быть степенью двойки(компьютеры у нас все-таки двоичные и все вокруг ее степени крутится).

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

std::println("char требует выравнивания: {} байт", alignof(char));
std::println("short требует выравнивания: {} байт", alignof(short));
std::println("int требует выравнивания: {} байт", alignof(int));
std::println("size_t требует выравнивания: {} байт", alignof(size_t));
std::println("float требует выравнивания: {} байт", alignof(float));
std::println("double требует выравнивания: {} байт", alignof(double));
std::println("void* требует выравнивания: {} байт", alignof(void*));
// OUTPUT:
// char требует выравнивания: 1 байт
// short требует выравнивания: 2 байт
// int требует выравнивания: 4 байт
// size_t требует выравнивания: 8 байт
// float требует выравнивания: 4 байт
// double требует выравнивания: 8 байт
// void* требует выравнивания: 8 байт


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

std::println("std::string: размер={}, выравнивание={}", sizeof(std::string), alignof(std::string));
std::println("std::vector<int>: размер={}, выравнивание={}", sizeof(std::vector<int>), alignof(std::vector<int>));
// OUTPUT:
// std::string: размер=32, выравнивание=8
// std::vector<int>: размер=24, выравнивание=8


Кстати говоря. 8 - не максимальный размер выравнивания, некоторые типы требуют большего числа:

std::println("alignof(long double) = {}", alignof(long double));
std::println("alignof(std::max_align_t) = {}", alignof(std::max_align_t));
std::println("alignof(__m128) = {}", alignof(__m128));
// OUTPUT
// alignof(long double) = 16
// alignof(max_align_t) = 16
// alignof(__m128) = 16


double на самом деле не самую большую точность имеет из стандартных типов. Есть тип long double, его размер и выравнивание равны 16(на gcc и clang).

Также есть специальный тип std::max_align_t, чьи требования к выравниванию удовлетворяют любому скалярному типу. Тоже 16.

Для sse векторных регистров выравнивание тоже побольше.

Для чего этот оператор может применяться помимо учебных целей - рассмотрим в следующих постах.

Align yourself. Stay cool.

#cppcore #cpp11 #compiler
1👍3011🔥82
​​alignas
#опытным

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

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

В мире С++ можно контролировать почти все и alignment не исключение. Мы можем сами своими руками указать компилятору, как нужно выровнять конкретные тип.

Для этого существует C++11 спецификатор alignas. Он применяется к объявления типа или переменной и устанавливает новые требования для их выравнивания.

Сравним:

struct Vector4 {
float x, y, z, w;
};
struct alignas(16) Vector4Aligned {
float x, y, z, w;
};

std::cout << "Alignment of Vector4 is " << alignof(Vector4) << std::endl;
std::cout << "Alignment of Vector4Aligned is " << alignof(Vector4Aligned) << std::endl;

float data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
alignas(32) float aligned_data[8] = {1, 2, 3, 4, 5, 6, 7, 8};

std::cout << "Alignment of data is " << alignof(data) << std::endl;
std::cout << "Alignment of aligned_data is " << alignof(aligned_data) << std::endl;
// OUTPUT:
// Alignment of Vector4 is 4
// Alignment of Vector4Aligned is 16
// Alignment of data is 4
// Alignment of aligned_data is 32


Без alignas данные выровнены по границе 4, как это нужно типу float. Используя же спецификатор, мы можем изменить требования.

Можно также выровнять данные на основе другого типа:

struct alignas(float) struct_float
{
// ...
};


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

struct alignas(32) Vector4AlignedTooMuch {
float x, y, z, w;
};

std::cout << "Alignment of Vector4AlignedTooMuch is "
<< alignof(Vector4AlignedTooMuch) << ", and size is "
<< sizeof(Vector4AlignedTooMuch) << std::endl;
// OUTPUT:
// Alignment of Vector4AlignedTooMuch is 32, and size is 32


В этом случае размер данных увеличился в соответствие с усиленным выравниванием и добавлением паддингов.

Выравнивание переменных же аффектит только их адрес, потому что тип уже фиксирован.

Получается, что в определенных случаях мы тратим дополнительную память в угоду выравниванию. Зачем это может быть нужно?

Например, у вас есть массив мьютексов, которым пользуются разные потоки.

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

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

Такая ситуация называется false sharing. Вы ожидаете, что данные не связаны и независимы, а на самом деле операции над одними данными влияют на другие.

В этом случае поможет выравнивание данных по границе размера кэш линии:

struct AlignedMutex
{
alignas(64) std::mutex mutex;
};
std::array<AlignedMutex, 10> mutexes;


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

Другой пример использования alignas - векторные инструкции sse и avx. Современные процессоры имеют специальные инструкции для обработки нескольких данных одновременно. Этими инструкциями управляются данные в векторных регистрах, их длины - это 128, 256 или 512 бит. Для более быстрой загрузки данные должны быть выровнены по ширине соответствующих векторных регистров:

alignas(32) float aligned_data[8] = {1.0f, 2.0f, 3.0f, 4.0f, 
5.0f, 6.0f, 7.0f, 8.0f}
__m256 avx_reg = _mm256_load_ps(aligned_data);


В общем, очень полезная штука, must have to know.

Have your separate space. Stay cool.

#cppcore #cpp11 #compiler
23🔥12👍8🎉2👏1
​​Плотно упаковываем данные
#новичкам

Мы все про выравнивание, да про выравнивание. Куча средств языка, которые позволяют грамотно работать с требованиями к alignment'у данных.

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

В С++ можно все. Ну почти.

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

#pragma pack. Это нестандартная директива препроцессора, которая тем не менее поддерживается большой тройкой компиляторов

В общем виде #pragma pack может использоваться тремя основными способами(будет немного нудно, но дождитесь примеров):

#pragma pack(push, n)
#pragma pack(pop)
#pragma pack(n)


- n - целое число, обычно степень двойки: 1, 2, 4, 8, 16 и т.д. Оно задаёт максимальное выравнивание для каждого члена. Член будет размещён по смещению, кратному min(n, alignof(тип)). Фактически n ограничивает выравнивание сверху.

- push - помещает текущее значение упаковки в стек (сохраняет его). Если после pushуказано n, то сначала сохраняется текущее, а затем устанавливается новое значение.

- pop - извлекает последнее сохранённое значение из стека и восстанавливает его.

На примерах это выглядит так:

struct Test {
char c; // смещение 0
int i; // смещение 4 (3 байта паддинга)
};

static_assert(sizeof(Test) == 8);
static_assert(alignof(Test) == 4);

#pragma pack(push, 1)
struct Packed {
char c; // смещение 0
int i; // смещение 1 (паддинга нет)
};
#pragma pack(pop)

static_assert(sizeof(Packed) == 5);
static_assert(alignof(Packed) == 1);

#pragma pack(push, 2)
struct Packed2 {
char c; // смещение 0
double d; // смещение 2 (1 байт паддинга после c)
};
#pragma pack(pop)

static_assert(sizeof(Packed2) == 10);
static_assert(alignof(Packed2) == 2);


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

attribute((packed)). Этот атрибут поддерживается gcc и clang. Механика у него чуть попроще - полностью убирает паддинги и выставляет выравнивание 1 для самого типа:

struct attribute((packed)) PackedStruct {
char c;
int i;
short s;
};

static_assert(sizeof(PackedStruct) == 7);
static_assert(alignof(PackedStruct) == 1);


Удобно, если не нужно сложной логики.

У нас же есть стандартный плюсовый синтаксис атрибутов. Давайте его и используем. В C++11 и новее также можно написать [[gnu::packed]] и эффект будет такой же, как в предыдущем пункте:

struct [[gnu::packed]] PackedStruct1 {
char c;
int i;
short s;
};

static_assert(sizeof(PackedStruct1) == 7);
static_assert(alignof(PackedStruct1) == 1);


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

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

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

Align yourself. Stay cool.

#cpp11 #compiler #NONSTANDARD
5123👍9🔥5😁5
​​Где аллоцируются элементы std::array?
#новичкам

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

1 уровень

Элементы std::array аллоцируются на стеке. std::array - это очень тонкая обертка над сишными массивами, в которых элементы располагаются именно на стеке.

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

std::array<int, 10> arr{0};
std::array<int, 20> arr1{0};

std::cout << sizeof(arr) << " " << sizeof(arr1) << std::endl;
// OUTPUT:
// 40 80


Два массива на 10 и на 20 элементов, каждый элемент занимает по 4 байта. И все сходится: размер arr 10*4 байт, а второго - 20*4.

2 уровень

Вообще говоря, std::array - это обычный объект. Объекты можно размещать на стеке, а можно и на ......... правильно, куче.

Делается это очень просто:

void* operator new(std::size_t size) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc{};
std::cout << "Allocation has happend" << std::endl;
return ptr;
}

int main() {
auto * ptr = new std::array{0, 1, 2, 3, 4};
delete ptr;
}
// OUTPUT:
// Allocation has happend


Один new и на стеке уже лежит лишь указатель, а сами элементы массива хранятся на куче.

Прям вот в таком виде вряд ли кто-то работать с std::array.

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

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

Reach advanced levels. Stay cool.

#cppcore #cpp11 #memory #interview
33🔥12👍8❤‍🔥2
​​мьютекс 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
51👍33🔥7😁3
​​Специальные методы классов
#новичкам

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

1️⃣ Конструктор по-умолчанию. Дефолтный конструктор без аргументов. Мы уже подробно разбирали его в предыдущих постах, останавливаться не будем.

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

2️⃣ Деструктор. Он семантически разрушает объект. Забирает у него память, закрывает дескрипторы и соединения, высасывает жизнь, отбирает квартиру, машину, жену и собаку и передает это государству операционной системе.

Как так получается, что компилятор вообще способен сам сгенерировать код деструктора объекта, который владеет ресурсами?

Все благодаря RAII и способности деструктора вызывать деструкторы своих полей. Если в вашем классе Foo содержится std::vector или std::unique_ptr, вам не нужно задумываться о менеджменте памяти, авторы этих классов уже все придумали за вас. Вам нужно лишь вызывать их деструктор, что деструктор Foo делает автоматически.

using File = std::unique_ptr<FILE, decltype(&std::fclose)>;

struct Foo {
std::vector<int> numbers;
File file;
// no explicit destructor ~Foo() {}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}, {std::fopen("example.txt", "r"), &std::fclose}};
} // destroy obj here


3️⃣ Копирующий конструктор
4️⃣ Перемещающий конструктор

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

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

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo(const Foo& other) {...}
// Foo(Foo&& other) {...}
};

void foo() {
Foo obj = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo copied_in_obj = obj; // copy obj
Foo moved_in_obj = std::move(obj); // move from obj


5️⃣ Копирующий оператор присваивания
6️⃣ Перемещающий оператор присваивания

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

И вот здесь интересная комбинация.

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

Чуть остановимся на синтаксисе:

Foo a, b;
a = b; // copy assign
a = std::move(b); // move assign

Foo a1 = b; // THIS IS NOT AN ASSIGN


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

Теперь примерчик:

struct Foo {
std::vector<int> numbers;
// no explicit copy/move constructors
// Foo& operator=(const Foo& other) {...}
// Foo& operator=(Foo&& other) {...}
};

void foo() {
Foo forward = {{1, 2, 3, 4, 5, 6, 7, 8, 9}};
Foo backward = {{9, 8, 7, 6, 5, 4, 3, 2, 1}};
Foo obj; // created obj here
obj = forward; // copy assign forward to obj
obj = std::move(backward); // move assign backward to obj


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

Be special. Stay cool.

#cppcore #cpp11 #interview
🔥128👍7
Когда компилятор не сгенерирует 5 специальных методов?
#опытным

После того, как в С++11 появилась семантика перемещения владения ресурсами, появились также объекты, которые единолично владеют определенным ресурсом. Они настолько не хотят им делиться с другими, что семантика копирования для них стала неприемлемой. Поэтому между новыми(перемещающими) и старыми(копирующими) специальными методами классов появилось некое противоборство - отказ компилятора автоматически генерировать определенные методы при определенных условиях.

Вам в любом случае стоит придерживаться правил 0 и 5, когда проектируете интерфейс создания и уничтожения объектов. Но все равно полезно знать, что будет если правила не выполнять.

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

1️⃣ Деструктор

Деструктор компилятор может всегда сгенерировать. Оно в целом понятно: объект может как-то создаваться, поэтому и должен уметь как-то уничтожаться. Если вы не делаете ничего экзотического, компилятор предоставит вам деструктор. Другой вопрос, правильно ли он будет работать.

Банальный пример:

struct Bad {
Bad() : p{new int{5}} {}
Bad(Bad&& other) {
p = other.p;
other.p = nullptr;
}
int * p;
};

{
Bad b;
} // memory leak


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

2️⃣ Конструктор копирования и копирующий оператор присваивания

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

struct Example {
Example() = default;
Example(Example&&) {}
};

Example a;
Example b = a; // Error: copy ctor is implicitly declared as deleted
Example c;
c = a; // Error: copy assign operator is implicitly declared as deleted


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

При этом если вы определили деструктор, то копирующий операции все равно сгенерируются.

struct Bad {
Bad() : p{new int{5}} {}
~Bad() {delete p;}
int * p;
};

{
Bad b;
Bad b1 = b;
} // double free


Простейший пример и сразу же ловим двойное освобождение.
Такое поведение - наследие от более ранних стандартов, когда было правило 3-х, но оно было только на словах. Даже если деструктор делает нетривиальные вещи, то копирующие операции генерировались.

Конечно, это unsafe. Но обратная совместимость заставляет С++ нести эти особенности в новые стандарты.

3️⃣ Конструктор перемещения и перемещающее присваивание

Это те 2 новых специальных метода, которые добавили в С++11. Для новых вещей стандарт может устанавливать новые условия, которые не несут груз ответственности за обратную совместимость.

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

struct Example {
~Example() {}
// or Example(Example&& other) {}
// or Example& operator=(const Example&) {}
// or Example& operator=(Example&&) {}
};

Example a;
Example b;
a = std::move(b); // Error: no move assign


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

Be special. Stay cool.

#cpp11
👍148🔥7
​​Tricky move
#новичкам

В прошлом посте специально была допущена определенная неточность. Скомпилировав этот код:

struct Example {
~Example() {}
};

Example a;
Example b;
a = std::move(b);


Вы не получите ошибку компиляции и все запустится без проблем.

Просто код будет работать не совсем так, как вы ожидаете.

Вы резонным образом ожидаете, что в последней строчке вызовется присваивание перемещением.

Однако вызовется копирующее присваивание.

#ЧЗХ?

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

Но не мешает копирующим!


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

Результат вызова std::move - rvalue reference aka &&. А правые ссылки спокойно биндятся к const &. И именно такой тип параметра принимает копирующее присваивание.

Есть подходящий кандидат для перегрузки, он и вызывается.

Еще один прикол на стыке легаси С++ и его модерновой версией.

See through tricks. Stay cool.

#cpp11
14👍11🔥9❤‍🔥1