Грокаем 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
​​Пользовательские литералы. А зачем?
#опытным

В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать 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
👍169🔥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😁178👍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
32🔥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👍8🤯65❤‍🔥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
18👍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👍3010🔥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
22🔥11👍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
5122👍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
30🔥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
50👍32🔥7😁3