Пользовательские литералы. А зачем?
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.
Поехали:
🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:
Меньше деталей, больше фокуса на происходящем.
🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании
Получится неожиданный результат, даже если функция работает верно.
Вот шобы такого не было, можно использовать соответствующие литералы:
🥨 Автоматический вывод типов может легкосломаться вывести не тот тип, который вы ожидаете, если вы работаете с сырыми литералами. Пользовательский литерал же сразу на месте конструирует нужный объект и компилятор будет правильно интерпретировать его тип.
Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:
Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.
🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:
И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.
В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.
Be useful. Stay cool.
#cppcore #cpp11 #cpp20
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать 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
👍16❤9🔥7😁1🤯1
Забавный факт про std::unordered_map
#опытным
std::unoredered_map обязана работать на базе хэш-таблицы, чтобы удовлетворить требованиям по асимптотической сложности ее операций.
А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранит связный список элементов, у которых одинаковый ключ.
При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):
Но это не единственный способ итерироваться по мапе!
У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:
Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!
Вывод:
Пользы в этом немного, но может помочь, например, в отладке своей кастомном хэш-функции, чтобы добиться равномерного распределения.
Inspect your solutions. Stay cool.
#cpp11
#опытным
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😁17❤8👍7🤯3❤🔥1
Стандартные пользовательские литералы. Строковые
#новичкам
Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.
Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без underscore'а приятнее визуально.
Вторая особенность - нужно обязательно указывать
В остальном, это те же кастомные литералы, только для стандартных типов. Подразделяются они по базовому типу литерала, к которому приписывается суффикс.
Строковые кастомные литералы
Интересно, что для них операторы принимают 2 параметра: указатель и длину:
Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.
Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:
1️⃣ std::string:
2️⃣ std::string_view:
Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.
У них у обоих есть одна особенность. Так как размер строки передается в оператор и этот размер потом используется для создания объекта, то есть некоторые отличия при создании объектов через конструктор и через оператор:
Во втором случае получилась строка длиннее, чем в первом. Почему?
Для
Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки.
Для
Теперь конструктор знает реальную длину строки и аллоцирует столько памяти, сколько нужно, чтобы поместить весь литерал в строку.
Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.
Остальные стандартные литералы не уместились в ограничения телеги, поэтому будет вторая часть.
See the difference. Stay cool.
#cpp11 #cpp17
#новичкам
Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.
Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без 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 строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.
С длинными строками, которые целиком не влезают на экран, неудобно работать: читать и редактировать. А если у вас настроены линтеры на ограничение длины строки, то все равно придется как-то разбивать на части эту длинную строку.
Можно это делать с помощью символов экранирования, например так:
Но здесь будут проблемы с тем, что вторая часть должна начинаться с самого начала следующей строки, иначе пробелы будут включены в сам литерал. Представьте, что будет с кодом, в котором внутри функций(например для репорта ошибок) будут так разделяться литералы.
Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:
Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.
Ну и теперь понятно, почему пользовательский суффикс применяется к полной конкатенации c-style строки. Фаза конкатенации строковых литералов идет раньше этапа компиляции, на котором определяется значение аргументов оператора. Поэтому аргументом и является уже склеенная строка.
Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.
Кстати, такая склейка есть только у строковых литералов. Цифры в числовых литералах обязательно должны идти подряд:
Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).
Don't break into pieces. Be whole. Stay cool.
#cppcore #cpp11
#новичкам
Спасибо, @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🤯6❤5❤🔥1
union class
#опытным
В прошлом посте мы упомянули, что union - это такой специальный класс. Это что значит, объединение может иметь методы?
Представьте себе, да!
Начиная с С++11 union'ы могут иметь полноценные конструкторы, деструкторы и другие методы.
Но есть ограничения:
👉🏿 не должно быть виртуальных методов
👉🏿 юнион не может быть наследником
👉🏿 юнион не может быть базовым классом
👉🏿 юнион не может хранить ссылочные типы
Во всем остальном - такой же класс!
Но вот как-то не можется мне придумать юзкейсы методов объединения.
Конструкторы и деструкторы нужны, чтобы union мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.
Например:
Попытка скомпилировать это дело приведет к ошибкам:
Плюс объекты хочется перемещать и мувать, там тоже могут быть нетривиальные специальные методы.
Ну а если уж разрешили специальные методы определять, то и обычные разрешили до кучи.
А вы используете методы объединений в своих проектах? Если да, то расскажите зачем оно может понадобиться, будет интересно.
Expand your horizons. Stay cool.
#cppcore #cpp11
#опытным
В прошлом посте мы упомянули, что 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. Выравнивание должно быть степенью двойки(компьютеры у нас все-таки двоичные и все вокруг ее степени крутится).
В прошлом посте я голосновно перечислил выравнивания для базовых типов. Но теперь есть пруфы:
Очень важно еще раз проговорить: размер типа и его выравнивание - это разные вещи. Так уж получилось, что они совпадают у тривиальных типов, но это не так для кастомных. Вот например:
Кстати говоря. 8 - не максимальный размер выравнивания, некоторые типы требуют большего числа:
double на самом деле не самую большую точность имеет из стандартных типов. Есть тип long double, его размер и выравнивание равны 16(на gcc и clang).
Также есть специальный тип std::max_align_t, чьи требования к выравниванию удовлетворяют любому скалярному типу. Тоже 16.
Для sse векторных регистров выравнивание тоже побольше.
Для чего этот оператор может применяться помимо учебных целей - рассмотрим в следующих постах.
Align yourself. Stay cool.
#cppcore #cpp11 #compiler
#опытным
Представим, что мы проектируем дизайн квартиры. Для того, чтобы расставлять предметы, нужно знать их размеры. Но не только это. Есть ещё одна важная характеристика — требования к размещению. Некоторые предметы можно ставить где угодно, другие требуют специального места у стены, третьи должны стоять строго в углу, четвертые прекрасно разместятся у вас в помойке за ненадобностью.
Чтобы дизайн получился хороший и все купленные предметы поместились и органично стояли на свои местах, нужно все эти требования знать.
С мебелью все просто - идешь и меряешь. Или смотришь размеры в онлайн магазине.
С данными так же, для них есть определенные требования к размеру и размещению. Но как их узнать?
Ну размер типа узнать просто - используем оператор 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) = 16double на самом деле не самую большую точность имеет из стандартных типов. Есть тип long double, его размер и выравнивание равны 16(на gcc и clang).
Также есть специальный тип std::max_align_t, чьи требования к выравниванию удовлетворяют любому скалярному типу. Тоже 16.
Для sse векторных регистров выравнивание тоже побольше.
Для чего этот оператор может применяться помимо учебных целей - рассмотрим в следующих постах.
Align yourself. Stay cool.
#cppcore #cpp11 #compiler
1👍30❤10🔥8⚡2
alignas
#опытным
Ну хорошо. Мы поняли, что такое выравнивание, как компилятор добавляет паддинги и как узнать требования к выравниванию для типа.
Да, в основном компилятор управляет выравниваем. Но компилятор не знает всех тонкостей работы вашей программы. И не всегда корректно может выровнять данные.
В мире С++ можно контролировать почти все и alignment не исключение. Мы можем сами своими руками указать компилятору, как нужно выровнять конкретные тип.
Для этого существует C++11 спецификатор alignas. Он применяется к объявления типа или переменной и устанавливает новые требования для их выравнивания.
Сравним:
Без alignas данные выровнены по границе 4, как это нужно типу float. Используя же спецификатор, мы можем изменить требования.
Можно также выровнять данные на основе другого типа:
Заметьте кстати, что в предыдущих примерах размер данных не изменялся, потому что мы выровняли по границе размера данных. Но можно же использовать и более сильные требования:
В этом случае размер данных увеличился в соответствие с усиленным выравниванием и добавлением паддингов.
Выравнивание переменных же аффектит только их адрес, потому что тип уже фиксирован.
Получается, что в определенных случаях мы тратим дополнительную память в угоду выравниванию. Зачем это может быть нужно?
Например, у вас есть массив мьютексов, которым пользуются разные потоки.
У каждого ядра есть свои кэш-линии для того, чтобы быстрее работать с медленной оперативной памятью. И потоки, которые получают доступ к массиву, сохраняют в кэш линию своего ядра этот массив. Получается, что потенциально в нескольких кэш-линиях будут находиться семантически одни и те же расшаренные данные.
Что будет, если одно из ядер изменит хотя бы один мьютекс? Кэш линии других ядер инвалидируются и заново будут загружаться из ОП. А это очень долго. И так будет повторяться при каждом захвате и отпускании замка.
Такая ситуация называется false sharing. Вы ожидаете, что данные не связаны и независимы, а на самом деле операции над одними данными влияют на другие.
В этом случае поможет выравнивание данных по границе размера кэш линии:
Тогда каждый мьютекс будет в своей кэш-линии и операции над одним из них не будут инвалидировать соседние кэш-линии.
Другой пример использования alignas - векторные инструкции sse и avx. Современные процессоры имеют специальные инструкции для обработки нескольких данных одновременно. Этими инструкциями управляются данные в векторных регистрах, их длины - это 128, 256 или 512 бит. Для более быстрой загрузки данные должны быть выровнены по ширине соответствующих векторных регистров:
В общем, очень полезная штука, must have to know.
Have your separate space. Stay cool.
#cppcore #cpp11 #compiler
#опытным
Ну хорошо. Мы поняли, что такое выравнивание, как компилятор добавляет паддинги и как узнать требования к выравниванию для типа.
Да, в основном компилятор управляет выравниваем. Но компилятор не знает всех тонкостей работы вашей программы. И не всегда корректно может выровнять данные.
В мире С++ можно контролировать почти все и 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'у данных.
А что если меня это все достало и я просто хочу понятной человеческой укладки данных? Подряд, без всяких паддингов непонятных. Можно так?
В С++ можно все. Ну почти.
Решение лежит за рамками стандарта. Оно и понятно, правила для выравнивания данных в нем очень гибкие, в основном все дается на откуп реализации. И инструменты для мануального управления выравниванием тоже находятся в руках конкретных компиляторов.
✅
В общем виде
-
-
-
На примерах это выглядит так:
Особенность механики работы со стеком в том, что мы можем применять одинаковое выравнивание для целого набора структур одной директивой.
✅
Удобно, если не нужно сложной логики.
✅ У нас же есть стандартный плюсовый синтаксис атрибутов. Давайте его и используем. В C++11 и новее также можно написать
Самая главная причина использовать плотную упаковку данных - это когда вам нужно в точности соответствовать компоновке данных в языке какому-либо требованию на уровне битов (аппаратура или протокол) и для этого требуется нарушить обычное выравнивание.
Большинство сетевых протоколов определяют строгую последовательность полей без лишних байтов. Использование упакованных структур позволяет им в точности соответствовать спецификации.
Но надо обязательно помнить про досуп к невыровненным данным: на некоторых архитектурах это в принципе приводит к ошибкам, а на других - к потенциальной деградации производительности(но это не точно, надо мерять).
Align yourself. Stay cool.
#cpp11 #compiler #NONSTANDARD
#новичкам
Мы все про выравнивание, да про выравнивание. Куча средств языка, которые позволяют грамотно работать с требованиями к 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
51❤22👍9🔥5😁5
Где аллоцируются элементы std::array?
#новичкам
Вроде бы простой вопрос с собеседования, но он имеет 2 уровня погружения. В зависимости от того, как вы ответите, интервьюер будет по-разному воспринимать глубину ваших знаний.
1 уровень
Элементы std::array аллоцируются на стеке. std::array - это очень тонкая обертка над сишными массивами, в которых элементы располагаются именно на стеке.
Это очень легко проверить, достаточно сравнить размеры двух арреев с разным количеством элементов:
Два массива на 10 и на 20 элементов, каждый элемент занимает по 4 байта. И все сходится: размер
2 уровень
Вообще говоря, std::array - это обычный объект. Объекты можно размещать на стеке, а можно и на ......... правильно, куче.
Делается это очень просто:
Один new и на стеке уже лежит лишь указатель, а сами элементы массива хранятся на куче.
Прям вот в таком виде вряд ли кто-то работать с std::array.
Но если std::array является полем какого-то класса, объекты которого аллоцируются на куче, то и элементы массива тоже будут на куче располагаться.
Поэтому ответ на вопрос из заголовка поста - где хотим, там и располагаем. А если чуть менее пафосно, но более точно - там, где аллоцирутся сам объект std::array.
Reach advanced levels. Stay cool.
#cppcore #cpp11 #memory #interview
#новичкам
Вроде бы простой вопрос с собеседования, но он имеет 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.
У семафора же нет никаких ограничений - любой поток может накручивать и скручивать счетчик.
Примеры
Мьютекс нужен для ограничения доступа потоков к критической секции. Например у вас есть какой-то кэш и вы хотите его потокобезопасно обновить:
Это все конечно нужно обернуть в класс, но суть понятна и так.
Семафор же нужен для ограничения количество одновременно используемых ресурсов.
Например, ограниченная потокобезопасная очередь:
Здесь семафоры нужны для того, чтобы в очередь не положили больше элементов, чем нужно, и не забрали больше, чем в очереди может потенциально быть. Обратите внимание на порядок инкремента и декремента.
Лайк, если понравилось. Да и если не понравилось, тоже ставьте.
Compare things. Stay cool.
#concurrency #cpp20 #cpp11
#новичкам
Мьютексы довольно часто используют при разработке потокобезопасных модулей. Хоть их использование в высокопроизводительных приложениях нежелательно, они все равно являются базовым выбором для реализации наивной потокобезопасности. Более менее все про них знают.
А вот про семафоры - не все. В стандарт их добавили только в С++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