C++ geek
3.61K subscribers
277 photos
5 videos
28 links
Учим C/C++ на примерах
Download Telegram
📌 Оптимизация кода: std::string_view вместо std::string

Привет, друзья! Сегодня хочу рассказать про std::string_view — полезный инструмент, который может значительно ускорить работу с строками в C++. Многие из вас, вероятно, используют std::string, но не всегда это лучший выбор.

Что такое std::string_view?
Это некопируемая, легковесная оболочка над строковыми данными. Она просто хранит указатель на начало строки и её длину, не создавая копии. Использование std::string_view вместо std::string позволяет избежать ненужных аллокаций памяти и ускорить код.

🔥 Пример использования:

#include <iostream>
#include <string_view>

void print(std::string_view str) { // Без лишнего копирования
std::cout << str << '\n';
}

int main() {
std::string s = "Hello, world!";
print(s); // Можно передавать std::string
print("Hi there"); // Можно передавать строковый литерал
}


🛠 Когда использовать?
При передаче строк в функции, если их не нужно модифицировать.
Для работы с подстроками (в отличие от std::string::substr, который делает копию).
Для обработки строк без создания динамических объектов.

⚠️ Важно помнить:
- std::string_view не владеет данными, поэтому нельзя использовать его для длительного хранения указателей на временные строки.
- Нужно быть осторожным с объектами, чей срок жизни может закончиться, пока std::string_view ещё используется.

🚀 Итог:
Использование std::string_view вместо const std::string& может ускорить работу с текстовыми данными и снизить нагрузку на аллокатор. Если не нужно изменять строку — это отличный выбор!

А вы уже используете std::string_view в своих проектах? Делитесь в комментариях! ⬇️

➡️ @cpp_geek
👍4
🔥 Оптимизация кода на C++: Ранний возврат вместо вложенных условий

Привет, друзья! Сегодня хочу поговорить об одной важной технике, которая делает код чище и читабельнее — ранний возврат (early return). Часто встречаю код, который уходит в глубину вложенных if, превращаясь в настоящий лабиринт. Давайте разберем, как этого избежать.

Плохой пример: Вложенные условия

void process(int value) {
if (value > 0) {
if (value % 2 == 0) {
if (value < 100) {
std::cout << "Обрабатываем " << value << std::endl;
} else {
std::cout << "Слишком большое число" << std::endl;
}
} else {
std::cout << "Нечетное число" << std::endl;
}
} else {
std::cout << "Отрицательное число" << std::endl;
}
}

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

Хороший пример: Ранний возврат

void process(int value) {
if (value <= 0) {
std::cout << "Отрицательное число" << std::endl;
return;
}
if (value % 2 != 0) {
std::cout << "Нечетное число" << std::endl;
return;
}
if (value >= 100) {
std::cout << "Слишком большое число" << std::endl;
return;
}

std::cout << "Обрабатываем " << value << std::endl;
}

Теперь код сразу проверяет граничные условия и делает ранний возврат (return), если условия не выполнены. В итоге у нас получился плоский код, который проще читать и сопровождать.

🎯 Вывод:
- Избегайте вложенных if, если можно этого не делать.
- Используйте ранний возврат, чтобы код был линейным и понятным.
- Чем меньше уровней вложенности — тем легче отладка и сопровождение.

➡️ @cpp_geek
👍124
📌 Оптимизация кода в C++: Используем std::move правильно!

Привет, друзья! Сегодня я расскажу об одной из самых частых ошибок, связанных с std::move. Многие знают, что std::move не перемещает объект, а лишь превращает его в rvalue. Но как его использовать правильно? Давайте разбираться!

Ошибка: Бессмысленный std::move

std::string getString() {
std::string str = "Hello, world!";
return std::move(str); // Неэффективно
}

Что здесь не так? Возвращаемый std::string и так является временным объектом (NRVO — оптимизация возврата), и std::move мешает этой оптимизации! В результате компилятор не сможет выполнить перемещение, а вызовет копирование.

Правильный вариант:

std::string getString() {
return "Hello, world!"; // NRVO оптимизация
}


🏆 Где std::move полезен?
Используйте std::move, когда точно знаете, что объект больше не нужен и его можно переместить:

void processString(std::string str) { /* ... */ }

int main() {
std::string s = "Example";
processString(std::move(s)); // 🔥 Теперь перемещение!
}


1️⃣ Не используйте std::move при возврате локальных объектов — дайте компилятору сделать свое дело!
2️⃣ Используйте std::move, когда объект больше не нужен — это ускорит работу кода.
3️⃣ После std::move не используйте переменную, кроме как для присвоения нового значения.

➡️ @cpp_geek
👍53
🚀Это отличный ресурс для программистов, работающих с C++. Можно найти подробную документацию по стандартной библиотеке, STL, различным версиям стандарта C++, а также примеры кода и объяснения по ключевым аспектам языка.

Справочник по C++
C++11, C++14, C++17, C++20, C++23, C++26 │ Поддержка компиляторами C++11, C++14, C++17, C++20, C++23, C++26

Справочник по языку C
C89, C95, C99, C11, C17, C23 │ Поддержка компиляторами C99, C23

https://ru.cppreference.com/w/

➡️ @cpp_geek
🗿8🔥6
Media is too big
VIEW IN TELEGRAM
Улучшенные версии STL-контейнеров из библиотеки Boost
Илья Мещерин

В любом учебном курсе по C++, даже начального уровня, обязательно изучают, как устроен std::vector. Детали внутреннего устройства std::vector в подробностях продолжают изучать в вузах, спрашивать на собеседованиях, обсуждать на конференциях. То же самое происходит с контейнерами std::list, std::deque, std::map и std::unordered_map: про их реализацию и особенности внутреннего устройства можно говорить бесконечно долго, про них все еще делают доклады, снимают лекции и пишут статьи. И их продолжают использовать в продакшен-коде даже в самых крупных и известных компаниях.

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

Спикер обсудил внутреннее устройство не таких уж стандартных контейнеров: stable_vector, devector, bimap, circular_buffer, а также интрузивных версий list, map, unordered_map и их разновидностей.

источник

➡️ @cpp_geek
👍7
🔥 Оптимизация кода на C++: Ранний возврат вместо вложенных условий

Привет, друзья! Сегодня хочу поговорить об одной важной технике, которая делает код чище и читабельнее — ранний возврат (early return). Часто встречаю код, который уходит в глубину вложенных if, превращаясь в настоящий лабиринт. Давайте разберем, как этого избежать.

Плохой пример: Вложенные условия

void process(int value) {
if (value > 0) {
if (value % 2 == 0) {
if (value < 100) {
std::cout << "Обрабатываем " << value << std::endl;
} else {
std::cout << "Слишком большое число" << std::endl;
}
} else {
std::cout << "Нечетное число" << std::endl;
}
} else {
std::cout << "Отрицательное число" << std::endl;
}
}

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

Хороший пример: Ранний возврат

void process(int value) {
if (value <= 0) {
std::cout << "Отрицательное число" << std::endl;
return;
}
if (value % 2 != 0) {
std::cout << "Нечетное число" << std::endl;
return;
}
if (value >= 100) {
std::cout << "Слишком большое число" << std::endl;
return;
}

std::cout << "Обрабатываем " << value << std::endl;
}

Теперь код сразу проверяет граничные условия и делает ранний возврат (return), если условия не выполнены. В итоге у нас получился плоский код, который проще читать и сопровождать.

🎯 Вывод:
- Избегайте вложенных if, если можно этого не делать.
- Используйте ранний возврат, чтобы код был линейным и понятным.
- Чем меньше уровней вложенности — тем легче отладка и сопровождение.

➡️ @cpp_geek
6👍5
🚀 Микро-оптимизация в C++20: Early Return + Атрибуты вероятности

В прошлом посте мы разобрали, как Early Return (ранний возврат) спасает нас от вложенных if и делает код чище. Но в C++20 мы можем сделать этот код еще и потенциально быстрее!

Встречайте атрибуты [[likely]] и [[unlikely]].

🧠 В чем суть?
Современные процессоры пытаются предсказать, какую ветку кода программа выполнит следующей (Branch Prediction). Если процессор угадал - всё летает. Если ошибся - теряем такты на очистку конвейера.

С помощью атрибутов мы даем компилятору (и процессору) «инсайд»: какая ветка будет выполняться чаще.

🛠 Как это выглядит в коде?

Обычно ошибки и проверки аргументов (Guard Clauses) срабатывают редко. Это идеальное место для [[unlikely]].


void ProcessImage(Image* img) {
// 1. Проверка на null.
// Это случается редко, помечаем как "маловероятно".
if (img == nullptr) [[unlikely]] {
return; // Компилятор уведет этот код "подальше" из горячего пути
}

// 2. Еще одна проверка
if (img->IsEmpty()) [[unlikely]] {
return;
}

// --- Happy Path ---
// Процессор сразу прыгнет сюда, ожидая, что проверки выше ложны.
img->ApplyFilter();
img->Save();
}



⚙️ Что происходит под капотом?
Компилятор переставит инструкции ассемблера так, чтобы «счастливый путь» шел линейно, без прыжков (jmp), что улучшает работу кэша инструкций. Код обработки ошибок (ветка [[unlikely]]) будет сдвинут в конец функции или в «холодную» зону.

⚠️ Важный нюанс:
Используйте это только тогда, когда вы уверены в вероятностях (например, ошибки случаются в 1 случае из 1000). Если поставить атрибуты наугад, можно сделать только хуже (pessimization).

🔥 Итог:
Чистый код (Early Return) + Подсказки компилятору ([[unlikely]]) = Читаемость и Производительность.

#cpp #cpp20 #coding #optimization #tips #programming

➡️ @cpp_geek
🔥12
✂️ C++17: Перестаньте копировать строки! (std::string_view)

Мы привыкли передавать строки в функции по константной ссылке: const std::string&. Нам кажется, что это эффективно, ведь мы не копируем объект, верно?

Не всегда. 🛑

Если вы передаете в такую функцию обычный текст в кавычках (строковый литерал) или часть другой строки, C++ втайне от вас создаст временный объект std::string, выделит память в куче (heap allocation), скопирует туда данные и только потом передаст ссылку.

Решение? std::string_view.

👀 Что это такое?
std::string_view - это супер-легкий объект, который ничего не хранит сам. Он просто «смотрит» на существующую строку. Внутри него только указатель на начало текста и длина.

Никаких аллокаций. Никаких копий. Ноль оверхеда.

🆚 Сравним:


// 🐢 ПЛОХО (до C++17)
void Log(const std::string& msg) { /* ... */ }

// При вызове создается временный std::string!
Log("Critical Error");




// 🚀 ХОРОШО (C++17)
void Log(std::string_view msg) { /* ... */ }

// Никаких аллокаций. Просто передаем указатель и длину.
Log("Critical Error");



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

⚫️std::string::substr() - создает новую строку (копирование + аллокация).
⚫️std::string_view::substr() - просто сдвигает указатель и меняет размер (математическая операция за наносекунды).

⚠️ Осторожно! (Подводный камень)
Так как string_view не владеет данными, а только смотрит на них, вы должны быть уверены, что исходная строка живет дольше, чем string_view.

⚫️ Использовать как аргумент функции.
⚫️ Возвращать из функции, если исходная строка была локальной переменной.

💡 Итог:
Если вам нужно только «почитать» строку (в аргументах функции), почти всегда используйте std::string_view вместо const std::string&.

#cpp #cpp17 #optimization #stringview #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍94
🏗 Что на самом деле происходит, когда std::vector «лопается»?

Мы все любим push_back. Это удобно: кидаешь данные в вектор, а он сам разбирается с памятью. Но что происходит, когда вы добавляете элемент, а capacity (вместимость) вектора закончилась?

Происходит Реаллокация (Reallocation). И это гораздо дороже, чем кажется.

⚙️ Алгоритм катастрофы:

1. Поиск новой земли: Вектор понимает, что места нет. Он обращается к оперативной памяти и просит выделить новый блок памяти. Обычно он в 1.5 или 2 раза больше текущего.

2. Великое переселение: Все элементы из старого блока памяти копируются (или перемещаются, если есть noexcept move-конструктор) в новый блок.
⚫️Если у вас там 1,000,000 тяжелых объектов - удачи процессору. 😅


3. Уничтожение: Для всех объектов в старом блоке вызываются деструкторы.

4. Снос: Старый блок памяти возвращается системе.

🚨 Почему это проблема?

1. Удар по производительности:
Обычно push_back работает за O(1) (мгновенно). Но в момент реаллокации сложность подскакивает до O(N). Это создает непредсказуемые лаги (latency spikes). В системах реального времени (gamedev, high-load) это недопустимо.

2. Инвалидация итераторов и ссылок (ОПАСНО):
Это источник багов №1.

std::vector<int> vec = {1, 2, 3};
int& ref = vec[0]; // Ссылка на первый элемент

// ... добавляем много элементов ...
for(int i=0; i < 100; ++i) vec.push_back(i);

// 💥 Вектор переехал в новую память.
// Старая память удалена. ref теперь указывает в мусор.
std::cout << ref; // Undefined Behavior (Crash)




🛡 Как лечить?

Если вы хотя бы примерно знаете, сколько элементов будет в векторе, всегда используйте reserve().


std::vector<User> users;
users.reserve(1000); // Сразу выделяем память

// Теперь первые 1000 push_back будут дешевыми
// и не вызовут реаллокации.



💡 Итог: std::vector это отличный инструмент, но за его «магию» расширения платит процессор. Помогайте ему через reserve().

#cpp #stdvector #performance #memory #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍84👀4💯1
📦 std::move vs std::forward: Когда и зачем?

На собеседованиях часто спрашивают про rvalue-ссылки, но в реальном коде мы постоянно путаемся: когда делать move, а когда forward?

Давайте разберем на жизненных примерах.

1. std::move - "Это мое, но забирай!" 🚚

std::move - это безусловное приведение к rvalue. Вы говорите компилятору: "Мне этот объект больше не нужен. Можешь выпотрошить его и забрать данные, не копируя их".

Сценарий 1: Передача владения (unique_ptr)
Это классика. std::unique_ptr нельзя скопировать, его можно только переместить.


auto ptr = std::make_unique<BigData>();

// process(ptr); // Ошибка компиляции! Копирование запрещено.
process(std::move(ptr)); // ОК. Владение передано, ptr теперь пуст.



Сценарий 2: Оптимизация тяжелых объектов
У вас есть локальный вектор, который вы хотите сохранить в поле класса. Зачем его копировать?


void SetData(std::vector<int> newData) {
// Мы крадем буфер памяти у newData.
// Копирования элементов НЕ происходит.
this->data_ = std::move(newData);
}




2. std::forward - "Я просто посредник" 📮

std::forward используется почти исключительно в шаблонах. Его цель - Perfect Forwarding (Идеальная передача).

Представьте, что вы пишете функцию-обертку (wrapper). Она принимает аргумент и должна передать его дальше другой функции.

⚫️Если ей передали временный объект (rvalue) - она должна передать его как rvalue (чтобы сработал move).
⚫️Если передали обычную переменную (lvalue) - она должна передать как lvalue (копия).

std::move здесь всё испортит (он всё превратит в rvalue). Тут нужен std::forward.

Сценарий: Фабрики и Обертки


template <typename T>
void LogAndAdd(std::vector<T>& vec, T&& item) {
std::cout << "Adding item...";

// forward сохранит категорию значения item.
// Если item был временным — сработает push_back(T&&) (перемещение).
// Если item был переменной — сработает push_back(const T&) (копия).
vec.push_back(std::forward<T>(item));
}




⚡️ Шпаргалка

1. std::move используем, когда мы знаем, что объект нам больше не нужен, и мы хотим отдать его ресурсы (обычный код).
2. std::forward используем, когда мы пишем шаблон, который принимает "универсальную ссылку" (T&&), и нам нужно пробросить аргумент дальше "как есть" (библиотечный код).

#cpp #cpp11 #movesemantics #coding #interview #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍83
🏗 Анатомия std::vector::push_back: Когда память заканчивается

Мы все любим push_back. Это удобно: просто кидаешь данные в вектор, а он сам разбирается с памятью. Но что происходит, когда вы добавляете элемент, а место (capacity) закончилось?

Происходит Реаллокация (Reallocation). И это дорогая операция.

⚙️ Что происходит «под капотом»?

1. Поиск новой земли: Вектор понимает, что текущий буфер полон. Он просит у операционной системы выделить новый блок памяти. Обычно он в 1.5 или 2 раза больше предыдущего (геометрический рост).

2. Великое переселение: Все элементы из старого блока копируются (или перемещаются, если есть noexcept move-конструктор) в новый блок.
⚫️Представьте, что вы перевозите 10,000 коробок в новый дом только ради того, чтобы поставить еще одну.

3. Зачистка: Для всех объектов в старом блоке вызываются деструкторы.

4. Снос: Старая память возвращается системе.

🚨 Почему это проблема?

1. Удар по производительности:
Обычно push_back работает за амортизированное O(1) (мгновенно). Но в момент реаллокации сложность подскакивает до O(N). Это вызывает непредсказуемые лаги (latency spikes).

2. Инвалидация итераторов и ссылок (ОПАСНО):
Это источник багов №1. После реаллокации старая память удалена. Все указатели, ссылки и итераторы, которые смотрели на элементы вектора, становятся недействительными.


std::vector<int> vec = {1, 2, 3};
int& ref = vec[0]; // Ссылка на первый элемент

// ... добавляем много элементов, вызывая реаллокацию ...
for(int i=0; i < 100; ++i) vec.push_back(i);

// 💥 Вектор переехал. Старая память удалена.
// ref теперь указывает в мусор.
std::cout << ref; // Undefined Behavior (Crash или мусор)





🛡 Как лечить?

Если вы хотя бы примерно знаете, сколько элементов будет в векторе, всегда используйте reserve().


std::vector<User> users;
users.reserve(1000); // Сразу выделяем память под 1000 мест

// Теперь первые 1000 push_back будут дешевыми
// и гарантированно не вызовут реаллокации.



💡 Итог: std::vector это мощный инструмент, но за его автоматическое расширение платит процессор. Помогайте ему через reserve(), чтобы код был быстрым и безопасным.

#cpp #stdvector #performance #memory #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍72
🏗 Анатомия std::vector: Что происходит, когда место заканчивается?

std::vector - самый популярный контейнер в C++. Мы просто пишем push_back, и магия работает. Но что происходит «под капотом», когда вы пытаетесь добавить элемент, а свободное место (capacity) закончилось?

Происходит Реаллокация. И это гораздо дороже, чем просто добавление числа.

⚙️ Сценарий катастрофы (пошагово):

Допустим, у вектора было место под 4 элемента, и оно занято. Вы добавляете 5-й.

1. Поиск новой земли: Вектор понимает, что текущий буфер полон. Он просит у операционной системы выделить новый блок памяти (обычно в 1.5 или 2 раза больше старого).

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

3. Зачистка: Старые объекты разрушаются (вызываются деструкторы), а старая память возвращается системе.

4. Вставка: И только теперь новый элемент добавляется в хвост.

🚨 Почему это проблема?

1. Удар по производительности
Операция push_back обычно мгновенна (). Но при реаллокации она превращается в тяжелую операцию . Если вектор огромный, программа может «подвиснуть» в самый неподходящий момент.

2. Инвалидация ссылок (Источник багов №1)
Это самое опасное. Как только произошла реаллокация, старая память удаляется. Все указатели, ссылки и итераторы, которые смотрели на элементы вектора, становятся невалидными.


std::vector<int> data = {1, 2, 3, 4};
int& ref = data[0]; // Ссылка на первый элемент

// Добавляем элемент -> места нет -> реаллокация!
data.push_back(5);

// ☠️ ОШИБКА: ref ссылается на очищенную память.
// Получим мусор или краш программы.
std::cout << ref;



🛡 Как лечить?

Если вы знаете (хотя бы примерно), сколько элементов будет в векторе - используйте reserve().


std::vector<int> data;
data.reserve(1000); // Сразу выделяем память

// Теперь реаллокации точно не будет,
// пока мы не превысим 1000 элементов.



💡 Итог: Помогайте вектору с помощью reserve(). Это спасает и от тормозов, и от сложнейших багов с памятью.

#cpp #stdvector #memory #performance #coding #tips

➡️ @cpp_geek
👍122
🏗 Тетрис в памяти: Почему порядок полей в классе важен?

Вы создали простую структуру: bool, int и еще один bool.
Математика проста: 1 байт + 4 байта + 1 байт = 6 байт.

Вы проверяете через sizeof и видите... 12 байт. 🤯
Куда делись еще 6 байт? Вы только что потеряли 50% памяти на "воздух".

Это называется Padding (Выравнивание).

⚙️ Как это работает?
Процессор не любит читать данные по произвольным адресам. Ему удобно читать кусками по 4 или 8 байт (слова). Чтобы int (4 байта) не "разломился" посередине двух слов, компилятор вставляет пустые байты-заглушки.

Плохой пример (Bad Layout):


struct Bad {
bool a; // 1 байт
// ... 3 байта PADDING (воздух) ...
int b; // 4 байта (должен начинаться с кратного 4 адреса)
bool c; // 1 байт
// ... 3 байта PADDING (чтобы выровнять общий размер) ...
};
// Итог: 12 байт



Хороший пример (Good Layout):

Просто меняем порядок полей. Правило: "От больших к маленьким".


struct Good {
int b; // 4 байта
bool a; // 1 байт
bool c; // 1 байт
// ... 2 байта PADDING (добиваем до кратности 4) ...
};
// Итог: 8 байт



📉 Почему это важно?
Кажется, что 4 байта ерунда. Но если у вас std::vector<Bad> на 1,000,000 элементов:

⚫️Bad: ~12 MB памяти.
⚫️Good: ~8 MB памяти.

Вы экономите 4 мегабайта просто переставив строчки местами! Плюс, более плотные данные лучше ложатся в кэш процессора (CPU Cache), что ускоряет обработку.

💡 Совет:
Объявляйте поля в порядке убывания их размера:

1. Указатели и double (8 байт)
2. int, float (4 байта)
3. short (2 байта)
4. bool, char (1 байт)

#cpp #optimization #memory #alignment #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144💯2
🔒 const в C++: Скрытый смысл, о котором молчат

Мы привыкли думать, что const после имени метода это просто защита от дурака: "Я обещаю не менять поля класса внутри этой функции".

Но в современном C++ (и в стандартной библиотеке STL) const означает нечто большее. Это контракт потокобезопасности (Thread Safety Contract).

🧵 Золотое правило STL:

1. const методы можно вызывать из разных потоков одновременно без блокировок. (Safe for concurrent reads).

2. Не-const методы требуют внешней синхронизации, если их вызывают несколько потоков.

🚨 Где кроется ловушка?

Ловушка в ключевом слове mutable.
Оно позволяет менять поля даже внутри const метода. Обычно это используют для кэширования или ленивых вычислений.

ОПАСНЫЙ КОД (Логический const, но физическая гонка):


class Widget {
mutable int cachedValue_ = -1; // Можно менять в const методе

public:
// Метод помечен const. Пользователь думает, что он безопасен
// для вызова из 10 потоков одновременно.
int GetValue() const {
if (cachedValue_ == -1) {
// 💥 DATA RACE!
// Два потока могут одновременно зайти сюда и начать писать.
cachedValue_ = HeavyCalculation();
}
return cachedValue_;
}
};



Если вы пишете библиотеку и помечаете метод как const, пользователи будут вызывать его параллельно, не используя мьютексы. Если внутри у вас есть несинхронизированный mutable - программа упадет.

Правильный подход:

Если вы используете mutable, вы обязаны защитить его мьютексом.


class Widget {
mutable std::mutex mtx_; // Мьютекс тоже должен быть mutable!
mutable int cachedValue_ = -1;

public:
int GetValue() const {
std::lock_guard<std::mutex> lock(mtx_); // Блокируем поток

if (cachedValue_ == -1) {
cachedValue_ = HeavyCalculation();
}
return cachedValue_;
}
};



💡 Итог: В C++ const - это не только "я не меняю данные". Это обещание: "Этот метод безопасен для одновременного вызова". Если вы нарушаете это обещание (используя mutable без защиты), вы создаете бомбу замедленного действия.

#cpp #multithreading #const #safety #coding #tips

➡️ @cpp_geek
👍81
🪄 Магия std::string: Почему короткие строки работают быстрее? (SSO)

Многие думают, что std::string - это всегда:

1. Выделение памяти в куче (new / malloc).

2. Копирование данных туда.

3. Освобождение памяти (delete) в деструкторе.

Это медленно. Но если вы создадите строку "Hello", никаких аллокаций не произойдет. Почему?

Благодаря Small String Optimization (SSO).

⚙️ Как это работает?

Стандартная строка (на 64-битной системе) обычно занимает 24 или 32 байта (размер самой структуры sizeof(std::string)). В ней хранятся указатель на данные, размер и вместимость (capacity).

Разработчики STL подумали:
"Зачем нам тратить эти байты на указатели, если строка очень короткая? Давайте хранить текст прямо внутри объекта!"

Внутри std::string используется union:

⚫️Вариант А (Длинная строка): Хранит указатель на кучу (Heap), размер и вместимость.
⚫️Вариант Б (Короткая строка): Использует те же байты памяти как буфер для хранения символов.

📏 Где граница?

Это зависит от компилятора:

⚫️MSVC (Windows): ~15 символов.
⚫️GCC (Linux): ~15 символов.
⚫️Clang (libc++): ~22 символа (благодаря хитрому сжатию битов).

Пример:


void Benchmark() {
// 🚀 БЫСТРО (SSO):
// Память не выделяется. Строка лежит на стеке, как char[16].
std::string shortStr = "Hello World";

// 🐢 МЕДЛЕННО (Heap Allocation):
// Текст не влезает в буфер SSO.
// Вызывается malloc/new, данные летят в кучу.
std::string longStr = "Hello World is a remarkably long phrase";
}



📉 Почему это важно для производительности?

1. Нет аллокаций: new и delete - это системные вызовы, они дорогие. SSO их исключает.

2. Cache Locality: Данные лежат на стеке, рядом с другими локальными переменными. Процессор обожает линейный доступ к памяти (L1 Cache), а прыжки в кучу (Heap) - ненавидит.

💡 Совет: Если вы оптимизируете структуру данных и у вас много коротких ID или имен (до 15 символов), обычный std::string будет работать великолепно без всяких хитростей. Не нужно менять его на char[] "для скорости" без замеров.

#cpp #optimization #sso #memory #stdstring #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🔥63
😱 std::vector<bool>: Великий обман C++

Вы думаете, что std::vector<bool> это просто вектор, который хранит булевы значения? Нет. Это совершенно уникальный монстр, который нарушает правила стандартной библиотеки.

📉 В чем подвох? Обычный bool занимает 1 байт (минимум адресуемой памяти). Но создатели C++ решили сэкономить память. std::vector<bool> - это специализация. Внутри него каждый bool занимает всего 1 бит.

В одном байте хранится сразу 8 значений true/false. Экономия памяти в 8 раз! Круто?

🛑 Проблема: Вы не можете взять адрес элемента

В C++ нельзя создать указатель или ссылку на отдельный бит. Память адресуется байтами.


std::vector<int> nums = {1, 2};
int* p = &nums[0]; // ОК. Указатель на первый int.

std::vector<bool> flags = {true, false};
bool* b = &flags[0]; // ОШИБКА КОМПИЛЯЦИИ!
// Мы не можем получить адрес бита.



🤖 Проблема: Прокси-объекты

Когда вы пишете flags[0], вектор возвращает не bool& (ссылку), а специальный временный объект - Proxy Class (std::vector<bool>::reference).

Этот объект "притворяется" ссылкой. Когда вы присваиваете ему значение, он делает побитовые сдвиги и маски (&, |, <<), чтобы изменить нужный бит внутри байта.

Это медленно.

⚠️ Ловушка с auto


std::vector<bool> vec = {true, false};

// Вы думаете, что val — это bool.
// На самом деле val — это 'std::vector<bool>::reference'.
auto val = vec[0];

vec.push_back(true); // Реаллокация памяти!

// 💥 Если val — это прокси, он может ссылаться на
// старую, уже удаленную память вектора.
val = false; // Undefined Behavior / Crash



💡 Что делать?

1. Если вам важна память: Используйте std::vector<bool> (или std::bitset для фиксированного размера).

2. Если вам важна скорость: Используйте std::vector<char> или std::vector<uint8_t>. Это займет в 8 раз больше памяти, но будет работать мгновенно, и вы получите нормальные ссылки.

3. Осторожно с auto: Всегда пишите тип явно: bool val = vec[0];, чтобы заставить прокси превратиться в значение.

#cpp #stl #vector #gotchas #memory #coding #tips

➡️ @cpp_geek
👍84
🗺 std::map или std::unordered_map: Битва за кэш

Когда нам нужно хранить пары «Ключ - Значение», рука сама тянется написать std::map. Это стандарт, это удобно, это сортировка из коробки.

Но с точки зрения производительности std::map это часто худший выбор. Почему?

🌲 1. std::map - Это Дерево (Red-Black Tree)
Каждый элемент в map - это отдельный узел (Node), выделенный в куче (new). Узлы разбросаны по памяти хаотично.

• Чтобы найти элемент, процессор прыгает по указателям: Root -> Left -> Right -> ...
• Каждый прыжок - это потенциальный Cache Miss (промах кэша). Процессор ждет сотни тактов, пока данные подтянутся из RAM.
• Сложность поиска: O(log N).

2. std::unordered_map - Это Хеш-таблица
Здесь нет деревьев. Ключ превращается в число (хеш), и мы сразу прыгаем в нужную ячейку массива (Bucket).

• Массивы любят кэш процессора (Cache Locality).
• Сложность поиска: O(1) (в среднем). Это мгновенно.

🐢 Насколько велика разница?
На маленьких объемах (до 100 элементов) разницы почти нет.
Но на 1,000,000 элементов std::unordered_map может быть в 3-5 раз быстрее просто за счет отсутствия прыжков по памяти.

🤔 Когда использовать std::map?
Только в одном случае: Вам жизненно важен порядок ключей.
Например, если вы хотите вывести пользователей по алфавиту или найти диапазон дат (lower_bound / upper_bound).

🚀 Бонус: C++23 std::flat_map
В новом стандарте завезли std::flat_map. Это гибрид: интерфейс как у map (сортированный), но внутри - сплошной вектор.
Это самый быстрый вариант для поиска, но медленный для вставки. Если у вас C++23 - присмотритесь!

💡 Итог: если вам не нужна сортировка, всегда пишите std::unordered_map. Не заставляйте процессор бегать по дереву указателей без причины.

#cpp #stl #optimization #performance #map #hashing #coding #tips

➡️ @cpp_geek
👍11🔥54
🌉 Забудьте про передачу указателей и размеров! (std::span)

Помните, мы обсуждали std::string_view - легковесное «окно» для строк? В C++20 у него появился старший брат для массивов и векторов - std::span.

До C++20 у нас была классическая проблема. Допустим, вы пишете функцию, которая должна обработать список чисел.

🐢 Как мы писали раньше:

Вариант 1: Принимать const std::vector<int>&.
Минус: Функция теперь намертво привязана к std::vector. Если у вас данные лежат в std::array или обычном си-массиве int arr[10], придется копировать их в вектор. Аллокации, тормоза.

Вариант 2: Си-стайл (Указатель + размер).
Минус: Легко ошибиться с размером, потерять контекст, код выглядит грязно.


void ProcessOld(const int* data, size_t size) { /* ... */ }



🚀 Как мы пишем теперь (C++20):


#include <span>

// Принимаем любой непрерывный кусок памяти!
void ProcessNew(std::span<const int> data) {
for (int val : data) {
std::cout << val << " ";
}
}



👀 Что такое std::span?
Как и string_view, это просто указатель на начало данных и их длина (обычно 16 байт). Он не владеет памятью, он только на нее смотрит.

Магия в том, что std::span умеет автоматически создаваться из чего угодно:


std::vector<int> vec = {1, 2, 3};
std::array<int, 3> arr = {4, 5, 6};
int raw[3] = {7, 8, 9};

// Одна функция работает со всеми типами контейнеров! Без копирования!
ProcessNew(vec);
ProcessNew(arr);
ProcessNew(raw);



✂️ Суперсила: Subspan (Подмассивы)
Вам нужно передать в функцию только часть вектора, например, со 2-го по 5-й элемент? Никаких итераторов и копирования:


// Передаем кусок вектора за O(1)
ProcessNew( std::span{vec}.subspan(1, 4) );



⚠️ Важный нюанс:
std::span не умеет изменять размер данных (никаких push_back). Но он может изменять сами элементы, если вы передадите std::span<int> (без const).

💡 Итог: Если ваша функция принимает набор данных только для чтения или изменения элементов на месте, всегда используйте std::span. Это золотой стандарт современного C++.

#cpp #cpp20 #stdspan #optimization #memory #coding #tips

➡️ @cpp_geek
👍10🔥32
🚀 Подборка полезных IT каналов в Max


Системное администрирование, DevOps 📌

https://max.ru/i_odmin Все для системного администратора
https://max.ru/bash_srv Bash Советы
https://max.ru/sysadminof Книги для админов, полезные материалы
https://max.ru/i_odmin_book Библиотека Системного Администратора
https://max.ru/i_devops DevOps: Пишем о Docker, Kubernetes и др.

1C разработка 📌
https://max.ru/odin1c_rus Cтатьи, курсы, советы, шаблоны кода 1С

Программирование C++📌
https://max.ru/cpp_lib Библиотека C/C++ разработчика

Программирование Python 📌
https://max.ru/python_of Python академия.
https://max.ru/BookPython Библиотека Python разработчика

Java разработка 📌
https://max.ru/bookjava Библиотека Java разработчика

GitHub Сообщество 📌
https://max.ru/githublib Интересное из GitHub

Базы данных (Data Base) 📌
https://max.ru/database_info Все про базы данных

Фронтенд разработка 📌
https://max.ru/frontend_1 Подборки для frontend разработчиков

Библиотеки 📌
https://max.ru/programmist_of Книги по программированию
https://max.ru/proglb Библиотека программиста
https://max.ru/bfbook Книги для программистов

Программирование 📌
https://max.ru/bookflow Лекции, видеоуроки, доклады с IT конференций
https://max.ru/itmozg Программисты, дизайнеры, новости из мира IT
https://max.ru/php_lib Библиотека PHP программиста 👨🏼‍💻👩‍💻

Шутки программистов 📌
https://max.ru/itumor Шутки программистов

Защита, взлом, безопасность 📌
https://max.ru/thehaking Канал о кибербезопасности
https://max.ru/xakkep_1 Хакер Free

Книги, статьи для дизайнеров 📌
https://max.ru/odesigners Статьи, книги для дизайнеров

Математика 📌
https://max.ru/Pomatematike Канал по математике
https://max.ru/phismat_1 Обучающие видео, книги по Физике и Математике

Вакансии 📌
https://max.ru/progjob Вакансии в IT

Мир технологий 📌
https://max.ru/mir_teh Канал для любознательных


Бонус 📌
https://max.ru/piterspb_78 Свежие новости Санкт-Петербурга
https://max.ru/mockva_life Свежие новости Москвы
💩8🤡4🤣4🤮2
C++: Заставьте компилятор работать за вас (constexpr и consteval)

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

В современном C++ мы можем «запекать» результаты функций прямо в итоговый .exe файл. Для этого есть два инструмента.

1. constexpr - «Вычисли до запуска, если сможешь» (C++11)

Ключевое слово constexpr говорит компилятору: "Если все аргументы этой функции известны заранее, вычисли её прямо сейчас. Если нет - оставь до выполнения программы (Run-time)".

Это невероятно удобно для универсальных функций.


// Функция может работать и до запуска, и во время!
constexpr int GetArea(int width, int height) {
return width * height;
}

int main() {
// 🚀 Вычислится компилятором! В код вставится просто "200".
// Zero runtime cost.
int a = GetArea(10, 20);

int w;
std::cin >> w;
// 🐢 Вычислится процессором во время работы (w неизвестно заранее).
int b = GetArea(w, 20);
}



2. consteval - «Вычисли до запуска, или умри!» (C++20)

У constexpr есть проблема: мы не всегда уверены, вычислилась ли функция компилятором, или она тихо «соскользнула» в Run-time, замедляя программу.

Поэтому в C++20 добавили consteval. Это строгий приказ (Immediate Function). Если компилятор не может выполнить функцию прямо сейчас - он выдаст ошибку компиляции.


// Обязана выполниться во время компиляции
consteval int MagicHash(std::string_view str) {
int hash = 0;
for (char c : str) hash += c;
return hash;
}

int main() {
// Отлично. Компилятор сам посчитает хэш слова "admin".
int h1 = MagicHash("admin");

std::string user_input = "test";
// ОШИБКА КОМПИЛЯЦИИ! user_input нельзя знать заранее.
int h2 = MagicHash(user_input);
}



📈 Зачем это нужно?

1. Максимальная производительность: Вы переносите время выполнения на этап сборки программы. Для пользователя всё работает за O(1).

2. Замена #define: Раньше константы и простые формулы писали через макросы препроцессора. Теперь constexpr делает это безопасно, с проверкой типов.

3. Безопасность: С consteval вы гарантируете, что тяжелая инициализация (например, генерация таблиц поиска) не ударит по производительности в продакшене.

💡Итог: Пишете математику или чистые функции без побочных эффектов? Ставьте constexpr.
Хотите 100% гарантию, что вычисления не попадут в готовый бинарник? Ставьте consteval.

#cpp #cpp20 #constexpr #optimization #performance #coding #tips

➡️ @cpp_geek
👍53