

- Главная
- Каталог
- Интернет технологии
- Грокаем C++
Грокаем C++
Авторский канал о программировании на С++ и базе computer science. Простым и легким слогом рассказываем про сложные концепции С++. Самая активная и вовлеченная аудитория в тематике.
Статистика канала
Полная статистикаchevron_right
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
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
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::accumutate:
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
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
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
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
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Отзывы канала
всего 7 отзывов
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
Грокаем C++ — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 9.4K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 23.9, количество отзывов – 7, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 8391.6 ₽, а за 29 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий