Библиотека C/C++ разработчика | cpp, boost, qt
19.1K subscribers
2.1K photos
67 videos
16 files
4.43K links
Все самое полезное для плюсовика и сишника в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/d6cd2932

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a5bac324c8ba6dcaa1ad17

#WXSSA
Download Telegram
🍴 Задача на выходные: до скольки считает стандарт?

Стандарт C++ гарантирует, что можно зарегистрировать минимум N функций через std::atexit. Найди баг в этом коде:

#include <cstdlib>
#include <iostream>

void dummy_handler() {
static int counter = 0;
std::cout << "Handler " << ++counter << "\n";
}

int main() {
// Регистрируем 100 обработчиков
for (int i = 0; i < 100; ++i) {
if (std::atexit(dummy_handler) != 0) {
std::cerr << "Failed at " << i << "\n";
return 1;
}
}

std::cout << "All registered!\n";
return 0;
}


Вопрос: Гарантирует ли стандарт, что все 100 обработчиков зарегистрируются? Какое минимальное количество гарантируется? Как правильно обработать ошибку регистрации?

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#междусобойчик
Please open Telegram to view this post
VIEW IN TELEGRAM
😁1
🔧 Guidelines Support Library: как Microsoft проверяет параметры

GSL — это не просто модная библиотека. Это набор практик от создателей C++, упакованный в код😎

#include <gsl/gsl>

// Традиционная проверка
void setAge(Person& p, int age) {
if (age < 0 || age > 150) {
throw std::invalid_argument("Invalid age");
}
p.age = age;
}

// GSL: семантика ясна из кода
void setAge(Person& p, int age) {
Expects(age >= 0 && age <= 150); // Precondition
p.age = age;
Ensures(p.age == age); // Postcondition
}


❗️Что даёт GSL:

Expects() — preconditions (проверка входа)
Ensures() — postconditions (проверка выхода)
gsl::not_null<T*> — указатель, который не может быть null

void process(gsl::not_null<int*> data) {
// Компилятор гарантирует: data != nullptr
*data = 42; // Безопасно без if
}


‼️ В debug режиме — runtime проверки, в release — могут быть отключены.

👉 Библиотека

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🙏2
🔧 std::enable_if_t для проверки иерархии классов

Нужно убедиться, что класс является наследником другого? Проверяйте это на этапе компиляции с помощью type traits.

#include <type_traits>
#include <memory>

class Base {
public:
virtual ~Base() = default;
};

class Derived : public Base {};
class Unrelated {};

// Фабрика, работающая только с наследниками Base
template<typename T>
std::enable_if_t<std::is_base_of_v<Base, T> && !std::is_same_v<Base, T>,
std::unique_ptr<T>>
createObject() {
return std::make_unique<T>();
}

int main() {
auto obj1 = createObject<Derived>(); // OK
// auto obj2 = createObject<Unrelated>(); // Compile error
// auto obj3 = createObject<Base>(); // Compile error
}


Контроль иерархии классов на этапе компиляции
💰 Предотвращение неправильного использования API
⚡️Статическая проверка без runtime-затрат

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#междусобойчик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
📌 Хранение float в std::map: правильный способ

std::map требует strict weak ordering. float нарушает его при наличии NaN. std::strong_order спасает.

#include <compare>
#include <map>
#include <iostream>

struct FloatCompare {
bool operator()(double a, double b) const {
// strong_order — total order, строго соответствует требованиям map
return std::strong_order(a, b) == std::strong_ordering::less;
}
};

int main() {
std::map<double, std::string, FloatCompare> m;

m[1.0] = "one";
m[-0.0] = "negative zero";
m[+0.0] = "positive zero"; // отдельный ключ! (strong_order различает)
m[std::numeric_limits<double>::quiet_NaN()] = "nan";
m[std::numeric_limits<double>::infinity()] = "inf";

// Все 5 ключей уникальны и упорядочены детерминированно
std::cout << m.size(); // 5
}


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
👍14
🎯 std::move_only_function: зачем нам ещё один тип функций?

std::function — хороший инструмент. Но есть одна проблема: он требует копируемости. А что, если твой callable некопируемый? В C++23 есть решение — std::move_only_function.

Представь: ты хочешь передать лямбду, которая захватывает std::unique_ptr. С std::function — UB или не скомпилируется. С std::move_only_function — просто работает.

// C++23
#include <functional>
#include <memory>

auto make_task(std::unique_ptr<int> data) {
// Работает! std::function здесь не справится
return std::move_only_function<void()>{
[d = std::move(data)]() {
std::println("Value: {}", *d);
}
};
}


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
👍71👾1
💡 __has_include() — это не проверка заголовка, а запрос к препроцессору

Если думал, что __has_include(<vector>) проверяет «существует ли файл на диске». Это не так. Директива работает на уровне препроцессора и взаимодействует с поисковыми путями компилятора, а не с файловой системой напрямую.

🔍 Как это работает

__has_include — это расширение препроцессора, стандартизированное в C++17. Когда компилятор встречает:

#if __has_include(<optional>)
# include <optional>
#endif


— препроцессор проходит по своим include-путям (-I, системные пути, -isystem) и проверяет, разрешится ли имя файла в один из них. Это тот же механизм, что используется при обычном #include, но без реальной вставки содержимого.

🏝 Два синтаксиса — два алгоритма поиска

__has_include(<header>)   // поиск только в системных путях
__has_include("header") // поиск сначала в локальных, затем в системных


Это зеркалит поведение обычных #include <> и #include "". Разница критична при наличии локальных заголовков с теми же именами, что и системные.

❗️ Ловушка: наличие != доступность

Файл может быть найден препроцессором, но при этом не компилироваться на данной платформе. Например, <windows.h> физически присутствует в MinGW, но использование некоторых его частей невозможно без нужного таргета. __has_include вернёт 1, но код всё равно сломается.

‼️ Практический вывод

Используй __has_include для определения наличия необязательных зависимостей, но всегда дополняй проверкой версии или feature-теста (__cpp_lib_optional). Это защитит от ситуации «файл есть, фича недоступна».


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#под_капотом
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤‍🔥41
🧩 Выходной челлендж: дочисти парсер команд

У тебя есть заготовка интерактивной оболочки — read-eval-print loop. Осталось дописать несколько ключевых частей.

#include <iostream>
#include <sstream>
#include <vector>
#include <string>

std::vector<std::string> parseCommand(const std::string& line) {
// TODO: разбить строку на токены по пробелам
// Учти: несколько пробелов подряд — не ошибка
}

void execute(const std::vector<std::string>& tokens) {
if (tokens.empty()) return;

if (tokens[0] == "echo") {
// TODO: вывести все аргументы через пробел
} else if (tokens[0] == "exit") {
exit(0);
} else {
// TODO: вывести "Unknown command: <имя команды>"
}
}

int main() {
std::string line;
while (true) {
std::cout << "> ";
if (!std::getline(std::cin, line)) break;
execute(parseCommand(line));
}
}


Задача: заполни три TODO.

💬 Покажи своё решение — особенно интересны варианты с std::istringstream и ручным разбором.


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчи
ка

#междусобойчик
😁31
👩‍💻 #include хотят переименовать в #please

Комитет ISO пришёл к выводу, что вежливое обращение к компилятору снижает количество ошибок компиляции на 12%.

#please <iostream>
#please <vector>


❗️ Компилятор тоже заслуживает уважения.

Попались? С первым апреля! 😁


📍Навигация:
ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#развлекалово
Please open Telegram to view this post
VIEW IN TELEGRAM
😁39❤‍🔥2😢21
🥳 C++ Ranges. Часть 1: Основы

«Если ты думаешь, что циклы — это нормально, ты просто ещё не видел Ranges»
— Каждый разработчик C++20, примерно через неделю после знакомства с библиотекой


🌸 Введение

Добро пожаловать в мир C++ Ranges — одного из самых мощных нововведений стандарта C++20.

Если ты когда-нибудь писал вот такой код:

std::vector<int> result;
for (const auto& x : data) {
if (x % 2 == 0) {
result.push_back(x * x);
}
}


..то ты знаешь, как это может быть многословно. Ranges позволяют написать то же самое так:

auto result = data
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });


Красиво? Ещё и эффективнее! Давай разберёмся, как это работает.


🍴 Что нам понадобится

Для работы со всеми примерами нужен компилятор с поддержкой C++20 (и лучше C++23):

#include <ranges> // std::views, std::ranges::*
#include <algorithm> // std::ranges::sort и другие


1️⃣ Что такое Range?

Range (диапазон) — это любой тип, у которого есть начало ( begin ) и конец ( end ). Всё, что ты
привык итерировать в цикле for , уже является диапазоном.

std::vector<int> v = {1, 2, 3, 4, 5}; // ✓ Range
std::string s = "hello"; // ✓ Range
int arr[] = {1, 2, 3}; // ✓ Range
std::list<double> l = {1.1, 2.2}; // ✓ Range


С технической точки зрения, тип R является диапазоном, если для него определены
std::ranges::begin(r) и std::ranges::end(r) . В C++ это выражается через концепт:

template<typename R>
concept range = requires(R& r) {
std::ranges::begin(r); // требуем наличие begin
std::ranges::end(r); // требуем наличие end
};



🍪 Категории диапазонов

input_range — однопроходный обход (Пример: поток ввода)
forward_range — многопроходный обход (Пример: std::forward_list)
bidirectional_range — обход в обе стороны (Пример: std::list)
random_access_range — доступ за O(1) по индексу (Пример: std::deque)
contiguous_range — данные в непрерывной памяти (Пример: std::vector , массив)


🐸 Продолжение следует... Дальше разберём, какие алгоритмы можно использовать с Ranges.

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
11👍9🙏2
🍿 C++ Ranges. Views и алгоритмы

Продолжаем тему Ranges в C++.

В библиотеке Ranges есть два главных инструмента работы с последовательностями:

1. std::ranges::* алгоритмы — переработанная версия классической библиотеки <algorithm>. Принимают не пару итераторов, а целый диапазон, поддерживают проекции и лучше взаимодействуют с современными типами.

2. std::views::* адаптеры — ленивые обёртки над диапазонами. Не копируют данные, а описывают как их обойти. Удобны для цепочек преобразований.


🐤 Старый стиль vs новый стиль

std::vector<int> v = {5, 3, 1, 4, 2};

// Старый стиль: передаём пару итераторов
std::sort(v.begin(), v.end());

// Новый стиль: передаём весь диапазон
std::ranges::sort(v);


❗️Разница кажется небольшой, но новый стиль:

• Синтаксически короче
• Поддерживают проекции — сортировку по произвольному полю объекта
• Работают с любым range-совместимым типом, не только с контейнерами


Пример 1. Cортировка

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
std::vector<std::string> names = {"Boris", "Anton", "Victor", "Anna"};

std::ranges::sort(names);

for (const auto& name : names) {
std::cout << name << "\n";
}
// Anna, Anton, Boris, Victor
}



Пример 2. Проекции — сортировка по полю структуры

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

#include <algorithm>
#include <vector>
#include <iostream>
#include <string>

struct Person {
std::string name;
int age;
};

int main() {
std::vector<Person> people = {
{"Boris", 30},
{"Anna", 25},
{"Victor", 35}
};

// Сортируем по возрасту, передав &Person::age как проекцию
std::ranges::sort(people, {}, &Person::age);

for (const auto& p : people) {
std::cout << p.name << " " << p.age << "\n";
}
// Anna 25, Boris 30, Victor 35
}



Пример 3. Views — ленивые цепочки

std::views не трогают исходный контейнер и не создают копий — они описывают трансформацию, которая применяется при обходе:

#include <ranges>
#include <vector>
#include <iostream>

int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};

// Берём только чётные, умножаем на 10, берём первые 3
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 10; })
| std::views::take(3);

for (int x : result) {
std::cout << x << "\n";
}
// 20, 40, 60
}


❗️Исходный вектор v не изменился. Весь пайплайн вычисляется лениво — только когда мы итерируемся в цикле.


🐸 Продолжение следует... В следующий раз разбираем pipelines

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
👍141❤‍🔥1
🩹 C++ Ranges. Конвейеры: оператор |

То, ради чего стоит использовать Ranges — это конвейеры (pipelines). Оператор | позволяет обрабатывать данные через цепочку преобразований:

🥲 Как это выглядело до C++20:

std::vector<int> temp, result;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(temp),
[](int n) { return n % 2 == 0; });
std::transform(temp.begin(), temp.end(), std::back_inserter(result),
[](int n) { return n * n; });


❗️ Вложенные алгоритмы, временные буферы, много шума.


🙂 С конвейером (C++20):

#include <ranges>
#include <vector>
#include <iostream>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Конвейер: фильтруем чётные → возводим в квадрат → берём первые 3
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);

for (int x : result) {
std::cout << x << " "; // 4 16 36
}
std::cout << "\n";
}


❗️ result — это не вектор, а «вид» (view). Вычисления происходят лениво — только в момент итерации в цикле for. Исходный вектор numbers остаётся нетронутым.

Нужен именно вектор? Материализуйте результат:

// C++23
auto vec = result | std::ranges::to<std::vector>();

// C++20
std::vector<int> vec(result.begin(), result.end());



🐸 Продолжение следует...


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥64👍1
🔥 Найди баг: копирование строки сломало указатель

#include <string>
#include <iostream>

struct Token {
std::string value;
const char* ptr;

Token(const std::string& s) : value(s), ptr(value.data()) {}
};

int main() {
Token t1("hi");
Token t2 = t1; // копируем

std::cout << t1.ptr << "\n"; // "hi"
std::cout << t2.ptr << "\n"; // ???
}


‼️ Задача: найди баг (если он есть), объясни, почему он связан с SSO, и предложи исправление.

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#междусобойчик
Please open Telegram to view this post
VIEW IN TELEGRAM
😁21👍1
🍙 C++ Ranges. Ленивые вычисления (C++20)
Cамое важное свойство адаптеров views: они ленивые (lazy).


Когда ты пишешь data | views::filter(...) | views::transform(...), ты не запускаешь обработку. Ты создаёшь лёгкий view-объект, который хранит ссылку на исходный диапазон и предикаты. Обработка происходит только тогда, когда ты начинаешь итерировать результат — поэлементно, по требованию.

⚠️ Важно: view хранит ссылку на исходный контейнер. Если контейнер изменится или выйдет из области видимости раньше, чем ты начнёшь итерацию — поведение будет неопределённым.


🌸 Пример 1: базовая ленивость
// C++20
#include <vector>
#include <ranges>
#include <iostream>

int main() {
std::vector<int> v = {1, 2, 3, 4, 5};

// Создаём view — никакой обработки ещё нет
auto view = v | std::views::filter([](int x) {
std::cout << "Проверяем " << x << "\n";
return x > 2;
});

std::cout << "Начинаем итерацию:\n";
for (int x : view) {
std::cout << "Результат: " << x << "\n";
}
}


❗️Вывод — фильтрация и результат чередуются поэлементно. Это и есть ленивость в действии:

Начинаем итерацию:
Проверяем 1
Проверяем 2
Проверяем 3
Результат: 3
Проверяем 4
Результат: 4
Проверяем 5
Результат: 5


Сравни с тем, как выглядел бы «жадный» (eager) подход: сначала все "Проверяем...", потом все "Результат:". Здесь же — один элемент за раз.

🌱 Пример 2: цепочка адаптеров

Можно выстраивать цепочки — каждый адаптер обрабатывает элемент по мере продвижения итератора:

#include <vector>
#include <ranges>
#include <iostream>

int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

auto view = v
| std::views::filter([](int x) { return x % 2 == 0; }) // только чётные
| std::views::transform([](int x) { return x * x; }); // возводим в квадрат

for (int x : view) {
std::cout << x << " "; // 4 16 36 64 100
}
}


Здесь transform никогда не видит нечётных чисел — filter не пропускает их дальше. Никаких промежуточных векторов, никаких лишних аллокаций.


🌳 Пример 3: ранняя остановка

Ленивость особенно выгодна, когда тебе не нужны все элементы. views::take берёт ровно столько, сколько нужно, и останавливает обработку:

#include <vector>
#include <ranges>
#include <iostream>

int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Берём первые 3 чётных числа — и всё, остальные даже не проверяются
auto view = v
| std::views::filter([](int x) {
std::cout << "Проверяем " << x << "\n";
return x % 2 == 0;
})
| std::views::take(3);

for (int x : view) {
std::cout << "Берём: " << x << "\n";
}
}


Вывод:
Проверяем 1
Проверяем 2
Берём: 2
Проверяем 3
Проверяем 4
Берём: 4
Проверяем 5
Проверяем 6
Берём: 6
Проверяем 7 // Могут быть проверены (зависит от компилятора)
Проверяем 8 // хотя это не обязательно


После шестёрки обработка полностью прекращается. Числа 7–10 не тронуты — хотя они есть в векторе.


💡 Итоги

Ленивость — это не просто деталь реализации. Это архитектурное свойство, которое позволяет:

• работать с бесконечными диапазонами (std::views::iota)
• не создавать промежуточные контейнеры при цепочках
• останавливать обработку раньше, как только результат получен

Плата за это — view не кэширует результаты. Если ты итерируешь view дважды, предикаты выполнятся дважды. Если это проблема — преврати результат в вектор через std::ranges::to<std::vector>() (C++23) или просто std::vector(view.begin(), view.end()).


🐸 Продолжение следует...


📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍32
🌳 std::call_once и once_flag: явная ленивая инициализация

Иногда нужно инициализировать ресурс ровно один раз, но логика инициализации сложнее, чем в конструкторе. Вот инструмент для этого.

#include <mutex>
#include <memory>

class ResourceManager {
std::once_flag init_flag_;
std::unique_ptr<HeavyResource> resource_;

public:
// once_flag нельзя копировать — явно запрещаем копирование класса
ResourceManager() = default;
ResourceManager(const ResourceManager&) = delete;
ResourceManager& operator=(const ResourceManager&) = delete;

HeavyResource& get_resource() {
std::call_once(init_flag_, [this] {
// Выполнится ровно один раз, даже при гонке потоков.
// Если инициализация бросит исключение — флаг не выставится,
// и следующий поток повторит попытку.
resource_ = std::make_unique<HeavyResource>(load_config());
});
return *resource_;
}
};

// Использование — просто и безопасно
ResourceManager mgr;
auto& r1 = mgr.get_resource(); // инициализирует
auto& r2 = mgr.get_resource(); // возвращает готовый


💡 Когда call_once лучше Meyers Singleton (static local-переменной):

• Инициализация — отдельный этап от конструктора объекта
• Нужно явно контролировать момент инициализации, а не привязывать его к первому обращению к static-переменной
• Логика инициализации требует захвата внешнего состояния (this, параметры)

📊 Оверхед call_once после первого вызова — минимален, но не нулевой: как минимум одна атомарная проверка с барьером памяти. Стоит замерить бенчмарком.

📍Навигация: ВакансииЗадачиСобесы

Библиотека C/C++ разработчика

#константная_правильность
Please open Telegram to view this post
VIEW IN TELEGRAM
👍91