fa
Feedback
Грокаем C++

Грокаем C++

رفتن به کانال در Telegram

Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

نمایش بیشتر
9 377
مشترکین
+224 ساعت
+57 روز
+1230 روز
آرشیو پست ها
​​Как запретить объекту создаваться на стеке? Классика #новичкам Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++. Можно удалить все конструкторы с помощью пометки delete и тогда объект вообще нигде нельзя будет создавать. Но вопрос скорее всего про другое: как запретить объекту создаваться на стеке, но не на куче или статической памяти? Очевидно, что-то надо колдовать с конструкторами, потому что именно они создают объект. Прямолинейный подход - сделаем конструктор приватным. Тогда внешний код не сможет его вызвать. Но создавать объект-то нам нужно все равно как-то. И для этого существуют фабричные методы - статические методы класса, которые дают доступ с объекту/его созданию. Если мы хотим создавать объект только на куче можно написать так:
class MyClass {
private:
    MyClass() { std::cout << "MyClass created\n"; }
public:
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
    static std::unique_ptr<MyClass> create() {
        return std::unique_ptr<MyClass>(new MyClass());
    }
    void hello() const {
        std::cout << "Hello, MyClass!\n";
    }
};

auto obj = MyClass::create();
obj->hello();
Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор. Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения. Такой подход может быть полезен, если: 👉🏿 размер объекта очень большой и просто не влезет в стек 👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики. 👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr. 👉🏿 вы хотите какой-то особый контроль времени жизни. Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:
class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;
        return inst;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;
    void foo() {}
private:
    Singleton() = default;
    ~Singleton() = default;
};
Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса. Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик. Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз. Limit wrong uses. Stay cool. #design #cppcore

Если хотите повеселиться, то почитайте комменты в последнем подкасте подлодки про С++ с Антоном Полухиным. Антон рассказал немного про эволюцию языка, в каком он состоянии сейчас, какие у него были и есть основные направления развития и причем здесь ржавчина. Естественно, в комментах порвались неженки. Их неустраивает 5 видов инициализации переменных, пугают звездочки и амперсанды, несовместимые сторонние библиотеки, раздутая и неправильная стандартная библиотека, куча диалектов языка и тд. А если уж увидят шаблоны в коде, то у них даже с пяток волосы выпадают. Уже даже не хочется с этим всем спорить. У языка, который одновременно используется в бэкэнде, геймдеве, эмбедде, hft, перформанс библиотеках, системщине и тд очевидно будут особенности. Они могут отталкивать и мешать, но в том числе из-за них вообще возможно использование языка в таких разных сферах. Есть и проблемы, без них никуда. И их предостаточно. Но здесь я согласен с Антоном, что ошибки в программах в основном зашиты в логике, а не вызваны кривизной языка. Я пишу на userver'е и за последние несколько лет не встретил на проде бага, который бы появился из-за опасной фичи языка. Когда есть сильные тесты, они выявляют большинство проблем. В общем смотрите https://youtu.be/ZYaBzFj3d4Y?si=HMxEeBONt6pL5VC7. И подкаст интересный, и комменты настоялись. Ну и давайте в противовес тем комментам, напишите под этим постом, почему вам нравится С++? Enjoy the tool. Stay cool. #fun

​​Почему тогда глобальные переменные зануляются? #новичкам Внимательные читатели 2-х последних постов могут задаться вопросом: "Почему в неинициализированных локальных переменных лежит мусор, а в глобальных - нули? Откуда такие двойные стандарты?". Можно конечно сказать, что такие двойные стандарты определены в стандарте С++ и на этом можно закончить. Ответ, как и в случае с локальными переменными, лежит за пределами языка.
int i;

int main() {
  std::cout << i << std::endl;
}
// OUTPUT is always
// 0
Все пошло от языка С. Там глобальные переменные занулялись, поэтому для совместимости с С С++ также перенял эту особенность. "Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь." Это был важный переход, который и приведет нас к ответу. Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык. Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями. И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись. Когда пришло время стандартизации С, то обнуление глобальных переменных органично перекочевало в стандарт языка. Вот так обнуление стало стандартом С, а затем и С++. Understand the root cause. Stay cool. #cppcore #os

​​Почему локальные переменные инициализируются мусором? #новичкам Изучать С++ больно по многим причинам. Одна из них - есть вещи, которые просто надо принять на веру. И чтобы понять, почему эти вещи работают так, а не иначе, нужно много времени за кодингом и качать сиплюсплюсные и computer science бицепсы. Сегодня разберем вопрос, который лично меня мучал в начале пути: почему локальная переменная будет заполнена мусором, если ее не инициализировать?
int main() {
  int i;
  std::cout << i << std::endl;
}
// POSSIBLE OUTPUT:
// 64
Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить? На самом деле мусор - не значит какие-то рандомные числа, никто их не генерирует. И чтобы понять, откуда берется мусор, нужно знать о стеке вызовов. Стек вызовов - пространство в памяти, которое содержит информацию об исполнении функций. Представим аналогию со стопкой книг. Каждая книга - небольшой кусочек памяти с информацией об отдельной функции(фрейм функции). Как только вы вызываете функцию, на стопку кладется новая книга(фрейм). Когда исполнение доходит до конца функции - сверху снимается книга. Это происходит автоматически на уровне машинных инструций. Так вот локальные переменные как раз хранятся на стеке. Если точнее, то значения локальных переменных конкретной функции лежат внутри фрейма этой функции. И нас интересует процесс выделения памяти под локальные переменные. При исполнении программы есть специальный указатель, который указывает на вершину стека. Выделение памяти под локальную переменную - это просто сдвиг этого указателя. И когда мы говорим:
int main() {
  int i;
  ...
}
Мы говорим, что хотим иметь переменную i и надо выделить под нее память, то есть просто передвинуть указатель на стек с позиции X на позицию X+4. Само значение мы при этом не задаем. Но значение-то все равно будет. И будет оно браться их тех битиков-байтиков, которые находились в куске памяти от X до X-4. А кто-то из вас заранее знает, какие байтики там будут храниться? Нет На этом месте почти наверняка уже располагались данные какой-нибудь другой функции, которая уже выполнилась ранее и ее фрейм снялся со стека. Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции. Чтобы более наглядно представить себе процесс работы со стеком вызовов, можете воспользоваться этим визуализатором. Там примеры фиксированные. Если же хотите на собственных примерах посмотреть - можете поиграться тут. "Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?" На самом деле вызывается, вы просто не видите этого. Чтобы подготовить программу к запуску, надо немало потрудиться и эти труды обязательно будут отражены на стеке. "Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?" Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься. "Очищение" памяти отнимает дополнительное драгоценное время исполнения программы. И никто его не хочет тратить на такую ненужную вещь, как обнуление памяти. Инициализируйте свои переменные и проблем с вывозом мусора не будет. Understand the root cause. Stay cool. #cppcore #os

Когда производительность упирается в железо, а когда в архитектуру? Как проектировать надежные и быстрые системы на C++? Каки
+4
Когда производительность упирается в железо, а когда в архитектуру? Как проектировать надежные и быстрые системы на C++? Какие подходы используют разработчики компиляторов, рантаймов и системного ПО? Ответы на эти и другие вопросы найдем на C++ Russia — конференции для C++ разработчиков, инженеров, разработчиков компиляторов, тимлидов и исследователей. 📅 7 мая 2026 — онлайн-день 📅 16–17 мая 2026 — Москва + онлайн Три дня докладов, воркшопов и общения C++ сообщества. Будем говорить про язык и инженерные задачи: архитектуру, производительность, управление памятью, многопоточность и разработку низкоуровневого ПО. Новое в этом году — системное программирование: компиляторы, рантаймы, операционные системы, управление ресурсами и дизайн языков программирования. В карточках собрали несколько топовых докладов из программы. Используйте промокод, чтобы купить персональный билет со скидкой — GROKAEMCPP Купить билет Реклама. ООО «Джуг Ру Груп». ИНН 7801341446

​​Объявления функций vs объявление переменной или зачем нужен extern #новичкам В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины: 👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление. 👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp. 👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения. 👉🏿 Организация кода и разделение ответственности. И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:
void foo(int a); // declaration

void foo(int a) {
  std::cout << a << std::endl; // definition
}
С переменными все немного сложнее. Но начнем с легкого:
int i = 0;
Это определение хоть глобальной, хоть локальной переменной i и ее инициализация нулем. Все все понимают. Если думать в аналогии с функциями, то легко дойти до мысли что:
int i;
это объявление переменной. Но это не так! Это в любом случае определение! Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него. Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте? Ровно для этого и нужно ключевое слово extern.
extern int i; // declaration

int main() {
    std::cout << i << std::endl;
}

int i = 42;

// OUTPUT
// 42
До мейна мы сказали, что будем ссылаться на переменную i, определение которой лежит в другом месте. И сразу после мейна предоставляем это определение. Компилятор все понял и в итоге мы получаем 42 на консоли, а не ноль. В этом случае строчка extern int i; является объявлением глобальной переменной i. Определение переменной может находиться хоть в другой единице трансляции. Просто тогда зависимости будут разрешаться на этапе линковки. Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:
int main() {
    extern int i;
    std::cout << i << std::endl;
}
То вы опять объявите глобальную переменную i. То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью extern. Declare your intentions. Stay cool. #cppcore

​​Конфликт в действии #опытным Спасибо, @Ivaneo, за любезно предоставленный примерчик. Посмотрите на этот код:
#include <iostream>

int read;

int main()
{
  std::ios_base::sync_with_stdio(false);
  std::cin >> read;
}
Как думаете, что случится при попытке компиляции со статической линковкой системных библиотек и запуска этого кода под linux? Поразмышляйте несколько секунд. А [получите](https://godbolt.org/z/Tndxbo8oz) ошибку сегментации: прилетит сигнал SIGSEGV. Почему так? Мы же ничего незаконного не делали! Просто пытаемся читать в переменную, заблаговременно отвязав С++ потоки от сишных. На самом деле сделали кое-что незаконное. Назвали переменную в честь системного вызова. Давайте по порядку. Когда у нас потоки синхронизированы, для операций с потоками используется стандартное сишное апи. Когда мы отвязываем потоки, то С++ ввод-вывод начинает работать самостоятельно и независимо. Имеются полноценные буферы и сложная система их менеджмента. А для общения с операционной системой можно использовать непосредственно системные вызовы. Посмотрим на примере gcc. В исходниках есть такое определение библиотечного вызова чтения:
/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
  return SYSCALL_CANCEL (read, fd, buf, nbytes);
}

weak_alias (__libc_read, read)
Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak). А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss). Естественно, что сильный символ всегда переписывает слабый. Поэтому в итоговом исполняемом файле символ read будет указывать не на функцию, а на переменную read. То есть при запуске программы и попытке вызвать функцию read, будет "вызываться" переменная. Отсюда и сегфолт. Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например readopenclosewriteexit). Avoid name clash. Stay cool. #cppcore #goodoldc #OS

​​sync_with_stdio #опытным std::cout, std::cerr и std::cin - стандартные объекты потоков для работы с вводом-выводом в С++. Но из С++ программы мы также можем вызывать и сишное апи для работы с IO. Например scanfprintf. Они также могут читать из консоли и писать в нее. Но как связаны С++ и С апи для работы с IO? Мешают ли они друг другу и скремблят результаты операций? Или все хитрее? Все хитрее) И разбирать все будем на примере потока вывода std::cout, для других все аналогично, просто так нагляднее будет. В базовой конфигурации std::cout и запись в stdout с помощью С апи синхронизированы. То есть следующие вызовы полностью эквивалентны(c- символьная переменная):
std::fputc(stdout, c);
// and
std::cout.rdbuf()->sputc(c);
Запись символа в буфер С++ объекта имеет тот же эффект, что и запись символа в буфер С потока. На практике это означает, что синхронизированные потоки C++ не буферизуются, и каждая операция ввода-вывода на потоке C++ немедленно применяется к буферу соответствующего потока C. Это позволяет свободно смешивать C++ и C I/O операции. Обращу внимание. Мы до сих пор говорим только об одном символе. И не спроста: для записи конкретного символа гарантируется синхронизация и потокобезопасность(thread-safety). Но записи в stdout из разных тредов могут перемешивать символы этих записей. То есть при вызове:
std::cout << "Hello, World!" << std::endl;
по сути имплементация посимвольно записывает приветствие миру в stdout в цикле, вызывая std::putc(stdout, c). Обычно такие функции(типа putc) реализованы с помощью внутренних механизмов синхронизации, обеспечивая потокобезопасность. И это предательски медленно! Поэтому дефолтные операции ввода-вывода в С++ такие медленные. Синхронизируют запись каждого символа только трусы! Но мы-то с вами не трусы. Можно отвязать С++ потоки от С потоков и сделать их независимыми. Тогда у С++ потоков появляется свой буфер, который работает оптимальнее, чем посимвольная запись. Это может дать сильный буст к производительности стандартных IO операций и по скорости они могут сравниться с сишными. Чтобы отвязать потоки нужно самой первой строчкой main вызывать следующую функцию. Например так
int main()
{
    std::ios::sync_with_stdio(false);
    std::cout << "a\n";
    std::printf("b\n");
    std::cout << "c\n";
}
Вывод может изменяться в разных ситуациях, но вот здесь получился такой вывод:
a
c
b
Видно, что разное апи работает независимо и непоследовательно. Если вам не нужна такая синхронизация, то выключайте ее полетите на третьей космической скорости бороздить просторы галактики. Be fast. Stay cool. #cppcore #optimization #goodoldc #compiler

📱 C++/Qt/QML разработчик в команду Mobile Android — делаешь UI и механику приложения — 20 млн пользователей каждый день — C+
📱 C++/Qt/QML разработчик в команду Mobile Android — делаешь UI и механику приложения — 20 млн пользователей каждый день — C++20 + Qt + QML + 3D-карта — многопоточность и продуктовая разработка 🧠 C++ разработчик в команду алгоритмов Поиска — разрабатываешь движок поиска 2ГИС — алгоритмы, структуры данных, архитектура — влияние на качество поиска для миллионов — производительность, память, ML-модели Обе роли: — удалёнка — сильные команды — сложные задачи 👉 Другие инженерные инсайты от 2ГИС → в Telegram-канале RnD

​​using namespace std #новичкам Десятки новичков спрашиваются в комментариях что это за конструкция и почему никто в более менее серьезном коде не использует ее. Ну чтож. Мы долго игнорировали этот запрос, но пора это исправить. Пост с разъяснениями просто обязан существовать. Поэтому сегодня все по полочкам разложим. Начнем с namespace В большинстве современных языков так или иначе реализована возможность разделения подключаемой функциональности на какие-то логические части. Отчасти это сделано для того, чтобы непосредственно в коде можно было увидеть к какой части принадлежит функциональность. В С++ тоже есть похожий механизм. Он называется пространство имен или namespace. Можно сказать, что это область, где вы можете определять сущности, логически связанные друг с другом. Работает это примерно так:
namespace MyLib {
    void hello() { ... }
}

MyLib::hello();
Мы определяем пространство имен MyLib, в котором будут все функции, классы и переменные, которые относятся только к этой либе. И при использовании функции hello мы обязаны указать перед ее именем неймспейс MyLib, чтобы компилятор понимал, где ему искать эту функцию. Заодно и все, кто читает код, теперь понимают, что мы использует функцию hello именно из пространства MyLib и ни откуда-нибудь еще. Потому что может случиться ситуация, когда функция с именем hello будет не только в этом пространстве:
void hello() { ... }

namespace MyLib {
    void hello() { ... }
}

namespace OtherLib {
  void hello() { ... }
}

MyLib::hello();
Вот сейчас критически важно указать, из какого пространства функция вызывается, потому что вы просто можете вызывать не ту функцию, если ничего не укажите. Теперь std. Это пространство имен для сущностей, которые уже реализованы за вас в стандартной библиотеке. Допустим, вы хотите что-то на консоль вывести. Вы подключаете заголовочник <iostream> и используете глобальный объект cout из пространства имен std:
#include <iostream>

int main() {
  std::cout << "Hello, World!\n";
}
Вы сами не реализовывали cout. Вы лишь использовали то, что уже существует в стандартной библиотеке. И сказали об этом с помощью указания пространства имен. Теперь using. Иногда у пространств имен бывают очень длинные названия. Плюс они могут быть вложены друг в друга. И не всегда хочется писать:
my_library::my_cool_module::my_class obj;
И если вы знаете, что: 1️⃣ в данном конкретном небольшом месте кода вы используете много сущностей из какого-то пространства имен 2️⃣ и нет никакой неоднозначности, как в примере с функцией hello вы можете использовать конструкцию using namespace:
void foo() {
  using namespace my_library::my_cool_module;
  my_cool_class obj;
  my_other_cool_class = my_cool_function(obj);
}
С ее помощью вы говорите компилятору: "Внутри этого блока кода я могу использовать сущности из этого пространства имен без префикса. Ищи неизвестные тебе имена там." Ну а конструкция using namespace std говорит о том, что мы можем использовать абсолютно все стандартные сущности без указания префикса. Почему же так никто не делает, если это избавляет от головной боли писать постоянно std::? 👉🏿 Код пишется для того, чтобы его читали. При указании неймспейса читатель сразу понимает, откуда берется данная сущность. Это улучшает читаемость. 👉🏿 Обузинг using namespace особенно в глобальном скоупе(вне функции и другого пространства имен) чреват конфликтом имен:
void hello() {}

namespace MyLib {
    void hello() {}
}

using namespace MyLib;

hello();
компилятор просто не поймет, какую функцию вы хотите реально вызывать. В данном случае будет ошибка компиляции. Но при определенных условиях компилятор может вызвать не ту версию и даже никак не сообщить вам об этом. Дебагаться потом будете долго. 👉🏿 Положите using namespace std в свой хэдэр и теперь проблемы будут у всего кода, который его подключает. В боевом коде почти никто не использует эту конструкцию, поэтому учитесь писать сразу так, как это будет требоваться в будущем. Ease is not always the best choice. Stay cool. #cppcore #badpractice

​​Где аллоцируются элементы std::array? #новичкам Вроде бы простой вопрос с собеседования, но он имеет 2 уровня погружения. В зависимости от того, как вы ответите, интервьюер будет по-разному воспринимать глубину ваших знаний. 1 уровень Элементы std::array аллоцируются на стеке. std::array - это очень тонкая обертка над сишными массивами, в которых элементы располагаются именно на стеке. Это очень легко проверить, достаточно сравнить размеры двух арреев с разным количеством элементов:
std::array<int, 10> arr{0};
std::array<int, 20> arr1{0};

std::cout << sizeof(arr) << " " << sizeof(arr1) << std::endl;
// OUTPUT:
// 40 80
Два массива на 10 и на 20 элементов, каждый элемент занимает по 4 байта. И все сходится: размер arr 10*4 байт, а второго - 20*4. 2 уровень Вообще говоря, std::array - это обычный объект. Объекты можно размещать на стеке, а можно и на ......... правильно, куче. Делается это очень просто:
void* operator new(std::size_t size) {
    void* ptr = std::malloc(size);
    if (!ptr) throw std::bad_alloc{};
    std::cout << "Allocation has happend" << std::endl;
    return ptr;
}

int main() {
    auto * ptr = new std::array{0, 1, 2, 3, 4};
    delete ptr;
}
// OUTPUT:
// Allocation has happend
Один new и на стеке уже лежит лишь указатель, а сами элементы массива хранятся на куче. Прям вот в таком виде вряд ли кто-то работать с std::array. Но если std::array является полем какого-то класса, объекты которого аллоцируются на куче, то и элементы массива тоже будут на куче располагаться. Поэтому ответ на вопрос из заголовка поста - где хотим, там и располагаем. А если чуть менее пафосно, но более точно - там, где аллоцирутся сам объект std::array. Reach advanced levels. Stay cool. #cppcore #cpp11 #memory #interview

Intern Week Offer Backend — ускоренный отбор на стажировку в Яндекс. Всего за неделю вы успеете пройти отбор, интервью с кома
Intern Week Offer Backend — ускоренный отбор на стажировку в Яндекс. Всего за неделю вы успеете пройти отбор, интервью с командами — и, если случится мэтч, получить долгожданный офер на стажировку. Компания ждет начинающих бэкенд-разработчиков, которые пишут на любом из этих языков: Java, Kotlin, Python, C++ или Go. Требования: 1️⃣ Базовое знание алгоритмов и классических структур данных 2️⃣Опыт работы над учебными или реальными проектами будет преимуществом До 8 апреля регистрируйтесь и решайте Контест, 13–15 апреля пройдите два технических интервью, 16–17 апреля знакомьтесь с командами и получайте офер. Успейте подать заявку на Intern Week Offer Backend.

​​Как изучать выравнивание? #новичкам Небольшой дисклеймер для начала. Надеюсь, что более менее все поняли, что в С++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

🏙️ Бэкэнд масштаба города: идём на Day&Night* 2026 Обсудим миллионные нагрузки, сложную архитектуру Яндекса и код, который ежесекундно воплощается в реальном мире: поездки такси, доставка продуктов и заказов. Будут доклады Саши Аникина про роботакси и Кирилла Неймана про облачные интеграции электрокара. Программу конференции и клубов готовили Илья Царёв — руководитель разработки в Яндекс Go, Стёпа Мороз — руководитель разработки в Яндекс Доставке и Женя Косенко — руководитель технических проектов в Техплатформе Городских сервисов Яндекса. Поговорим про: 🔹 Инфраструктуру, платформы и инструменты разработки 🔹 Архитектуру бигтеха и роль ИИ-агентов А ещё можно пообщаться с единомышленниками в клубах музыки и винила и активного образа жизни. 🍸 Завершит всё вечеринка с диджеями и коктейлями. 🚀 Регистрация открыта — успейте подать заявку! Все заявки проходят модерацию, дождитесь обратной связи. *День и Ночь

С++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);
Размеров поста не хватает, чтобы обсудить все изменения, поэтому вот вам ссылочка , там они все перечислены. Don't be fooled. Stay cool #cpp26 #fun

​​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

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

Идея - главный капитал на сегодня Я знаю кучу людей, у которых есть отдельная заметка в телефоне аля "бизнес идеи". Только вот никто эти идеи не реализует. "Тяжело", "нет времени", "не знаю, с чего начать?", "как раскрутиться?" - эти вопросы заталкивают мотивацию под плинтус. И все продолжают довольствоваться работой в найме и грезить о своем проекте. Но в современном мире ллм'ок отмазки больше не принимаются. Идеи достаточно, чтобы начать действовать. Не умеешь писать код? - За тебя напишут. Нет времени? - Тут недавно агенты антропиков смастерили С'шный компилятор на 100к строк кода за 2 недели(с багами и говноархитектурой, но все же). Так что твоя аппка для подсчета каллорий может увидеть свет уже через несколько недель, работая по вечерам. Нет плана? - Тебе напишут пошаговый план до получения первых клиентов. Но если тебе и этого мало и "мотиуацию надо поднять", то поможет коммьюнити солопренеров, в котором постоянно запускаются такие проекты. Например, Денис из канала @its_capitan запустил собственную детективную игру: каждый персонаж — это реальный Telegram-аккаунт. Там сидят AI-чат боты, которые отвечают за героев. Что в итоге: — 3 месяца на подготовку + 3 месяца на разработку — 40+ покупок за полтора месяца — выручка — $1500+ — чек — $40 — стек: Python, Telegram API, OpenAI + Anthropic Денис двигался в одиночку и изначально была только идея. А через полгода проект уже приносит деньги. В сообществе "Короче Капитан" разобрано уже куча таких запусков. Честно говорят про их успехи и (!)провалы. И главное - собирают и анализируют опыт этих проектов, чтобы с минимальными усилиями получить первых клиентов и масштабироваться. Я сам читаю этот канал и если б не посты про кэш-линии, то уже давно запустил бы что-нибудь эдакое. А если серьезно, то навыки анализа идей и конкурентов и знания, как продвигать проект с минимальными усилиями становятся все более актуальными сейчас. Подпишись на канал, набирай насмотренность и смелость реализовывать свои идеи.

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

​​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