Грокаем C++
9.37K subscribers
49 photos
1 video
3 files
565 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Что не так с модулями?
#опытным

Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.

Модули - это новая фундаментальная единица организации кода, которая должна дополнить и в идеале(в мечтах комитета) заменить старую концепцию заголовочных файлов.

Если по простому, то модуль - это такой бинарный черный ящик, у которого четко определен интерфейс, который он экспортирует наружу.

Экспортируемые сущности явно помечаются в коде модуля. Затем модуль компилируется и из бинарного его представления можно дергать только эти экспортируемые сущности.

Короткий пример:

// math.cppm - файл модуля
export module math; // Объявление модуля

import <vector>; // Импорт, а не включение

// Макросы НЕ экспортируются!
#define PI 3.14159

// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);

// Внутренние функции скрыты
void internal_helper();


и его использование:

// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода

// Используем экспортированную функцию
double area = calculate_circle_area(10);

// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются


Модули призваны решать следующие проблемы:

Одни и те же заголовки могут сотни раз обрабатываться компилятором при компиляции программ из многих единиц трансляции. Модули же компилируются один раз, в них кэшируется информация, необходимая для нормальной компиляции cpp файлов и потом эта информация просто используется при компиляции. Никакой повторной работы!
Это значит, что время компиляции должно заметно уменьшиться.

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

Никакой макросятины! Ни один макрос не прошмыгнет внутрь клиентского кода из модуля, потому что он уже скомпилирован.

На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...

А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?

😡 Модули - довольно сложная штука в реализации. Не будем вдаваться в нюансы, но компилятор должен сильно измененить свое поведение и преобрести свойства системы сборки, чтобы нормально компилировать модули. А делать они этого не хотят. Плюс многие компиляторы опенсорсные и не так-то просто в опенсорсе реализовывать такие масштабные идеи. На винде с этим попроще, потому что во главе всего Microsoft и они завезли модули раньше всех.

😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.

😡 Из-за этого в том числе хромает тулинг. Дело в том, что модуль - это бинарный файл и программист просто так не может, например, посмотреть сигнатуру метода в каком-то файле. Это большая проблема, которую должны решить редакторы и анализаторы кода. Но отсутствие стандартизации формата мешает интеграции модулей в них.

😡 Очень много усилий нужно потратить на переработку архитектуры и кода существующих проектов, чтобы перевести их на модули.

😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.

😡 Нужны новейшие версии систем сборки, компиляторов и других инструментов, чтобы заработали модули.

😡 Пока популярные библиотеки не начнут распространяться через модули, существующие проекты не будут иметь большое желание переезжать на модули, потому что получится частичное внедрение.

Тем не менее, если у вас есть самые актуальные инструменты, вы запускаете новый проект или решили в тестовом режиме обновлять уже существующий, то пользоваться модулями уже можно, хоть и осторожно и с ожиданием возможных проблем.

Use new features. Stay cool.

#cppcore #compiler #tools
16👍7🔥6
Вы используете модули С++20 в проде? Какой у вас компилятор?
Anonymous Poll
76%
Не использую
15%
Использую. GCC
10%
Использую. Clang
7%
Использую. MSVC
😁9🔥43👎1
Output параметры
#новичкам

Если у вас нет std::variant, std::expected, std::optional, вам лень засовывать ошибки в объекты, то у вас остается не так много вариантов. Один из них - output параметры функции.

Идея донельзя простая. Не хочешь использовать сложные типы? Добавь дополнительный параметр функции!

Но конфигурация возвращаемого значения и параметров функции может быть разная:

👉🏿 С-style. Возвращаем int, который каким-то образом кодирует ошибку + потенциально какую-то полезную информацию, а результат работы функции записывается в выходной параметр. Например системный вызов read:

ssize_t read(size_t count, int fd, void buf[count], size_t count);


Возвращает количество прочитанных байт, либо -1, если произошла ошибка. Конкретная ошибка передается через errno. Сами данные записываются в buf.

Так как это не С++ подход, то он содержит все недостатки отсутствия ООП.

👉🏿 Код результата 1. Возвращаем enum успешности операции, а сам результат возвращаем в одном или нескольких выходных параметрах.

enum Code {
Success,
DivisionByZero,
NegativeNumber,
Overflow
};

Code safe_sqrt(double x, double& result) {
if (x < 0) {
return Code::NegativeNumber;
}
result = std::sqrt(x);
return Code::Success;
}

double result = 0.;
auto code = safe_sqrt(-4.0, result);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}


В этом случае приходится объявлять переменную результата заранее.

👉🏿 Код результата 2. Наоборот, возвращаем результат, а в выходные параметры передаем потенциальную ошибку:

double safe_sqrt(double x, Code& code) {
if (x < 0) {
code = Code::NegativeNumber;
return {};
}
code = Code::Success;
return std::sqrt(x);
}

Code code = Code::Success;

auto result = safe_sqrt(-4.0, code);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}


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

Этот подход используется в том числе в стандартной библиотеке:

bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;


Теперь уже заранее надо код результата инициализировать.

👉🏿 Универсальный. Можно ввести и универсальный кодстайл: возвращаем bool в качестве индикатора успешности выполнения функции, а результаты и ошибки возвращаем в output параметрах.
bool parse_coordinates(const std::string& input, 
double& x, double& y, double& z,
std::string& error_message);
double x, y, z;
std::string error_msg;

if (parse_coordinates("10.5,20.3,30.7", x, y, z, error_msg)) {
std::cout << "Coordinates: " << x << ", " << y << ", " << z << std::endl;
} else {
std::cout << "Error: " << error_msg << std::endl;
}


Здесь сигнатура явно форсит проверить результат(особенно с nodiscard). Но еще больше данных нужно объявлять до вызова функции и код становится все менее декларативным.

Наверное когда-то это был популярный подход к обработке ошибок. Но в современном С++ существуют хорошие альтернативы, которые сделают ваш код проще, понятнее и консистентнее.

Prevent misuse. Stay cool.

#design
4👍1813🔥7
Обработка ошибок Шердингера
#опытным

Мы уже поговорили о том, что есть 2 подхода к обработке ошибок - исключения и возврат кода ошибки(std::expected или output параметры).

И хоть стандартная библиотека насквозь пропитана исключениями, она все-таки иногда, очень редко предоставляет альтернативные варианты. Например std::from_chars или std::to_chars.

Интересно, что в библиотеке std::filesystem очень многие функции и методы имеют две перегрузки работы с данными: одна с исключениями, другая - без. Например:

bool exists( const std::filesystem::path& p );
bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;

// or

bool remove( const std::filesystem::path& p );
bool remove( const std::filesystem::path& p, std::error_code& ec ) noexcept;


std::filesystem завезли в стандарт относительно поздно, поэтому было время задуматься о людях, пишущих небросающий код.

Однако выше приведены "образцово показательные" перегрузки. Посмотрите вот на это:

directory_iterator& operator++();
directory_iterator& increment( std::error_code& ec );


Есть класс std::filesystem::directory_iterator и эти итераторы нужно уметь инкрементировать, чтобы двигаться по элементам директории. Так как сигнатура операторов в С++ не поддерживает лишние параметры, то для варианта с кодом ошибок приходится определять именованный метод.

Обратите внимание, что increment не объявлен как noexcept!

То есть используя increment, вы не можете гарантировать отсутствие исключений. Да, ошибки при работе с файловой системой ОС передаются в качестве кодов ошибок. Но тот же std::bad_alloc increment кинуть может.

По всей видимости, мотивация не выбрасывать исключения связана с тем, что вызывающие стороны, использующие версию с исключениями, часто замусорены локальными блоками try/catch для обработки «рутинных» событий. Условно: при работе с файлами может оказаться, что у программы нет прав доступа для них. Это в целом нормальная ситуация в файловой системе, но в первой перегрузке эти ситуации репортятся через исключения, как исключительные ситуации.

Дизайн странный и путает людей. Поэтому будьте аккуратны с std::filesystem, если реально хотите убрать исключения с глаз долой.

Don't be confused. Stay cool.

#cpp17
👍1710🔥6😁2
Всегда приятно, когда громко рассказывают об успехах отечественных технологий. Я к этому хоть отношения никакого не имею, но все равно на душе как-то приятно становится.

И двигают ее вперед в первую очередь энтузиасты, которым по кайфу работать в той области, где еще очень неуверено ступала нога массового пользователя, бизнеса и тд.

Сегодня хочется рассказать об одном необычном для русского айти событии. Программист-любитель просто взял и организовал соревнование по алгоритмическому программированию на C/C++ под «Эльбрусы» (e2k). Соревы собрали 31 студентов со всей России в онлайн-формате, призовым фондом были личные 215 тысяч рублей организатора.

Какие были задачи, как происходила подготовка инфраструктуры совернований, откуда взялись эти бравые студенты - обо всем этом вы можете почитать в статье организатора на Хабре. Там расписаны буквально все подробности от подготовки до юридичыеских тонкостей.

Обязательно зайдите и поддержите автора.

А еще в марте следующего года будет еще одно такое мероприятие. Так что если вы студент и знакомы с Эльбрусами, вас будут там ждать)
16👍14🔥7👎3🗿1
​​Уплощаем многомерный массив
#опытным

Иногда у вас есть коллекция элементов, для каждого из которых вы выполняете операцию, возвращающую вектор значений:

std::vector<int> Process(const std::string& str);

std::vector<std::string> elems = ...;

auto result_view = elems | std::views::transform([](const std::string& str) {
return Process(str);
})


Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?

С помощью С++20 отображения std::views::join:

std::vector<int> Process(const std::string& str);

std::vector<std::string> elems = ...;

auto result = elems | std::views::transform([](const std::string &str) {
return Process(str);
}) |
std::views::join | std::ranges::to<std::vector>();

std::print("{}", result);


Это все сработает и на экране появлятся заветные чиселки.

Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте

Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:

auto result = elems | std::views::transform([](const auto & elem) {
return Process(elem);
}) |
std::views::join | std::views::as_rvalue |
std::ranges::to<std::vector>();


Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.

Don't stuck in a loop. Stay cool.

#cpp20 #cpp23
19👍13🔥7
Константная мапа
#новичкам

Определяете вы какое-нибудь отображение в коде:

using CommandCreator = std::function<std::unique_ptr<ICommand>(const std::vector<std::string>&)>;
const std::unordered_map<std::string, CommandCreator> commands = {
{"create", [](const std::vector<std::string>& vec){return std::make_unique<CreateCommand>(vec)}},
{"delete", [](const std::vector<std::string>& vec){return std::make_unique<DeleteCommand>(vec)}},
{"save", [](const std::vector<std::string>& vec){return std::make_unique<SaveCommand>(vec)}}
};


Никаких больше команд вы не обрабатываете, отображение константно.

И вот вы хотите получить доступ к элементам мапы:

auto command = commands[command_name](vec);
command->Execute();


И тут бац! И ошибка компиляции о том, что нет такого оператора[], который бы принимал константную std::unordered_map.

Почему так? У вектора же есть.

Проблема тут комплексная.

Здесь мы рассказали о том, что operator[] у мапы имеет одну особенность. Если вы ему передаете новый ключ, то он изменяет мапу и вставляет в нее элемент с этим новым ключом и дефолтным значением.

Это сделано по всей видимости по причине универсализации поведения между операторами[] для большинства контейнеров. Обычно operator[] не бросает никаких исключений. Он может приводить к неопределенному поведению, как например у вектора при доступе за границу массива. Но он не бросает.

И в случае мапы не очень понятно, что делать, если переданного ключа нет, бросать нельзя и хочется сохранить идентичность интерфейса по всему STL с возвратом ссылки.

Вот и решили конструировать объект налету.

Но для константного оператора вообще непонятно, что делать, если ключа нет, бросать нельзя, нужно возвращать ссылку, да еще и изменять мапу нельзя. И UB тоже не хочется, чем меньше его в стандарте, тем лучше.

Поэтому решили проблему гениально: вообще не вводить эту версию оператора.

В условиях отсутствия константного operator[] для std::map и std::unordered_map вы можете использовать либо метод at(), который бросает std::out_of_range, если ключа нет. Или find(), который вернет итератор на конец мапы:

auto command = commands.at(command_name)(vec);
command->Execute();
// or
if (auto it = commands.find(command_name); it != commands.emd()) {
auto command = it->second();
command->Execute();
} else {
std::cout << "ERROR" << std::endl;
}


Find compromis. Stay cool.

#STL
20👍14🔥8😱1
​​pointer to data member
#опытным

В этом посте мы рассказывали о том, что с помощью ranges и и параметра проекции можно кастомизировать алгоритмы с соответствии с определенным полем класса. Например, чтобы найти в коллекции элемент с максимальным определенным полем, то можно сделать так:

struct Payment {
double amount;
std::string category;
};
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);


max в этом случае будет транзакцией с максимальным размером платежа.

В последней строчке используется &Payment::amount - указатель на поле amount в классе Payment. Но если это параметр функции, то это значение какого-то типа. Но какой тип у этого указателя?

Если про указатели на конкретные мемберы знают не только лишь все, то это совсем дебри плюсов.

Явный тип указателя на поле класса используется так:

struct Payment {
double amount;
std::string category;
};

double Payment::*ptr = &Payment::amount; // Here!

Payment payment{3.14, "Groceries"};
std::cout << payment.*ptr << std::endl;
// OUTPUT:
// 3.14


double Payment::*       ptr         = &Payment::amount;
// тип указателя имя указателя инициализатор


По сути это особый тип указателя, который хранит смещение поля относительно начала объекта в байтах. Это не специфицировано в стандарте, но примерно везде так работает.

Мы обязательно должны указать, на какой тип полей этот указатель может указывать. Таким образом указатель ptr может указывать на любое поле класса Payment, имеющее тип double. То есть:

struct Type {
int a;
int b;
float c;
};

int Type::*p = nullptr;

p = &Type::a; // OK, a is int
p = &Type::b; // OK, b is int

p = &Type::c; // ERROR! c is float


Указателю на интовое поле нельзя присвоить указатель на флотовое. И наоборот, указатель p работает с любыми полями типа int.

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

// Takes pointer to any data member for any type
template<typename T, typename FieldType>
void print_field(const T& obj, FieldType T::*field) {
std::cout << obj.*field << std::endl;
}

Payment payment{3.14, "Groceries"};
Type t(42, 69, 3.14);

print_field(payment, &Payment::amount);
print_field(payment, &Payment::category);

print_field(t, &Type::a);
print_field(t, &Type::b);
print_field(t, &Type::c);
// OUTPUT
// 3.14
// Groceries
// 42
// 69
// 3.14


print_field может печатать значение любого поля любого класса по его указателю. Обратите внимание на шаблонную сигнатуру.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
322🔥14👍11
​​WAT
#опытным

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Всегда ли nullptr указатель равен нулю?

Казалось бы в названии дан ответ:

int * p = nullptr;
std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
// OUTPUT
// true
// 0


Но в общем случае это неправда! Смотрим на пример:

struct A {
int i;
};

int main() {
int A::* p = 0;

std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
std::cout << std::boolalpha << (std::bit_cast<std::uintptr_t>(p) == 0xffffffffffffffff) << "\n";
}
// OUTPUT:
// true
// ffffffffffffffff
// true


nullptr указатель равен совсем не нулю, как декларировалось в начале main.

WAT? Что за фокусы с пропажей нуля?

Во вчерашнем посте мы рассказывали об особом типе указателя - pointer to data member. Этот указатель, которым и является p из примера, по сути хранит информацию о том, как в объекте найти нужное поле класса.

И в большинстве случаев эта информация представляет собой просто смещение поля относительно начала объекта в байтах.

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

Вместо этого обычно используется число -1, которое в байтовом представлении как раз выглядит как все единички:

std::cout << std::hex << static_cast<long long int>(-1) << "\n";
// OUTPUT:
// ffffffffffffffff


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

struct Type {
double a;
char b;
float c;
long long d;
short e;
unsigned f;
};

std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::a) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::b) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::c) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::d) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::e) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::f) << "\n";

// OUTPUT:
// 0
// 8
// 12
// 16
// 24
// 28


Опять же, интересный уголок плюсов.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
21👍12🔥8🤯4❤‍🔥2
​​Сколько весит объект полиморфного класса?
#новичкам

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

У нас уже был пост про размер объекта пустого класса.

А что если это будет класс с виртуальными методами? Сколько тогда будет весить этот класс?

struct SomeClass {

virtual ~SomeClass() = default;

virtual void Process() {
std::cout << "Process" << std::endl;
}
};


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

Таблица одна на все объекты заданного класса. И им каким-то образом нужно получить доступ к этой таблице.

Учитывайте, что нельзя захардкодить эту информацию по статическому типу объекта(например SomeClass& или SomeClass*), потому что под его личиной может скрываться наследник.

Значит надо ее класть в каждый объект. И самое простое - положить в них указатель на свою таблицу виртуальных функций. Так и делают на самом деле. Этот указатель называют vptr.

Соответственно размер класса зависит от битности системы. Для 64-бит указатель имеет размер 8 байт(64 бита) поэтому и размер класса SomeClass будет 8 байт.

std::cout << sizeof(SomeClass) << std::endl;
// OUTPUT
// 8


Пустые наследники SomeClass кстати тоже будут иметь размер 8 из-за того, что им нужно лишь другое значения указателя.

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

Be lightweight. Stay cool.

#cppcore #interview
120👍15🔥7😁7🆒1
join
#опытным

Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:

my_list = ["John", "Peter", "Vicky"]
x = " ".join(my_list)
print(x)
# OUTPUT
# John Peter Vicky


И как же сложно того же результата достичь в плюсах!

То делают через потоки:

std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";

std::ostringstream oss;
oss << vec[0];

for (size_t i = 1; i < vec.size(); ++i) {
oss << delimiter << vec[i];
}

return oss.str();
}


то через std::accumulate:

std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";

return std::accumulate(
std::next(vec.begin()), vec.end(),
vec[0],
[&delimiter](const std::string& a, const std::string& b) {
return a + delimiter + b;
}
);
}


Ну вы что! Стандартная строка же себе не может позволить иметь метод join, принимающий коллекцию строк и возвращающий объединенную строку с разделителями. Это же не универсально и никому не надо...

Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:

std::string join(const std::vector<std::string> &vec,
const std::string &delimiter) {
return vec | std::views::join_with(delimiter) |
std::ranges::to<std::string>();
}


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

И жизнь стала чуть-чуть счастливее...

Make thing simple. Stay cool.

#cpp23
24👍10🔥9😁5
​​Как динамически выделить память на стеке?
#опытным

В книжке "Вредные советы для С++ программистов" от PVS-студии есть такой вредный совет: "массив на стеке - это лучшее решение"

Типа выделение памяти в куче - это зло. char c[256] хватит всем, а если не хватит, то потом поменяем на 512. В крайнем случае – на 1024.

Да, использование буферов, фиксированного размера действительно может привести в проблемам в коде. Запилили новую фичу, изменили размер данных, а забыли поменять размер массива. Пожалуйста, UB.

Но возникает вопрос: а как тогда можно динамически выделять память на стеке? Ведь стандартные C-style массивы работают только с известными в compile-time размерами.

Пара способов есть, но они с "нюансом":

1️⃣ Variable Length Array. Вы просто берете и создаете массив переменного размера:

void foo(size_t n) {
float array[n];
// ....
}


Круто!

Да, но это не часть стандарта С++) Это фича языка С, доступная с С99. Однако GCC например поддерживает ее, как компиляторное расширение языка и вы сможете g++'ом сгенерировать код выше.

2️⃣ alloca. Функция, которая аллоцирует заданное количество байт на стеке:

void *alloca(size_t size);

void foo(size_t n) {
float array = (float)alloca(sizeof(float) * n);
}


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

Эта также нестандартная функция, которая не является даже частью ANSI-C стандарта, но поддерживается многими компиляторами(в т.ч. является частью Linux API).

Ну и на этом все.

Стандартных решений нет. И на это есть причина. Дело в том, что такой код провоцирует возникновение в программах уязвимостей.

В стандарте MISRA C есть правило MISRA-C-18.8, указывающее не использовать VLA. Другие руководства, такие как SEI CERT C Coding Standard (ARR32-C) и Common Weakness Enumeration (CWE-129), не запрещают использовать эти массивы, но призывают проверять перед созданием их размер.

Не учли, что могут прийти неожиданные данные, выделили охренелион байт и получили переполнение стека. Это вариант DoS-атаки уровня приложения.

На счет alloca вообще есть интересные интересности. Представьте себе, что будет если компилятор попытается встроить код этой функции:

void DoSomething() {
char pStr = alloca(100);
//......
}


в вызывающий код:

void Process() {
for (i = 0; i < 1000000; i++) {
DoSomething();
}
}


Так как память, выделенная alloca освобождается только после завершения функции, а не выходе из скоупа, то получает взрыв размера стека.

И теперь представьте лицо программиста, который написал этот код с учетом вызова alloca именно во фрейме функции DoSomething.

Манипуляции со стеком - не очень безопасно, поэтому ничего такого и не вводят в плюсы.

Be safe. Stay cool.

#NONSTANDARD #goodoldc
👍2213🔥8
​​Двойной unlock
#опытным

Если не пользоваться RAII, то можно наткнуться на массу проблем. Все знают про double free. Но менее известна проблема double unlock.

Все просто, вы используете ручной lock-unlock мьютекса и возможно попадаете в ситуацию двойного освобождения:

void unsafe_function(int value) {
mtx.lock();

if (value < 0) {
std::cout << "Error: negative value\n";
mtx.unlock();
// forget to return!
}

shared_data = value;
std::cout << "Data has updated: " << shared_data << std::endl;

mtx.unlock(); // second unlock
}


Практически всегда двойной unlock происходит из-за некорректного кода в той или иной степени. Забыть вызвать return кажется детской проблемой, но если вы например не написали тесты на эту ветку, то возможно вы наткнетесь на проблемы только в проде.

А проблемы могут быть примерно любыми. Потому что двойной unlock мьютекса - UB по стандарту. Соответственно, можете получить много непрятностей, от сегфолта до бесконечного ожидания.

Поэтому просто используйте RAII и спина болеть не будет:

void safe_function(int value) {
std::lock_guard lg{mtx};

if (value < 0) {
std::cout << "Error: negative value\n";
return;
}

shared_data = value;
std::cout << "Data has updated: " << shared_data << std::endl;
}


Use safe technics. Stay cool.

#concurrency #cpp11
👍2114🔥7😁3
Этот митап уже становится доброй традицией: ведь это — Сплошные Плюсы и никаких минусов ❤️

ЦУ и PVS-Studio приглашают плюсистов на четвёртую встречу клуба С++ разработчиков.

«Сплошные плюсы» это прежде всего интересное общение в дружеской обстановке и атмосфера комьюнити. А ещё вас ждут приятные бонусы, увлекательные квизы и эксклюзивный мерч. 🔥

Темы докладов:
🔵 Неопределённое поведение
Если про него не думаете, это не значит, что его нет C++ — опасный инструмент, и не помешает лишний раз напомнить, как правильно держать его в руках. Причём с приходом инструментов вайб-кодинга ситуация, скорее всего, даже ухудшится, так как станет ещё сложнее удерживать неопределённое поведение под контролем. Обсудим эту тему, заглянув в будущее.

🔵 Российские операционные системы реального времени семейства Багет
В докладе будет представлена уникальная отечественная разработка, созданная в НИЦ «Курчатовский институт» — НИИСИ, — ОСРВ семейства Багет (2.x, 3. x, 4. x), поддерживающие жесткие требования реального времени, современные стандарты, включая POSIX и ARINC 653, и работающие на российских процессорах собственной разработки.

🗓 Когда: 25 ноября в 18:00
📍Где: Москва, кампус ЦУ, ул. Гашека, д.7 стр. 1.

Подробности и регистрация по ссылке 🔗
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥114👍4🤯1
​​enum class
#новичкам

Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы

В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:

enum class Enumeration {CATEGORY1, CATEGORY2, CATEGORY3};


Он решает две большие проблемы обычных перечислений:

👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.

Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:

cpp
enum Color { RED, GREEN, BLUE };

Color c = RED;
Color next = c + 1; // Implicit conversion to int and visa versa!


Что значит прибавить красному цвету единицу - решительно непонятно.

Неявные преобразования enum class'ов же запрещено:

enum class Color { RED, GREEN, BLUE };

Color c = Color::RED;
Color next = c + 1; // ERROR!


Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:

Color c = Color::RED;
Color next = static_cast<Color>(static_cast<int>(c) + 1);


👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
enum Color { RED, GREEN, BLUE };

enum TrafficLight { RED, YELLOW, GREEN }; // ERROR!

void graphics_library() {
Color c = RED;
}


У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:

enum class Color1 { RED, GREEN, BLUE };
enum class Color2 { RED, GREEN, BLUE };

void graphics_library() {
Color1 c1 = Color1::RED;
Color2 c2 = Color2::RED;
}


И все прекрасно компилируется.

С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.

Protect your scope. Stay cool.

#cppcore #cpp11
👍3215🔥14
​​enum struct
#опытным

В прошлом посте мы рассказали про enum class. И в 99.999% случаев эту сущность будут писать в коде именно, как enum class.

Но можно написать enum struct и это тоже будет работать!

enum class FileMode { Read, Write, Append };
enum struct LogLevel { Debug, Info, Warning, Error };

int main() {
FileMode mode = FileMode::Read;
LogLevel level = LogLevel::Info;
std::cout << (mode == FileMode::Read) << std::endl;
std::cout << (level == LogLevel::Info) << std::endl;
}


Немного кто знает вообще о существовании такой конструкции. Свойства enum struct абсолютно аналогичны enum class и структуры перечислений были введены просто для консистентности и поддержания паритета в возможности использования двух ключевых слов.

Вот такой короткий и бесполезный факт из мира плюсов)

Be useful. Stay cool.

#fun #cpp11
2👍28😁218🔥6🤣4