🕵️♂️ Анатомия лямбд: Что скрывает компилятор и где прячутся тормоза?
Мы привыкли писать auto func = [x](int y) { return x + y; }; и думать о лямбдах как о «магических» встроенных функциях. Но в C++ нет никакой магии. На уровне компилятора лямбд не существует.
Лямбда-выражение - это просто синтаксический сахар для создания анонимного класса (функтора).
⚙️ Что происходит под капотом?
Когда вы пишете такой код:
Компилятор молча генерирует для вас класс с уникальным непроизносимым именем и перегруженным оператором ():
Вывод №1: Блок захвата [...] - это просто конструктор класса и его поля. Если вы захватываете по ссылке [&x], полем класса станет ссылка.
🚨 Где прячутся скрытые аллокации?
Сама по себе лямбда создается на стеке. Никаких new или malloc не происходит. Это работает мгновенно.
Тормоза начинаются, когда мы решаем сохранить лямбду с помощью std::function.
std::function - это тяжелая обертка (Type Erasure). Она должна уметь хранить любой вызываемый объект с нужной сигнатурой.
Чтобы избежать выделения памяти в куче, в std::function встроена оптимизация для маленьких объектов (SBO - Small Buffer Optimization). Размер этого буфера обычно равен размеру нескольких указателей (например, 16-32 байта).
* Если ваша лямбда захватывает мало данных (или ничего) - она поместится во внутренний буфер std::function. Аллокаций нет.
* Если вы написали [=] и захватили кучу тяжелых переменных (или несколько толстых строк/векторов по значению) - размер сгенерированного класса превысит буфер. std::function будет вынужден вызвать new и выделить память в куче.
💡 Правила чистого кода с лямбдами:
1. Храните через auto: Всегда используйте auto для локальных лямбд. Это бесплатно.
2. Передавайте через шаблоны: Если функция принимает лямбду, лучше сделать её шаблоном template <typename Func> void DoWork(Func f), чем принимать std::function. Шаблон сохранит тип лямбды и позволит компилятору заинлайнить код.
3. Используйте std::function только на границах API: Когда вам действительно нужно хранить функции разного типа в одном контейнере (например, вектор коллбеков) или передать их через границу динамической библиотеки.
4. Не пишите [=] вслепую: Захватывайте только то, что реально нужно. Это спасет и от лишних аллокаций, и от случайного копирования тяжелых объектов.
#cpp #lambdas #optimization #memory #under_the_hood #coding #tips
➡️ @channel_cpp_geek
Мы привыкли писать auto func = [x](int y) { return x + y; }; и думать о лямбдах как о «магических» встроенных функциях. Но в C++ нет никакой магии. На уровне компилятора лямбд не существует.
Лямбда-выражение - это просто синтаксический сахар для создания анонимного класса (функтора).
⚙️ Что происходит под капотом?
Когда вы пишете такой код:
int multiplier = 10;
auto doMath = [multiplier](int val) {
return val * multiplier;
};
Компилятор молча генерирует для вас класс с уникальным непроизносимым именем и перегруженным оператором ():
// Примерно это генерирует компилятор:
class __Lambda_Uniq_42 {
int multiplier; // Захваченная переменная стала полем класса
public:
__Lambda_Uniq_42(int m) : multiplier(m) {}
// Сам код лямбды:
auto operator()(int val) const {
return val * multiplier;
}
};
// А ваше создание лямбды превращается в:
auto doMath = __Lambda_Uniq_42(multiplier);Вывод №1: Блок захвата [...] - это просто конструктор класса и его поля. Если вы захватываете по ссылке [&x], полем класса станет ссылка.
🚨 Где прячутся скрытые аллокации?
Сама по себе лямбда создается на стеке. Никаких new или malloc не происходит. Это работает мгновенно.
Тормоза начинаются, когда мы решаем сохранить лямбду с помощью std::function.
// 🚀 БЫСТРО (Zero-cost)
auto fast_lambda = [a, b, c]() { ... };
// 🐢 МОЖЕТ БЫТЬ МЕДЛЕННО (Heap allocation)
std::function<void()> slow_lambda = [a, b, c, d, e]() { ... };
std::function - это тяжелая обертка (Type Erasure). Она должна уметь хранить любой вызываемый объект с нужной сигнатурой.
Чтобы избежать выделения памяти в куче, в std::function встроена оптимизация для маленьких объектов (SBO - Small Buffer Optimization). Размер этого буфера обычно равен размеру нескольких указателей (например, 16-32 байта).
* Если ваша лямбда захватывает мало данных (или ничего) - она поместится во внутренний буфер std::function. Аллокаций нет.
* Если вы написали [=] и захватили кучу тяжелых переменных (или несколько толстых строк/векторов по значению) - размер сгенерированного класса превысит буфер. std::function будет вынужден вызвать new и выделить память в куче.
💡 Правила чистого кода с лямбдами:
1. Храните через auto: Всегда используйте auto для локальных лямбд. Это бесплатно.
2. Передавайте через шаблоны: Если функция принимает лямбду, лучше сделать её шаблоном template <typename Func> void DoWork(Func f), чем принимать std::function. Шаблон сохранит тип лямбды и позволит компилятору заинлайнить код.
3. Используйте std::function только на границах API: Когда вам действительно нужно хранить функции разного типа в одном контейнере (например, вектор коллбеков) или передать их через границу динамической библиотеки.
4. Не пишите [=] вслепую: Захватывайте только то, что реально нужно. Это спасет и от лишних аллокаций, и от случайного копирования тяжелых объектов.
#cpp #lambdas #optimization #memory #under_the_hood #coding #tips
➡️ @channel_cpp_geek