C++ geek
3.62K subscribers
257 photos
3 videos
18 links
Учим C/C++ на примерах
Download Telegram
💰Олимпиада по программированию с призовым фондом 50 000 рублей!

🏃💨Для школьников от 10 до 16 лет, задачи можно решать на языках GO, Python, JavaScript, C++

🏆Решить олимпиаду можно 23 июля (среда) с 11:00 до 19:00

🗣Регистрация закроется 20 июля в 23:55

1️⃣ место - 25 000 рублей
2️⃣ место - 15 000 рублей
3️⃣ место - 10 000 рублей

😎Регистрируйся по ссылке, участие бесплатное
Олимпиада ZamaCode

📲А также подписывайся на наш телеграм-канал, чтобы не пропустить новые олимпиады и розыгрыши!
Почему std::move может не сработать

Многие думают, что std::move всегда приводит к перемещению. Но это не так. std::move не двигает — он просто превращает объект в rvalue-ссылку. Всё остальное — за перегрузками.

Вот ловушка:


void take(std::string s) {
// s — копия, даже если передавали std::move
}

std::string str = "hello";
take(std::move(str)); // копирование, не перемещение


Почему? Потому что take принимает по значению, а значит, всегда делает копию. Даже если ты передал std::move(str), она скопируется в s. Чтобы работало перемещение, нужна rvalue-ссылка:


void take(std::string&& s) {
// теперь s — это rvalue-ссылка
}


Но даже тут можно облажаться:


void take(std::string&& s) {
process(s); // снова копия!
}

void process(std::string); // по значению


Нужно снова вызвать std::move, если хочешь передать дальше как rvalue:


void take(std::string&& s) {
process(std::move(s)); // перемещение
}


Правило: std::move — это обещание, что объект можно "украсть". Но воровать будет только та функция, которая перегружена для rvalue.

➡️ @cpp_geek
👍8🤔32👨‍💻1
Что такое ADL и как она может вас подставить

Argument-Dependent Lookup (ADL) — это фича, которая может как упростить код, так и привести к сюрпризам.

Представьте:


namespace math {
struct Vec {};

void print(const Vec&) {
std::cout << "Vec\n";
}
}

void foo() {
math::Vec v;
print(v); // компилятор найдёт math::print через ADL!
}


Хотя print не в текущем скоупе, компилятор ищет её в namespace аргумента v, т.е. math. Это и есть ADL.

Звучит удобно. Но...

⚠️ Опасность:


namespace lib {
struct Widget {};
void process(const Widget&) {
std::cout << "lib::process\n";
}
}

void process(int) {
std::cout << "global::process\n";
}

void bar() {
lib::Widget w;
process(w); // вызовет lib::process, а не global::process!
}


Такой вызов может внезапно потянуть неожиданные функции, особенно в generic-коде (например, в шаблонах), где ADL может найти «лишнее».

Best practices:

* Не полагайся на ADL без крайней нужды.
* Используй ::qualified::names для ясности.
* В шаблонах — лучше явно передавать функции (через policy, traits, или параметры шаблона).

➡️ @cpp_geek
👍12😁2
Зачем std::move от std::string в emplace_back?

Кажется, emplace_back(std::move(str)) — избыточно: ведь emplace_back сам конструирует объект на месте. Но с std::string и другими перемещаемыми типами всё не так просто.

Пример:


std::vector<std::string> vec;
std::string str = "hello";

vec.emplace_back(str); // копия
vec.emplace_back(std::move(str)); // перемещение


Почему? Потому что:

> emplace_back вызывает конструктор std::string(const std::string&), если аргумент — const std::string&,
> и std::string(std::string&&), если аргумент — std::string&&.

А str — это lvalue. Даже внутри emplace_back. Без std::move, ты просишь вектор скопировать строку. С std::moveпереместить.

Это относится ко всем emplace_*, когда ты передаёшь существующий объект. emplace_back("hello") — другое дело: тут создаётся временный const char*, и он уже используется для конструктора string.

👉 Правило: если аргумент — перемещаемый объект (например, std::string, std::vector), и ты передаёшь его в emplace_*, не забывай std::move. Иначе будет лишняя копия.

➡️ @cpp_geek
👍5
Move-семантика: где можно ловко сэкономить

Многие знают про std::move, но не всегда используют его там, где это реально ускоряет код. Простой пример — возврат локального объекта из функции:


#include <string>

std::string make_string() {
std::string s = "Hello";
return s; // RVO или move
}


С C++17 тут почти всегда RVO (Return Value Optimization). Но если RVO невозможен (например, возвращаем тернарный оператор), компилятор применит move:


std::string make_string(bool flag) {
std::string a = "foo", b = "bar";
return flag ? a : b; // тут будет move
}


А вот так можно подсказать компилятору явно:


return std::move(flag ? a : b);


Но осторожно: не делайте std::move для локальной переменной в простом return — это может сломать RVO и привести к лишнему перемещению.

Ещё полезно помнить: move не всегда бесплатный. Например, для std::vector он копирует указатель и размер, но не элементы. Для std::string — зависит от Small String Optimization: короткие строки перемещаются как копия.

Вывод: используйте std::move там, где явно хотите отдать объект, а не копировать. Но не злоупотребляйте им: компилятор с C++17 сам неплохо справляется.

➡️ @cpp_geek
👍4
RAII — твой лучший друг (и почему не стоит бояться умных указателей)

Старый добрый new / delete — это классика, но и источник утечек, крашей и боли. В современном C++ ручное управление памятью почти всегда антипаттерн.

Решение — RAII (Resource Acquisition Is Initialization): ресурсы живут ровно столько, сколько объект, который ими владеет. Ушёл объект из области видимости — ресурс освободился.

Пример с умными указателями:


#include <memory>
#include <iostream>

struct Foo {
Foo() { std::cout << "Init\n"; }
~Foo() { std::cout << "Destroy\n"; }
};

void bar() {
std::unique_ptr<Foo> p = std::make_unique<Foo>(); // RAII
// делаем что-то
} // тут автоматически вызовется ~Foo()


Что важно знать:

* std::unique_ptr — владение в единственном числе, идеально для большинства случаев.
* std::shared_ptr — разделённое владение (но дороже по производительности).
* Никогда не делай new без обёртки — почти всегда лучше std::make_unique или std::make_shared.

RAII работает не только для памяти: файлы, мьютексы, сокеты — всё. Достаточно обернуть ресурс в класс с деструктором.

Профит: меньше багов, меньше утечек, чище код.

➡️ @cpp_geek
3👍3👌1
Почему std::vector<bool> — это ловушка

Если вы думаете, что std::vector<bool> — обычный вектор, только для bool, то… сюрприз! Это не совсем так.

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


std::vector<bool> v{true, false, true};
bool* p = &v[0]; // Ошибка! Тут нет обычного bool*


Элементы vector<bool> — это не настоящие bool, а прокси-объекты. Они ведут себя как bool, но фактически это обёртки над битами. Итог:

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

Что делать?
Если вам нужна коллекция булей — берите std::vector<char> или std::vector<uint8_t>. Памяти уйдёт чуть больше, но всё будет предсказуемо и быстро.

std::vector<bool> стоит использовать только если критична экономия памяти, и вы понимаете все его подводные камни.

➡️ @cpp_geek
👍122
Невидимый враг: std::vector<bool>

Кажется, что std::vector<bool> — обычный вектор из bool. Но на самом деле это не так.

Под капотом vector<bool> хранит биты плотно упакованными (1 бит на элемент), а не как bool (обычно 1 байт). Из-за этого:

- Элемент — это не bool&, а прокси-объект (std::vector<bool>::reference).
- Нельзя получить настоящий указатель на элемент: &v[0] не даст bool*.
- Код работает медленнее из-за лишней возни с битовыми масками.

Пример неожиданности:


std::vector<bool> v{true, false};
auto p = &v[0]; // Ошибка! Нет bool*


Хотите реально быстрый и предсказуемый контейнер?

- Используйте std::vector<char> или std::vector<uint8_t> для хранения флагов.
- Или подключите boost::dynamic_bitset, если нужна именно битовая упаковка.

Итог: vector<bool> — это костыль ради экономии памяти. Если важны скорость и нормальная семантика — обходите стороной.

➡️ @cpp_geek
👍8💩3
Зачем [[nodiscard]] и когда он реально спасает?

Сколько раз вы видели: функция возвращает значение, а кто-то его тупо игнорирует. А потом баги из ниоткуда. В C++17 завезли [[nodiscard]], и это реально полезная штука.


[[nodiscard]] int compute() {
return 42;
}

int main() {
compute(); // warning: ignoring return value
}


Компилятор предупреждает: "эй, ты вызвал, но не используешь результат".
Где это особенно важно:

- Функции, которые возвращают ошибки (std::error_code, std::optional, bool успеха операции);
- Функции, где пропуск результата ломает логику (например, RAII-объекты, токены отмены, хендлы).

Можно навесить [[nodiscard]] и на типы (C++20):


struct [[nodiscard]] Result {
bool ok;
};

Result foo();
foo(); // warning!


Вместо неочевидных багов — раннее предупреждение компилятора.

Лайфхак: Если компилятор слишком шумит — можно сделать [[nodiscard("объяснение")]] (C++20), чтобы явно подсказать, зачем это.

Вывод: ставьте [[nodiscard]] на всё, что нельзя безнаказанно проигнорировать. Это дешёвая страховка от глупых багов.

➡️ @cpp_geek
👍114
std::exchange — простой способ менять значения и возвращать старые

Вместо того чтобы писать руками:


auto old = value;
value = new_value;
return old;


В modern C++ есть готовый инструмент - std::exchange (C++14+).


#include <utility>
#include <string>
#include <iostream>

int main() {
std::string s = "Hello";
auto old = std::exchange(s, "World");

std::cout << "old = " << old << ", s = " << s << '\n';
}


Вывод:


old = Hello, s = World


Когда полезно:

- Реализация move-конструкторов/операторов:


MyType(MyType&& other)
: data_(std::exchange(other.data_, nullptr)) {}

- Сброс состояния объектов и возвращение старого значения.
- Реализация одноразовых флагов (`once_flag` паттерн).

Плюсы:

- Одна строка вместо трёх.
- Читаемость выше — сразу видно, что мы заменяем значение и берём старое.

Помни: по умолчанию второе значение копируется/перемещается, так что это не нулевой по стоимости вызов.

➡️ @cpp_geek
👍43
Как уменьшить время компиляции C++ проектов

Сегодня я хочу поговорить про боль, которую мы все знаем — долгую компиляцию больших C++ проектов.
Когда проект растёт, время сборки иногда становится просто катастрофическим. Вот несколько приёмов, которые реально помогают:

1. PCH (Precompiled Headers) — вынесите редко меняющийся код (например, <iostream>, <vector>, <string>) в precompiled header. Это может срезать время компиляции в разы.
2. Разделяйте код на интерфейсы и реализации — заголовки должны быть минимальными, только объявления. Всё, что можно, уносите в .cpp.
3. Используйте #pragma once вместо include guard — чуть быстрее и проще.
4. Минимизируйте include-цепочки — подключайте в заголовках только то, что реально нужно. Остальное — в .cpp.
5. Инкрементальная сборка — убедитесь, что сборочная система (CMake, Ninja, Make) не пересобирает лишнее.

Когда я внедрял эти подходы в одном проекте, время компиляции сократилось с 18 минут до 6. Это реально чувствуется.

➡️ @cpp_geek
🔥7👍2
std::move vs std::forward: когда и что использовать

Сегодня покажу вам коротко, чем отличаются std::move и std::forward.

- std::move(obj) — безусловно превращает объект в rvalue. После этого объект считается "пустым" для повторного использования (в рамках контракта move). Используем, когда мы точно хотим "забрать" ресурсы.

- std::forward<T>(obj) — условно делает rvalue, если изначально пришёл rvalue. То есть это "perfect forwarding" для шаблонных функций.

Пример:


#include <utility>
#include <string>
#include <iostream>

template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // сохраняет rvalue/lvalue-семантику
}

void process(const std::string& s) { std::cout << "Lvalue: " << s << '\n'; }
void process(std::string&& s) { std::cout << "Rvalue: " << s << '\n'; }

int main() {
std::string str = "Hello";
wrapper(str); // Lvalue
wrapper(std::move(str)); // Rvalue
}


Запомните:

- std::move - "забрать".
- std::forward - "передать как есть".

➡️ @cpp_geek
🔥5🥴4
🧵 Сегодня покажу вам, как удобно логгировать значения в отладке с помощью макроса, который реально упрощает жизнь.

Когда мы отлаживаем код, часто хочется быстро вывести значения переменных. Писать std::cout << "x: " << x << std::endl; каждый раз — боль. Давайте упростим:


#include <iostream>

#define LOG(var) std::cout << #var << " = " << (var) << std::endl;

int main() {
int x = 42;
double pi = 3.1415;

LOG(x); // x = 42
LOG(pi); // pi = 3.1415
}


Фишка в том, что #var превращает имя переменной в строку. А (var) - значение.

🔥 Такой макрос отлично заходит при написании алгоритмов, отладки функций, проверки значений — и при этом делает код аккуратнее.

Хочешь улучшить - можно сделать вывод в файл или добавить таймстемпы.

➡️ @cpp_geek
🔥12👍5🗿1
Media is too big
VIEW IN TELEGRAM
🚀 YADRO приглашает C++ разработчиков в команду OpenBMC и встроенных систем!

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

📌 Кого мы ищем:

• Ведущего разработчика C++ (Linux/OpenBMC)
• Ведущего разработчика интерфейсов встроенных систем
• TeamLead разработки OpenBMC

🧰 Технологический стек и задачи:

• C++ (стандарты 17, 20, 23), STL, Boost
• Linux-среда, systemd, D-Bus, Yocto, bash, Python
• Работа с ядром прошивки OpenBMC, взаимодействие с UEFI/BIOS
• Разработка и поддержка сложных интерфейсов встроенных систем

💼 Условия работы:

• Гибкий формат: удалённо или в офисах в Москве, Санкт-Петербурге, Екатеринбурге, Нижнем Новгороде и Минске
• Работа с масштабными проектами в уникальной команде инженеров
• Возможность горизонтального и вертикального карьерного роста

💙 Узнайте больше и откликайтесь на вакансии прямо на сайте!
Please open Telegram to view this post
VIEW IN TELEGRAM
🖕43😁1
Что такое виртуальный деструктор и зачем он используется в C++?

В C++ виртуальный деструктор используется для правильного освобождения памяти при удалении объекта через указатель на базовый класс. Если базовый класс имеет виртуальный деструктор, то при удалении объекта через указатель на базовый класс будет вызван деструктор не только базового класса, но и всех его производных классов. Это позволяет избежать утечек памяти и неопределенного поведения при работе с полиморфными объектами.

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

➡️ @cpp_geek
👍62🤯2
Изменяемые лямбда-функции

Ключевое слово mutable используется для сохранения состояния в лямбда-функциях. Обычно оператор вызова функции замыкания является константным. Другими словами — лямбда не может модифицировать переменные, захваченные по значению.

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

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

➡️ @cpp_geek
👍21😱1
Сейчас покажу вам простой, но мощный приём для ускорения компиляции - разделение объявления и реализации шаблонов с использованием explicit instantiation.

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

🔧 Что можно сделать?

Разделяем интерфейс и реализацию:


// MyTemplate.hpp
#pragma once

template<typename T>
class MyTemplate {
public:
void doSomething();
};



// MyTemplate.cpp
#include "MyTemplate.hpp"
#include <iostream>

template<typename T>
void MyTemplate<T>::doSomething() {
std::cout << "Doing something\n";
}

// Явная инстанциация
template class MyTemplate<int>;


Теперь в клиентском коде:


#include "MyTemplate.hpp"

int main() {
MyTemplate<int> obj;
obj.doSomething();
}


Это даст:

- Сокращение времени компиляции
- Чище зависимости
- Потенциально меньший размер бинарника

Но помните: explicit instantiation работает, только если вы заранее знаете типы, с которыми будете использовать шаблон.


➡️ @cpp_geek
👍8😱2
Генерируем пароли с помощью C++

Эта программа создает константы для допустимых символов в пароле и для длины пароля. Затем она инициализирует генератор случайных чисел и генерирует пять случайных паролей.

Каждый пароль формируется путем выбора случайного символа из всего набора символов. Этот процесс повторяется до достижения желаемой длины пароля.

➡️ @cpp_geek
👍9🤣1
memmove

Функция memmove используется для копирования блока памяти из одного места в другое. Она объявлена в заголовочном файле. Она принимает аргументы типа void * и const void *, что позволяет ей работать с любыми типами данных. Она просто копирует указанное количество байтов из исходного буфера в целевой.

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

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

➡️ @cpp_geek
❤‍🔥1👍1
Move-only объекты и почему std::unique_ptr нельзя копировать

Многие удивляются, когда компилятор ругается: «unique_ptr не имеет конструктора копирования». Это не баг, а фича: он move-only. Логика простая: владелец ресурса должен быть только один.


#include <memory>
#include <iostream>

int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);

// std::unique_ptr<int> p2 = p1; // ошибка копирования

std::unique_ptr<int> p2 = std::move(p1); // перенос
std::cout << *p2 << "\n"; // 42
std::cout << (p1 ? "not null" : "null") << "\n"; // null
}


Ключевой момент: после std::move старый указатель «обнуляется», чтобы избежать двойного освобождения памяти.

Если нужен shareable ресурс – используйте std::shared_ptr. Но помните: это дороже (счётчик ссылок, атомики).

Частая ошибка на собеседованиях: «почему нельзя вернуть unique_ptr по значению?» - на самом деле можно! Он спокойно двигается:


std::unique_ptr<int> make_ptr() {
return std::make_unique<int>(99); // move происходит неявно
}


👉 Правило: если объект владеет чем-то уникально – делайте его move-only (удалите копирование). Это повышает безопасность кода и явно выражает семантику владения.

➡️ @cpp_geek
👍3😁3