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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Доступ к приватным членам. Явная инстанциация и друзья
#опытным

Оказывается способов легально залезть в непубличные кишки вашего класса довольно много, и сегодня обсудим еще один метод.

Мы уже с вами говорили про дружественные функции и что им дозволено получать доступ к приватным членами класса.

Но давайте посмотрим на следующий пример:

template <typename T>
struct Foo {
friend void bar() { cout << "Got it!" << endl; }
};

void bar();
template struct Foo<int>;

bar();


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

Давайте проследим, что произошло. Функция bar - по сути свободная функция, которая может использовать все члены Foo. Но не только их. Она еще может использовать шаблонные параметры конкретной инстанциации.

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

Давайте сделаем шаблонный параметр Foo указателем на поле и инстанцируем этот шаблон с указателем на приватное поле класса:

class Private {
private:
int data{};
};

template<int Private::* Member> // pointer to data member
struct Stealer {
friend int& dataGetter(Private& iObj) {
return iObj.*Member;
}
};

template struct Stealer<&Private::data>; // explicit instantiation
int& dataGetter(Private&);

int main() {
Private obj;
dataGetter(obj) = 42;
}


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

И еще один из шаблоны сломали инкапсуляцию. Да что ж это такое!

Спасибо, @SoulslikeEnjoyer, за материалы для поста)

Exploit loopholes. Stay cool.

#cppcore #template #fun
17🔥15❤‍🔥6👍3😱1
​​Бросаем дичь
#новичкам

В С++ есть исключения. Вы можете их любить или ненавидеть, но от этого не сбежать(почти).

Обычно как происходит. Есть стандартный std::exception или любой кастомный базовый класс исключения my_exception::BaseException. У них куча наследников и вот вы их бросаете в подходящих ситуациях.

Но это же С++: "Вы думали, что бросать можно только исключения? Пфф. Не смешите мои подковы и подержите мое пиво."

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

Например так:

throw 1;


Бросаем число. А что, какие-то проблемы?

Или вот так:

throw nullptr;
throw "This is the end!";

void panic() { std::cout << "PANIC!" << std::endl; }
throw static_cast<void(*)()>(panic); // Указатель на функцию


Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.

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

throw []{std::cout << "Things are going really bad...\n"; };


Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.

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

Be amazed. Stay cool.

#cppcore
🔥4015😁9👍5🤯1
Квиз
#опытным

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

Допустим, мы вот хотим создать вектор пар строковых вьюшек и вывести это добро на консоль. Просто? Просто.

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


#include <iostream>
#include <string>
#include <string_view>
#include <vector>


int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};

for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}
👍9🔥54🤔1😭1
Какой результат попытки компиляции и запуска кода выше под С++23?
Anonymous Poll
18%
Ошибка компиляции
31%
one and two\nthree and four
8%
one and three
3%
two and four
40%
Где-то здесь, рядом с собакой, уб зарыто...
😁87👍3🔥3
WAT. История скобок, изменивших все
#опытным

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

Посмотрите еще раз на этот пример:

int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};

for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}


Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:

one and two
three and four


Но у вашего компилятора на это другое мнение. Кланг, например, выводит:

one and three


WAT? А где 2 и 4? И почему вообще 1 элемент?

С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.

Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:

/->/{/<-/{"one", "two"}, {"three", "four"}/->/}/<-/


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

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

Тогда получается, что вью на строку можно создать с помощью {"one", "two"}?!?

Без проблем. Вот вам подходящий конструктор:

template< class It, class End >
constexpr basic_string_view( It first, End last );


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

The behavior is undefined if [first, last) is not a valid range


оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.

Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.

А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.

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

std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{std::pair{"one", "two"}, std::pair{"three", "four"}}
};


Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного pair. Компилятор, следуя правилам инициализации из списка, развернул вложенный список и интерпретировал содержимое как два отдельных элемента для вектора. Можно убедиться тут. Спасибо @Shuomi за комментарий по поводу различного поведения при разных стандартах)

Avoid ambiguity. Stay cool.

#STL #cpp17 #cpp23
30👍14🤯9🔥7
switch
#новичкам

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

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

Но для начала пойдем с начала. switch - это чуть более компактное и продвинутое условие. Скажем, у вас есть переменная типа int и в зависимости от ее значений, вы по разному обрабатываете данные. Пусть у вас есть http статус код и вы на его основе строите какую-то логику:

switch(status_code) {
case 200: // OK
processSuccess();
break;
// ...
case 400: // Bad Request
logClientError(status_code);
break;
// ...
case 500: // Service Unavailable
logServerError(status_code);
break;
// ...
default:
handleUnknownStatus(status_code);
break;
}


То есть 1 переменная - много вариантов развития событий.

Теперь погнали по особенностям:

👉🏿 Вы будете проваливаться в нижележащие кейсы, если не напишите break в конце текущего кейса. Это накладывает свои когнитивные сложности, но дает и возможности. Если у вас одинаково обрабатываются разные значения, то вы можете проваливаться в нижележащие кейсы, пока не дойдете до нужного обработчика и не брякнитесь(встретите break):

switch(status_code) {
case 200: // OK
processSuccess();
break;
case 400: // Bad Request
case 401: // Unauthorized
case 403: // Forbidden
case 404: // Not Found
logClientError(status_code);
break;
}


👉🏿 Не очень удобно в switch работать с диапазонами значений. Скорее всего выразительнее и безопаснее воспользоваться обычными условиями, если у вас обработчики соответствуют длинным диапазонам.

Существуют расширения компилятора, которые позволяют указывать в switch диапазон:

switch(x) {
case 0 ... 9: // GNU Extension
std::cout << "0-9\n";
break;
case 10 ... 19: // GNU Extension
std::cout << "10-19\n";
break;
}


Но это непереносимо и вообще не для этого розочка цвела. switch хорошо оптимизируется через jump table(вместо кучи if-else с линейной сложностью используется массив меток обработчиков, в котором за О(1) ищется нужный), а диапазоны значений в эту концепцию не вписываются.

👉🏿 В С++ очень органично вписаны объекты и пользовательские типы, наряду с тривиальными С-совместимыми типами. Поэтому может сложиться впечатление, что switch может, например, со строками работать. Но нет, он работает только с целочисленными типами, то есть с целыми числами и перечислениями(scoped и unscoped).

Если хотите выбирать обработчики на основе пользовательских типов, нужно использовать ассоциативные контейнеры:

using Handler = std::function<void()>;
std::unordered_map<std::string, Handler> handlers = {
{"start", &start},
{"stop", []{ stop(); }}
};

void execute(const std::string& cmd) {
if (auto it = handlers.find(cmd); it != handlers.end()) {
it->second();
}
}



👉🏿 Значения кейсов должны быть константными выражениями. То есть значениями, известными на этапе компиляции. Вы не можете выбрать обработчик, соотвествующий runtime значению.

При использовании enum'ов такой проблемы в принципе не возникает, но вот с int'ами да:

constexpr int getValue() {
return 10;
}

int main() {
constexpr int y = getValue();
int z = getValue();

int x = 10;

switch (x) {
case y: // OK: y - constant expression
std::cout << "x equals y\n";
break;
case z: // ERROR: z is runtime value
std::cout << "x equals z\n";
break;
default:
std::cout << "default\n";
}

return 0;
}



Вроде простая конструкция, но все равно есть нюансы.

Explore nuances. Stay cool.

#cppcore
21👍11🔥72
Как применять switch?
#новичкам

Мы вспомнили про switch и некоторые его особенности. Теперь посмотрим, в каких сценариях их адекватно применять и как это делать.

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

В прошлом посте мы свичались по http статус кодам. Тогда они были просто числами, так было проще вспомнить механику этой инструкции. Давайте перепишем в нормальный вид:

switch(status_code) {
case HTTPStatus::OK:
processSuccess();
break;
case HTTPStatus::BadRequest:
case HTTPStatus::Unauthorized:
case HTTPStatus::Forbidden:
case HTTPStatus::NotFound:
logClientError(status_code);
break;
case HTTPStatus::InternalServerError:
case HTTPStatus::BadGateway:
case HTTPStatus::ServiceUnavailable:
return logServerError(status_code);
default:
// ...
}


Теперь магических чисел нет.

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

Эти особенности уже так сильно ограничивают применение switch'а. Он идеален в критичных к производительности местах и в плотных целочисленных диапазонах(тогда он хорошо оптимизируется).

Дальше идем.

Чем вообще может быть плох switch?

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

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

👉🏿 Расширение кейсов требует изменения клиентского кода. Опять же, при небольшом количестве веток - ничего страшного. Но с увеличением объема растет количество деталей, которые разработчик держит у себя в голове при чтении кода. У нас и так профессия сложная, давайте разгружать друг другу мозг. Код не должен меняться, если у вас ожидаемо вдруг появился новый кейс.

👉🏿 Много кейсов - много кода. Особенно если писать инструкции обработчиков прям внутри кейсов. Много кода в одном месте - почти всегда очень плохо читается.

Из этих проблем можно сделать несколько выводов:

1️⃣ Если мало кейсов и в обработчиках мало кода(1-2 строчки) оставляйте как есть.

2️⃣ Если обработчики большие, то всегда выносите их в отдельные функции. Помните, что есть метрика "количество строк в функции" и она редко должна превышать 15-20 строк.

3️⃣ Если у вас много кейсов, то вынесите свитч в отдельную функцию с подходящим названием. Тогда вы скроете эту большую простыню из логики кода. И она будет описываться одним вызовом функции, по имени которой будет понятно, что вы хотите сделать.

void dispatch(Editor &editor, Command cmd) {
switch (cmd.type) {
case Command::NewFile:
executeNewFile(editor);
return;
case Command::OpenFile:
executeOpenFile(editor);
return;
// ...
}
}

void processUserInput(Editor &editor, UserInput input) {

if (auto cmd = interpretInput(input)) {
dispatch(editor, *cmd);
}
}


Когда не надо применять switch?

🔞 Пользовательский тип под условием.

🔞 Кейсы накидываются или выкидываются динамически(например из конфигурации)

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

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


Use the right tool. Stay cool.

#cppcore #goodpractice #design
18👍11🔥5🤣5😁1
​​Обзор книжки #3

Начнем с цитаты: "Языки программирования бывают двух видов: которые ругают, и на которых не пишут."

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

Всем геймдевистам, активным и латентным, приготовиться. Потому что именно про это книга Сергея Кушниренко "Game++. Устройство и оптимизация игрового движка".

Как с помощью С++ выжимать все соки из железа, чтобы игроки получали плавную графику и стабильный fps, а разработчики могли поддерживать и расширять игровой функционал? - после прочтения у меня сложилось впечатление, что именно на этот вопрос отвечает "Game++".

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

Да, немного об авторе. Больше 20 лет практики программирования. Работал в EA над The Sims и SimCity BuildIt под слабые устройства тех лет, в Gaijin Entertainment довел с командой с нуля до стора Nintendo Switch и AppleTV порты Blades Of Time и Cuisine Royale, а также занимался ИИ-монстрами во вселенной Metro. Сейчас работает над движком Age of Empires 2 — старая игра, но современные требования к производительности и функциональности заставляют бежать вслед за прогрессом. То есть, это и ААА, и АА, и инди проекты. Сергею точно есть, что рассказать о игрострое.

Пройдемся по поинтам в контенте.

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

Дальше добрую часть книги автор уделяет оптимизациям памяти. Как сэкономить количество обращений к памяти, ее занятый объем и время выделения|очистки(как на очистку может тратиться даже больше времени, чем на выделение, см. в книге). Мой краш - глава про кастомные аллокаторы. Тема и так не простая, а здесь все собрано вместе с кодом и реальными кейсами применения.

Минуя краткий обзор алгоритмов STL(с интересными повествовательными приемами, типа "вредные советы") вы попадаете в многопоточный мир ад. Вы посмотрите на примеры задач с многопоточностью аля тредпул и с выделенными потоками для конкретных игровых подсистем. А также узнаете, почему блокировки потоков отбрасывают производительность в каменный век и какого это, программировать без мьютексов.

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

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

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

Все, что вам надо сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом слово "Конкурс". Повторные комментарии будут удаляться. Шанс залететь у всех будет еще ровно 7 дней, начиная с этого момента. На 8 день выйдет пост с результатами.

Победителя естественно выберем рандомайзером.

Be lucky. Stay cool.
32🔥16👍8❤‍🔥2👏2😁1
​​Выравнивание. А зачем?
#новичкам

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

struct Pixel {
unsigned char grayscale;
int x, y;
};


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

Если паримся, значит надо считать.

Посчитаем, сколько байт занимает структура Pixel. Это по сути sizeof(unsigned char) + 2 * sizeof(int). размер char'a 1 байт, размер int на современных десктопах 4 байта. С помощью применения дифференциального исчисления в пространстве Минковского получаем 9 байт.

Теория - это прекрасно, но надо все проверять:

std::cout << sizeof(Pixel) << std::endl;


И получаем..... 12 байт.

/+ 33% от расчетов. Вообще неоптимально получается. Но почему расчеты не сходятся с реальностью?

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

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

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

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

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

Pixel p{255, 1, 1};


Исходя из рассуждений выше, логично предположить, что начало p будет лежать по уже выравненному адресу для более быстрого чтения. Допустим, он делится на 4. В начале структуры лежит 1 байт цвета. А после лежат 2 int'а по 4 байта, доступ к которым должен быть выровнен. Но если к числу, кратному 4-м, прибавить 1, то оно перестанет делиться на 4. То есть, если данные лежат прям последовательно, то доступ будет невыровненный, а значит медленный.

Но компилятор у нас - не тупая машина, питающаяся текстом программы. Он умный и умеет оптимизировать. Если для скорости работы программы нужно, что адреса x и y должны быть выровнены по четверке, то он сделает магию и выровняет их. Заклинание простое - добавляем 3 байта после поля grayscale и дело в шляпе! Вот поэтому на практике размер Pixel равен 12 байтам.

Ну ладно, мы еще повоюем за память. Попробуем перетасовать поля:

struct Pixel {
int x, y;
unsigned char grayscale;
};


Теперь-то будет 9 байт размер?!

Да нет. Все те же 12 байт.

Представьте, что вы сделали массив пикселей. Да, у первого элемента x и y выровнены по 4-ке. Но начиная со второго элемента выравнивание поедет к черту. Так не годится и приходят на помощь все те же 3 байта, только уже в конце структуры.

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

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

Align yourself. Stay cool.

#cppcore #optimization #compiler
136👍18🔥7❤‍🔥3😁3
Выравнивание. Принципы
#новичкам

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

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

Для наглядности можно представить себе массив чаров:

char array[10];
std::cout << sizeof(array) << std::endl;
// OUTPUT: 10


Это просто 10 подряд идущих символов. И адреса каждого символа не нужно выравнивать. Поэтому и количество байт, которое занимает этот массив, равно просто количеству его элементов. Без всяких дополнений.

🔍 Адрес поля тривиального типа должен быть выровнен по размеру этого типа

- для short адреса должны быть кратными 2
- для int адреса должны быть кратными 4
- для size_t адреса должны быть кратными 8
- для float адреса должны быть кратными 4
- для double адреса должны быть кратными 8
- для указателей адреса должны быть кратными 8

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

Все это актуально для современных 64-битных десктопов.

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

Например:

struct A {
char a;
char b;
};

struct B {
char first;
A second;
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 2
// 3


У нас есть составная структура А, размер которой равен 2 байтам. Структура B содержит А в качестве поля. Так как в А только char поля, для доступа к ним не нужны паддинги. Поэтому размер В равен 3-м.

Но если будет вот так:

struct A {
char a;
double b;
};

struct B {
char first;
A second;
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 16
// 24


Мало того, что паддинг размером 7 добавися перед полем b структуры А, так он же еще добавится перед `second` в В. И в B он такого размера именно потому, чтобы доступ до b в итоге всех размещений был выровнен.

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

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

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

struct A {
double a;
char b;
};

A array[10];


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

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

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

struct A {
char a;
// 7 byte padding
double d;
short b;
// 2 byte padding
int c;
};

struct B {
double d;
int c;
short b;
char a;
// 1 byte padding
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 24
// 16


Реальный размер данных - 15 байт. Для double поля в любом случае нужно выравнивание по 8-ке. Но правильно расположив данные, мы экономим 8 байт.

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

Align yourself. Stay cool.

#cppcore #optimization #compiler
1🔥2614👍12
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👍2910🔥82
Итоги конкурса

Мы долго ждали и, наконец, дождались. Вчера мы честно взяли генератор случайных чисел и нашли победителя и будущего счастливого обладателя книжки "Game++. Устройство и оптимизация игрового движка" Сергея Кушниренко. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете нашей непредвзятости)

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

Ну а победителем стал таинственный незнакомец с ником R Y (@ru108) давайте похлопаем ему👏👏👏. Победитель, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.

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

Be lucky. Stay cool.
👍31👏18🎉53🔥3❤‍🔥2
​​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
​​Как узнать размер кэш-линии?
#опытным

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

Там я использовал выравнивание по границе 64 байта - это типичный размер кэш-линии на современных процессорах.

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

Для этого начиная с С++17 в стандарте появились константы std::hardware_destructive_interference_size и std::hardware_constructive_interference_size.

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

std::hardware_destructive_interference_size - Минимальное смещение, которое гарантирует отсутствие false sharing.

std::hardware_constructive_interference_size - Максимальный размер участка памяти, внутри которого гарантируется true sharing.

С false sharing мы разобрались:

struct GuaranteeFalseSharingAbsence
{
alignas(std::hardware_destructive_interference_size ) std::atomic<uint64_t> counter1;
alignas(std::hardware_destructive_interference_size ) std::atomic<uint64_t> counter2;
};


Помещаем два атомарных счетчика в разные кэш-линии и их изменения никак не влияют друг на друга.

А что такое true sharing?

Это ситуация, когда данные попадают в одну кэш линию.


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

struct alignas(std::hardware_constructive_interference_size) A {
std::uint32_t one;
std::uint32_t two;
};


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

Спасибо, @topin89, за идею для поста)

Share with others. Stay cool.

#cpp17
1🔥34👍106❤‍🔥5
​​std::aligned_alloc
#опытным

alignas задает требования к выравниваю для типа или переменной. И компилятор, при размещении объектов на стеке слушает и повинуется этим правилам.

Но, например, malloc следует только своим внутренним правилам. Он выравнивает адреса, но только по границе alignof(std::max_align_t]). Это 16 байт на современных десктопах.

Что делать, если мне нужны более строгие требования к адресу? Например нужно выровнять выделенные на куче данные по границе 32, 64 или вообще по размеру страницы 4096 байт?

Для этого используется С++17 функция aligned_alloc:

void* aligned_alloc( std::size_t alignment, std::size_t size);


где alignment - требования к выравниванию, а size - размер данных для аллокации в байтах. size должен быть кратным alignment. Функция выделяет просто size байт и не конструирует никаких объектов. Подразумевается также возможность выделить массив значений размером size/alignment, каждое из которых выравнено по границе alignment.

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

template <typename T>
T *allocate_aligned(size_t count) {
if (count == 0)
return nullptr;

const size_t alignment = alignof(T);
const size_t type_size = sizeof(T);
const size_t total_bytes = count * type_size;

char *raw_memory =
static_cast<char *>(std::aligned_alloc(alignment, total_bytes));
if (!raw_memory)
throw std::bad_alloc();

for (size_t constructed = 0; constructed < count; ++constructed) {
new (raw_memory + (constructed * type_size)) T();
}

return reinterpret_cast<T *>(raw_memory);
}


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

Align yourself. Stay cool.

#cppcore #cpp17 #compiler
20👍7🔥4🤪2
​​Плотно упаковываем данные
#новичкам

Мы все про выравнивание, да про выравнивание. Куча средств языка, которые позволяют грамотно работать с требованиями к 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
Идея - главный капитал на сегодня

Я знаю кучу людей, у которых есть отдельная заметка в телефоне аля "бизнес идеи". Только вот никто эти идеи не реализует.

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

Но в современном мире ллм'ок отмазки больше не принимаются. Идеи достаточно, чтобы начать действовать.

Не умеешь писать код? - За тебя напишут.

Нет времени? - Тут недавно агенты антропиков смастерили С'шный компилятор на 100к строк кода за 2 недели(с багами и говноархитектурой, но все же). Так что твоя аппка для подсчета каллорий может увидеть свет уже через несколько недель, работая по вечерам.

Нет плана? - Тебе напишут пошаговый план до получения первых клиентов.

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

Например, Денис из канала @its_capitan запустил собственную детективную игру: каждый персонаж — это реальный Telegram-аккаунт. Там сидят AI-чат боты, которые отвечают за героев.

Что в итоге:
— 3 месяца на подготовку + 3 месяца на разработку
— 40+ покупок за полтора месяца
выручка — $1500+
— чек — $40
— стек: Python, Telegram API, OpenAI + Anthropic

Денис двигался в одиночку и изначально была только идея. А через полгода проект уже приносит деньги.

В сообществе "Короче Капитан" разобрано уже куча таких запусков. Честно говорят про их успехи и (!)провалы. И главное - собирают и анализируют опыт этих проектов, чтобы с минимальными усилиями получить первых клиентов и масштабироваться.

Я сам читаю этот канал и если б не посты про кэш-линии, то уже давно запустил бы что-нибудь эдакое.

А если серьезно, то навыки анализа идей и конкурентов и знания, как продвигать проект с минимальными усилиями становятся все более актуальными сейчас. Подпишись на канал, набирай насмотренность и смелость реализовывать свои идеи.
👎134👍4🔥3🤬3❤‍🔥1🤣1👀1
WAT
#опытным

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

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

class A1
{
private: // THIS IS PRIVATE
int a;
short s;
int b;
char d;
};

class A2
{
public: // THIS IS PUBLIC
int a;
short s;
int b;
char d;
};


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

class B1 : public A1
{
public:
char c;
};


class B2 : public A2
{
public:
char c;
};


Какими будут размеры объектов классов B1 и B2?

std::cout << sizeof(B1) << std::endl;
std::cout << sizeof(B2) << std::endl;


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

Но нет!

Вывод будет такой:

16
20


Убедитесь сами.

WAT? Куда делись 4 байта?

Начнем с того, что размер структур A1 и А2 одинаковый и равен 16 байтам. Компилятор в обоих случаях вставил 2 байта паддинга после short переменной и 3 после char.

Теперь давайте посмотрим на смещение поля c в наследниках относительно начала объекта. Это можно сделать с помощью трюка из этого поста:

std::cout << std::dec << std::bit_cast<std::uintptr_t>(&B1::c) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&B2::c) << "\n";


Вывод:

13
16


Видно, что смещение поля c относительно начала объекта для В1 и В2 разное. Для В2, в котором поля публичные, ожидаемо c начинается с 16-го байта. А вот для В1, где поля приватные, c начинается ровно там, где заканчивается char поле d.

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

Ключевую роль здесь играет понятие POD в терминах старого стандарта C++03, которое до сих пор влияет на решение, можно ли использовать хвостовое заполнение (tail padding) базового класса для размещения новых членов производного класса.

Хвостовое заполение - это те самые неиспользуемые байты, которые компилятор добавляет в конец объекта для удовлетворения требований выравнивания.

POD тип - массив или класс, не имеющий пользовательских конструкторов, приватных/защищённых нестатических данных, виртуальных функций, базовых классов и обладающий тривиальными копирующими операциями и деструктором.

Для pod типов нельзя оптимизировать хвостовое заполнение для сохранение ABI. Для не pod типов - можно.

Получается, что A1 - не pod тип: компилятор оптимизировал хвостовой паддинг и поместил поле c внутрь него., сэкономив место. Но A2 - pod тип, поэтому хвостовое заполнение остается.

Align yourself. Stay cool.

#optimization #compiler
23🔥13👍10😱4👏2❤‍🔥1
​​WAT
#опытным

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

Еще один сбивающий с толку примерчик, вдогонку прошлого поста:

struct Empty {};

struct Good : Empty {
int i;
char c;
};

struct Bad {
int i;
char c;
};

template <typename T>
struct S : T {
char c;
};

static_assert(sizeof(S<Good>) < sizeof(S<Bad>));

int main() {
return 0;
}


Два одинаковых, казалось бы, класса Good и Bad. У них одинаковый состав полей. Только у Good есть пустой базовый класс, а у Bad - нет. Разве это как-то может повлиять на размер наследника S?

Еще как может.

Программа выше успешно компилируется aka размер S<Good> меньше размера S<Bad>.

WAT? У Good и Bad же идентичный состав полей, откуда разница?

Кстати код выше успешно компилируется только под gcc и clang, и его сборка валится на msvc. Можно посмотреть тут

Дело в том, что разница лежит уже не на уровне языка С++, а на уровне ABI(Application Binary Interface), которое поддерживает компилятор.

gcc и clang поддерживают Itanium C++ ABI, а msvc - Microsoft ABI.

Для всех компиляторов верно, что размер структур Good и Bad одинаковый(для пустой базы применяется Empty base optimization) - 8 байт.

Но как только появляется наследник S - стандарт перестает регламентировать layout объекта. Всю ответственность несет реализация.

Вы уже догадались, что речь снова зашла про наши любимые pod типы и хвостовые паддинги.

Кратко напомню. POD тип - массив или класс, не имеющий пользовательских конструкторов, приватных/защищённых нестатических данных, виртуальных функций, базовых классов и обладающий тривиальными копирующими операциями и деструктором.

struct Empty {};

struct Good : Empty {
int i;
char c;
};

struct Bad {
int i;
char c;
};


В нашем случае Good - не pod, а Bad - pod тип

Для POD типов для сохранения обратной совместимости и совместимости с С Itanium C++ ABI не позволяет оптимизировать хвостовое заполнение и компилятор честно добавляет новое поле в классе S после хвостового паддинга.

Good же не pod тип, поэтому требования abi на него не распространяются и компилятор может оптимизировать размер класса и положить данные поля c в классе S внутрь паддинга.

std::cout << "Size of S<Good>: " << sizeof(S<Good>) << std::endl;
std::cout << "Size of S<Bad>: " << sizeof(S<Bad>) << std::endl;
// Size of S<Good>: 8
// Size of S<Bad>: 12


Именно поэтому размеры классов S<Good>и S<Bad> будут 8 и 12 соответственно.

У Microsoft ABI на это все дело совершенно свое мнение(как и всегда), поэтому там и размеры одинаковы.

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

Behave consistently. Stay cool.

#compiler
1🔥259👍6😁6
С++26
#опытным

Ну что, мы все этого ждали 3 года и наконец оно случилось. Тут намедни утвердили С++26!

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

🔥 Триграфный оператор утверждения ??! - возвращает true, если выражение может быть скомпилировано, иначе false.

template<typename T, typename U>
constexpr void can_add(T&& t, U&& u) {
if constexpr (x + s ??!) {
std::cout << "Addition can be performed!\n";
} else {
std::cout << "Addition can not be performed\n";
}
}


С++17 триграфы уничтожил, С++26 их вернул. "Компьютерные войны. Часть С++26. Месть триграфов".

🔥 Наконец-то в стандарт добавили тредпул! Класс std::thread_pool, который принимает количество потоков, политику планирования выполнения задач и размер очереди. Политики исполнения могут быть std::thread_pool::scheduling_policy::fifo и std::thread_pool::scheduling_policy::priority, а размер задается чиселкой, но по умолчанию очередь неограничена std::thread_pool::unbounded:

std::thread_pool pool(4, std::thread_pool::scheduling_policy::priority);

pool.enqueue([] {
std::cout << "Critical task!\n";
}, std::thread_pool::priority::critical);

pool.enqueue([] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "Normal task\n";
}, std::thread_pool::priority::normal);


🔥 Добавлена поддержка рефлексии! Еще один пункт, которого все ждали. Рефлексия позволяющей отслеживать и модифицировать элементы программы на стадии компиляции. Добавлен новый оператор (|.|) для получения метаинформации о грамматической конструкции. Для преобразования и обработки полученной в ходе инспектирования информации предложена библиотека std::meta и доступны такие возможности, как вычисления с константами.

constexpr int i = 42, j = 42;
static_assert((|.|)i == (|.|)j); // check if i and j values are equal in compile time


Кстати, отпишитесь в комментах, на что похож новый оператор. Что-то напоминает, но никак не могу конкретно сказать.

🔥 Предложена реализация массива переменного размера std::inplace_vector, размещаемого в стеке. И у него вообще ничем не ограничен размер!. API близок к std::vector, но элементы массива хранятся не на куче, а внутри объекта.

std::inplace_vector vec;
for (int i = 0; i < 10000; i++) {
vec.push_back(i);
}


И вообще никаких аллокаций!

🔥 Внесены изменения для усиления безопасности стандартной библиотеки, такие как проверки допустимых значений и выхода за границы буфера. Например, при доступе к элементу "reference operator[](size_type idx) const;" добавляется проверка условия "idx < size()". И мякотка: проверка даже рантаймовых контейнеров происходит на этапе компиляции!

🔥 Добавлен атрибут [[maybe_unused]] для блока кода. Бывает начинаете писать какой-то сервис и тут бац! бизнес требования поменялись и допинывание сервиса до прода ушло глубоко в конец бэклога. Чтобы новые сотрудники не гадали, почему код есть, но ничего не работает, очень полезно пометить его атрибутом maybe_unused:

[maybe_unused]] {
// potentially unused code
}


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

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

std::linalg::matrix<double> A(3, 3);
A(0,0) = 1; A(0,1) = 2; A(0,2) = 3;
A(1,0) = 4; A(1,1) = 5; A(1,2) = 6;
A(2,0) = 7; A(2,1) = 8; A(2,2) = 9;
auto A2 = std::linalg::mul(A, A);
auto diag = std::linalg::diag(A); // std::vector<double> {1, 5, 9}
double det = std::linalg::det(A); // 0
auto flat = std::linalg::reshape(A, 1, 9);


PS Надеюсь, что вы попались на первоапрельскую шутку) По этой ссылочке можно посмотреть реальные нововведения.

Don't be fooled. Stay cool

#cpp26 #fun
53👍19😁19🔥15🤣5👎1🤨1👀1
​​Как изучать выравнивание?
#новичкам

Небольшой дисклеймер для начала.

Надеюсь, что более менее все поняли, что в С++26 нет тредпула, mayby_unused, бесконечно расширяемого на стеке вектора и матриц. Ну а если нет, то у вас спина белая! Была вчера.

Back to the business.

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

Такой механизм есть и он называется указатель на поле класса. У нас уже есть пост на эту тему, но кратко напомню. Это особый тип указателя, который хранит смещение поля относительно начала объекта в байтах.

struct Type {
int a;
int b;
float c;
};

&Type::a; // pointer to a
&Type::b; // pointer to b
&Type::c; // pointer to c


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

template<typename T, typename FieldType>
void print_offset(FieldType T::*field) {
std::cout << std::dec << std::bit_cast<std::uintptr_t>(field) << "\n";
}

print_offset(&Type::a);
print_offset(&Type::b);
print_offset(&Type::c);
// OUTPUT
// 0
// 4
// 8


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

Вспомним пару примеров из недавнего WAT

struct Empty {};

struct Good : Empty {
int a;
char b;
};

struct Bad {
int a;
char b;
};

template <typename T>
struct S : T {
char c;
};

print_offset(&S<Good>::a);
print_offset(&S<Good>::b);
print_offset(&S<Good>::c);
std::cout << std::endl;
print_offset(&S<Bad>::a);
print_offset(&S<Bad>::b);
print_offset(&S<Bad>::c);
// OUTPUT
// 0
// 4
// 5

// 0
// 4
// 8


Здесь четко видно, что данные поля c в классе S<Good> начинаются с пятого байта (из-за оптимизации хвостового заполнения), а в классе S<Bad> - с 8-го байта.

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

Don't guess. Stay cool.

#memory
12👍11🔥5❤‍🔥3😱2