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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Capture this
#новичкам

Бывают ситуации, когда вы хотите зарегистрировать коллбэк, в котором будет выполняться метод текущего класса.

Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку graceful shutdown. Для этого получаем объект обработчика сигнала и регистрируем коллбэк на SIGINT и SIGTERM:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [](){
Shutdown();
});
}

void Shutdown() {...}
};


Сработает ли такой код?

Он не соберется. Будет ошибка: 'this' was not captured for this lambda function. Методу Shutdown нужен объект, на котором его нужно вызвать.

Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [this](){
// this->Shutdown(); - don't need this syntax
Shutdown();
});
}

void Shutdown() {...}
};


До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:

SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [= /*& also works*/](){ // works
Shutdown();
});


Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.

Be explicit. Stay cool.

#cppcore #cpp11 #cpp20
👍225❤‍🔥4🔥2👏1
Почему еще важен std::forward
#опытным

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

Также спасибо ему за предоставление следующего примера:

#include <iostream>

void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }

void foo(auto&& v) { bar(v); }

int main() {
foo(1);
foo(2.0f);
}


Как думаете, что выведется на консоль? Подумайте пару секунд.

Ну нормальный человек ответит:

int 1
float 2


Однако командная строка вам выдаст следующее:

float 1
int 2


Если не верите, по посмотрите в годболте. И можете уже сейчас написать в комментах: "ЧЗХ", "WTF", "WAT" и прочее.

А нам пораразбирацца.

Тут используется auto в аргументах функции, значит эта функция неявно шаблонная. Посмотрим, что нам выдаст cppinsights по этому коду:

#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int>(int && v)
{
bar(static_cast<float>(v));
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<float>(float && v)
{
bar(static_cast<int>(v));
}
#endif


Просто прекрасно. Какого черта компилятор кастит переменные к противоположным типам?

Первое, что важно понимать: внутри функции foo переменная v - это уже lvalue, так как имеет имя. Значит просто так вызвать перегрузки для правых ссылок он не может.

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

То есть происходит следующее: компилятор понимает, что подходящей перегрузки нет, поэтому начинает применять стандартные преобразования в другие типы. Любой каст дает временный объект. А временный объект типа int легко биндится к float&&, как и временный объект float легко биндится к int&&.

Вот и получается обмен вызовами.

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

#include <iostream>

void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }

void foo(auto&& v) { bar(std::forward<decltype(v)>(v)); }

int main() {
foo(1);
foo(2.0f);
}


В этом случае вывод будет ожидаемым.

Be amazed. Stay cool.

#cppcore #cpp11 #template
🔥46🤯298👍7❤‍🔥2
Just throw it forward
#опытным

Работа с исключениями - целое искусство. В этой теме куча фишек, которые помогут извлекать из исключений максимум пользы.

Пусть вы хотите работать с какой-то сущностью в коде эксклюзивно:

void func(std::shared_ptr<Resource> res) {
if (res->TryToAcquire()) {
// do dirty things
res->Release();
} else {
// you don't have permission to do dirty, so return
return;
}
}


Что будет, если в этом коде в результате обработки эксклюзивно захваченного ресурса произойдет исключение?

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

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

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

Автоматически освободить ресурс нам поможет RAII обертка, которая освободит ресурс в деструкторе, если его получилось захватить, а залогировать ошибку поможет совместное использование catch + простой вызов "throw".

void func(std::shared_ptr<Resource> res) {
auto raii_res = RaiiWrapper(res);
if (raii_res->TryToAcquire()) {
try {
// do dirty things
} catch (const std::exception& ex) {
Log("Houston, we have a problem");
throw;
}
} else {
// you don't have permission to do dirty, so return
return;
}
}


Инструкция throw делает единственную вещь - пробрасывает то же самое исключение выше по стеку вызовов.

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

Don't leak your resources. Stay cool.

#cppcore
19👍17🔥8
​​std::clamp
#новичкам

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

И чтобы не городить подобных конструкций:

if (value < min) {
value = min;
}
if (value > max) {
value = max;
}


есть замечательная стандартная функция std::clamp. Она полностью инкапсулирует логику ограничения значения сверху и снизу:

template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );


Теперь ограничивать значения переменным проще, чем написать Hello, World.

double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}


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

Вряд ли вы заходите в такие глубины стандарта и cppreference, чтобы std::clamp попалась вам на глаза. Поэтому и решил здесь рассказать про нее.

Don't reinvent the wheel. Stay cool.

#STL
646👍21🔥18
​​Как определять константы локальных функций
#новичкам

Когда мы определяем константу в скоупе функции у нас есть несколько вариантов, как это сделать: пометить const, constexpr, const static, constexpr static. Какой вариант выбрать?

Идентификатор constexpr говорит о том, что значение переменной обязательно должно быть известно во время компиляции. Просто const перед локальной переменной такого не требует, это обычная константа времени выполнения. Ну а static в этом контекcте продлевает время жизни переменной до конца работы программы.

Особенности функционала диктуют кейсы применения.

1️⃣ Если константа зависит от входных данных функции, то ее нужно помечать, как const. constexpr здесь по определению не подойдет, не-consteval функции могут выполняться в рантайме, а в consteval функциях входные параметры не считаются за constexpr. static тоже, потому что один раз определив статическую константу, вы ее не сможете поменять.

std::pair<double, double> normalizeVector(double x, double y) {
const double LENGTH = std::sqrt(x * x + y * y); // Зависит от x и y
return {x / LENGTH, y / LENGTH};
}


2️⃣ Если константа не зависит от входных данных функции и она известна во время компиляции, то лучше ее определить, как constexpr static. Тогда такая локальная переменная гарантированно не будет занимать пространство на стеке при каждом вызове функции, ее инициализация точно пройдет в compile-time и об этом будет явно сказано в коде программы.

double metersToMiles(double meters) {
constexpr static double METERS_IN_MILE = 1609.34;
return meters / METERS_IN_MILE;
}


Вообще этот пост в принципе родился из вопроса: "как помечать локальные константы, значения которых известны на этапе компиляции?". В большинстве случаев можно пометить константу просто constexpr, компилятор скорее всего ее оптимизирует и вставит ее значение в места вызовов, но это не гарантируется. Например, если вы по каким-то причинам будете использовать адрес этой локальной константы, то компилятор будет вынужден класть ее на стек на каждом вызове.

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

Be expressive. Stay cool
20👍14🔥6😁4
Ревью
#новичкам

Сегодня у нас #ревью довольно простого кода, который показывает простые механики С++. Однако не расслабляйтесь! Это мир С++, а он не такой уж солнечный и приветливый даже в самых простых случаях.

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

Сегодняшний лот:
#include <cstring>
#include <iostream>

class Data {
public:
char* buffer;

Data(const char* input) {
buffer = new char[strlen(input)];
std::strcpy(buffer, input);
}

~Data() {
delete buffer;
}
};

bool compareData(Data data1, Data data2) {
return data1.buffer == data2.buffer;
}

int main() {
Data d("Hello");
bool result = compareData(d, d);

std::cout << "Result: " << result << std::endl;
}


Продуктивной прожарки всем!

Critique your solutions. Stay cool.
1029🔥15👍10😱2
Разбор ревью
#новичкам

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

Как всегда было сложно выбрать один комментарий, у каждого свое видение итогового решения и разные предложения. Решил отметить двух подписчиков: @Ivaneo(коммент), как наиболее продуктивного по количеству замечаний, и @monah_tuk(коммент), за более нативный формат. Давайте похлопаем им 👏👏👏.

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

Обычно при ревью мы пытаемся максимально сохранить идею кода вымышленного автора при обсуждении решении проблем. А то можно было бы сказать "надо использовать std::vector и мозг себе не блендерить". Так не интересно)

Поехали!

🔞 Создаем массив через new[], а удаляем через delete. Это UB, нужно использовать одинаковые операторы для создания и удаления объектов и массивов.

🔞 Автор кода хочет, чтобы его класс обладал всеми специальными методами, и при этом надеется, что компилятор сможет сгенерировать их так, как надо автору. Надеяться не надо, потому что компилятор довольно тупой и выполняет поверхностное копирование и перемещение в самосгенеренных методах. Он просто копирует значение указателя в другой объект. В compareData аргументы передаются по значению, значит будет копирование. Значит после выхода из скоупа функции будет double free при разрушении второго объекта. Надо писать свои специальные методы по правилу пяти. До кучи можно использовать swap idiom.

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

🔞 Автор был невнимательным и забыл единичку при копировании входных данных. Таким образом в данные не попадает null-terminator, а значит не представляется возможным по одному указателю определить размер данных.

Тут вопрос на самом деле намного шире: а что мы вообще может принимать, в качестве аргументов конструктора? Туда спокойно можно передать указатель на обычный char массив, у которого нет null-terminator'а. Более того, в середине такого массива может быть валидный нулевой символ, просто как часть общего массива.

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

Плюс, если мы допускаем нулевой символ в середине, то функции типа std::str* нам не подходят, потому что ориентируются на терминатор. Лучше использовать std::copy.

Если уж очень хочется передавать строковые литералы и не передавать для них размер можно использовать такой трюк:

template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}


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

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

🔞 Использование сырых указателей попахивает С-style'ом в плюсовом коде. У нас давно появились умные указатели и если уж так хочется пользоваться чаровскими буферами(ударение не на 3-й слог!) то можно использовать std::unique_ptr<char[]>. Тогда реализация всех специальных методов становится проще, а деструктор - вообще тривиальным.
7👏6👍5🔥3
🔞 Если принимаете сырой указатель, как параметр - не забудьте проверить его на nullptr. Меньше будете голову ломать при будущем дебаге.

🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.

🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.

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

🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.

🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.

Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.

Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.

Фух, на этом вроде все.

Вот что вышло по итогу исправлений:

class Data final {
private:
std::unique_ptr<char[]> buffer;
size_t buf_size = 0;

public:
Data(const char* input, size_t size)
: buf_size(size) {
if (input == nullptr && size != 0) {
throw std::invalid_argument("Input cannot be null for non-zero size");
}

if (size > 0) {
buffer = std::make_unique<char[]>(size);
std::copy(input, input + size, buffer.get());
}
}

template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}

// Копирующий конструктор
Data(const Data& other)
: Data{other.buffer.get(), other.buf_size} {
}

// Универсальное присваивание
Data& operator=(Data other) {
swap(*this, other);
return *this;
}

// Перемещающий конструктор
Data(Data&& other) noexcept {
swap(*this, other);
}

~Data() = default;

// Оператор сравнения
bool operator==(const Data& rhs) const noexcept {
return std::string_view{buffer.get(), buf_size} == std:;string_view{rhs.buffer.get(), buf_size()};
}

// Вспомогательная функция для обмена
friend void swap(Data& first, Data& second) noexcept {
using std::swap;
swap(first.buffer, second.buffer);
swap(first.buf_size, second.buf_size);
}
};

[[nodiscard]] bool compareData(const Data& data1, const Data& data2) noexcept {
return data1 == data2;
}


Пишите, если что забыл.

Critique your solution. Stay cool.

#cppcore #OOP #goodpractice #design
34🔥10👍8❤‍🔥1👎1
Почему важно проверять входные данные
#новичкам

Вы делаете какие-то вычисления и среди них есть целочисленное деление:

void fun(int a, int b) {
// some calculations
auto res = a / b;
// some calculations
}


Вы уже наслышаны о том, что в С++ есть механизм обработки исключений и просто оборачиваете вашу функцию в try-catch и думаете, что С++ позаботится о том, что вы будете информированы о всех проблемах, которые могут произойти в коде:

try {
fun(1, 2);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


Однако помните, что вы пишите на С++. Здесь уровень заботы примерно, как у бати, который вывозит неумеющего плавать сына на середину реки и бросает его в воду с криками "Плыви, сынок!".

Если вдруг вы передадите в fun вторым параметром ноль:

try {
fun(1, 0);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


то С++ не пошлет исключение об этой ситуации.

Стандарт явно говорит: целочисленное деление на ноль приводит к неопределенному поведению. У меня например программа просто падает с надписью: Floating point exception. Очень иронично со стороны компилятора обрабатывать эту ситуацию, выводя в консоль текст о появлении исключения, хотя его тут нет и его никак не отловить.

Поэтому нужно проверять входные данные и самим явно кидать исключения или использовать другой способ обработки ошибки:

void fun(int a, int b) {
if (!b) {
throw std::runtime_error("Devision by zero!");
}
// some calculations
auto res = a / b;
// some calculations
}

try {
fun(1, 0);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


Тогда на консоль явно выведется ожидаемое Calculation error: Devision by zero!.

Так что будьте осторожны и проверяйте входные данные.

Stay alert. Stay cool.

#cppcore
👍3212🔥11❤‍🔥4🤯3
​​std::to_address
#опытным

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

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

Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение: obj.operator->(). Однако для простых указателей это не прокатит: они не классы и у них нет методов. Да и не прокатит для любых других объектов, у которых не определен этот оператор. Что делать?

Использовать C++20 функцию std::to_address! Вот ее примерная реализация:

template<class T>
constexpr T* to_address(T* p) noexcept {
static_assert(!std::is_function_v<T>);
return p;
}

template<class T>
constexpr auto to_address(const T& p) noexcept {
if constexpr (requires{ std::pointer_traits<T>::to_address(p); })
return std::pointer_traits<T>::to_address(p);
else
return std::to_address(p.operator->());
}


То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().

Обычно эта функция требуется для вызова сишного апи в обобщенном коде:

void c_api_func(const int*);

template<typename T>
void call_c_api_func(T && obj) {
c_api_func(std::to_address(obj));
}

std::vector<int> data{10, 20, 30};
call_c_api_func(data.begin()); // works
auto ptr = std::make_unique<int>(42);
call_c_api_func(ptr); // works
call_c_api_func(ptr.get()); // also works


Use the right tool. Stay cool.

#cpp20 #cppcore
223👍9🔥7😁3
Квиз

Сегодня потенциально самый необычный #quiz на канале. Воистину, C/С++ - это самые интересные языки из существующих. Столько безобразия можно с ними натворить. С одной стороны, это мешает языкам выходить в мейнстрим "патамушта небезопасна". С другой стороны, здесь максимальная свобода творчества и полета фантазии.

У меня для вас всего один вопрос: Что произойдет в результате попытки компиляции и запуска этого С-кода:

const int main[] = {
-98693133, -443987883, 440, 113408,
-1922629632, 4149, 1227264, 84869120,
15544, 266023168, 1970231557, 1701994784,
1701344288, 1936024096, -1878384268, 258392925
};


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

Сhoose with your heart. Stay cool.
19🤣3🤯21
Ответ
#опытным

Ну что. Пора раскрывать карты.

Правильных ответов здесь несколько(не зря был квиз с множественным ответом) и все зависит от версии компилятора и опций компиляции. Интереснее всего смотреть, что получится при использовании например gcc9 без опций.

А получится вот что. Программа успешно скомпилируется и на консоли появится сообщение:

You are the best!


Да, да. Это она про вас, наши подписчики.

Дело вот в чем. Есть такой варнинг в gcc: warning: 'main' is usually a function [-Wmain].
Погодите, main "обычно" является функцией. Это что, может быть не так?

В С нет или не было прям жесткого требования на тип символа main. Он в целом может быть и массивом:
char main[10];

хоть указателем на функцию:
void (main)();


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

Ну а дальше дело техники. Пишем прокси программу:

void main() {
__asm__ (
// print You are the best!
"movl $1, %eax;\n" /* 1 is the syscall number for write */
"movl $1, %ebx;\n" /* 1 is stdout and is the first argument */
// "movl $message, %esi;\n" /* load the address of string into the second argument*/
// instead use this to load the address of the string
// as 16 bytes from the current instruction
"leal 16(%eip), %esi;\n"
"movl $18, %edx;\n" /* third argument is the length of the string to print*/
"syscall;\n"
// call exit (so it doesn't try to run the string Hello World
// maybe I could have just used ret instead
"movl $60,%eax;\n"
"xorl %ebx,%ebx; \n"
"syscall;\n"
// Store the You are the best! inside the main function
"message: .ascii \"You are the best!\\n\";"
);
}


Компилируем ее и запускаем gdb для дизассемблирования main:
(gdb) disass main
Dump of assembler code for function main:
0x0000000000001129 <+0>: endbr64
0x000000000000112d <+4>: push %rbp
0x000000000000112e <+5>: mov %rsp,%rbp
0x0000000000001131 <+8>: mov $0x1,%eax
0x0000000000001136 <+13>: mov $0x1,%ebx
0x000000000000113b <+18>: lea 0x10(%eip),%esi # 0x1152 <main+41>
0x0000000000001142 <+25>: mov $0x12,%edx
0x0000000000001147 <+30>: syscall
0x0000000000001149 <+32>: mov $0x3c,%eax
0x000000000000114e <+37>: xor %ebx,%ebx
0x0000000000001150 <+39>: syscall
0x0000000000001152 <+41>: pop %rcx
0x0000000000001153 <+42>: outsl %ds:(%rsi),(%dx)
0x0000000000001154 <+43>: jne 0x1176 <__libc_csu_init+6>
...


Ну и пожалуйста. Слева четко видим шестнадцатиричные числа, которые и представляют собой тело функции main.

Единственное, что осталось - вывести эти числа в десятиричной форме по 4 байта, как инты:

(gdb) x/16dw main
0x1129 <main>: -98693133 -443987883 440 113408
0x1139 <main+16>: -1922629632 4149 1227264 84869120
0x1149 <main+32>: 15544 266023168 1970231557 1701994784
0x1159 <main+48>: 1701344288 1936024096 -1878384268 258392925


Делаем из этих чиселок массив и готово! Закодированная программа будет выполняться.

В более новых версиях компиляторов эту лавочку прикрыли, потому что на gcc10 и более такая прога сегфолтится.

Но в любом случае, очень прикольно, что есть такая возможность.

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

Благодарю @PyXiion за предоставление материалов для этого поста.

Be amazed. Stay cool.

#fun #OS
❤‍🔥53🤯18🔥118👍6
Обогащаем исключение
#новичкам

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

Вот пилишь проект на 100500 строк кода. Запускаешь и вылетает ошибка: "абракадабра, ты хреновый программист". И как понять, откуда это исключение прилетело?

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

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

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

void ComplicatedCalculations() try {
// use db
} catch (std::exception& ex) {
throw std::runtime_error(std::string("ComplicatedCalculations Error:") + ex.what());
}

void HandlingCalculations() try {
ComplicatedCalculations();
} catch (std::exception& ex) {
throw std::runtime_error(std::string("HandlingCalculations Error:") + ex.what());
}


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

Track your problems to the bottom. Stay cool.

#cppcore
30👍10🔥5
​​Засовываем исключение в исключение
#опытным

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

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

void ComplicatedCalculations() try {
// use db
} catch (std::exception& ex) {
std::throw_with_nested(std::runtime_error("Complicated Calculations Error"));
}

void HandlingCalculations() try {
ComplicatedCalculations();
} catch (std::exception& ex) {
std::throw_with_nested(std::runtime_error("Handling Calculations Error"));
}


Теперь исключение, которое вылетит из HandlingCalculations будет на самом деле содержать 3 исключения: от базы данных, от ComplicatedCalculations и от HandlingCalculations.

Вложенные исключения существуют с С++11 и очень интересно устроены. Рассмотрим несколько упрощенные версии сущностей, которые находятся под капотом механизма вложенных исключений. Есть класс std::nested_exception:

class nested_exception
{
exception_ptr _M_ptr;
public:
/// The default constructor stores the current exception (if any).
nested_exception() noexcept : _M_ptr(current_exception()) { }
...
};


Этот класс ответственен за захват текущего исключения с помощью вызова std::current_exception().

Дальше имеется класс, который хранит в себе все множество исключений:

template<typename Except>
struct Nested_exception : public Except, public nested_exception
{
explicit Nested_exception(const Except& ex)
: Except(ex) { }
};


Объекты этого класса наследуются от nested_exception, в котором захвачено старое исключение, и от Except - нового исключения.

Ну и последний компонент - std::throw_with_nested:

template<typename Tp>
[[noreturn]]
inline void throw_with_nested(Tp&& t)
{
throw Nested_exception<remove_cvref_t<Tp>>{std::forward<Tp>(t)};
}


При вызове throw_with_nested создается объект Nested_exception на основе переданного типа исключения и, неявно, nested_exception, которых сохраняет в себе указатель на старое исключение.

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

Очень прикольная техника, которая позволяет строить цепочки объектов. Это как тупл, только расширяемый в рантайме.

Это все хорошо и интересно. Прокидывать вложенные исключения мы научились. Но рано или поздно их придется обработать. Как это сделать? Об этом будем говорить в следующем посте.

Inherit knowledge from your ancestor. Stay cool.

#cppcore #cpp11
🔥168👍5
Раскрываем вложенное исключение
#опытным

Каша заварилась, пришло время расхлебывать. В прошлом посте мы успешно завернули кучу исключений в одно, теперь нужно как-то все обратно развернуть. Для этого существует функция std::rethrow_if_nested. Работает она вот так:

class nested_exception
{
exception_ptr _M_ptr;
public:
/// The default constructor stores the current exception (if any).
nested_exception() noexcept : _M_ptr(current_exception()) { }

[[noreturn]] void rethrow_nested() const {
if (_M_ptr)
// just throw _M_ptr
rethrow_exception(_M_ptr);
std::terminate();
}
...
};

template<class E>
void rethrow_if_nested(const E& e) {
if (auto p = dynamic_cast<const std::nested_exception*>(std::addressof(e)))
p->rethrow_nested();
}


Так как объект вложенного исключения - это наследник std::nested_exception, то мы имеем право динамически привести указатель переданного в rethrow_if_nested исключения к std::nested_exception. Если каст прошел успешно, то бросается исключение, сохраненное в nested_exception. Если каст провалился, значит E - это уже не наследник nested_exception и у нас в руках самое первое исключение в цепочке.

Обычно в статьях по этой теме приводят мягко говоря странные примеры того, как надо обрабатывать вложенные исключения:

int main() {
try {
Login("test@example.com", "secret");
} catch (SecurityError &e) {
std::cout << "Caught a SecurityError";
try {
std::rethrow_if_nested(e);
} catch (AuthenticationError &e) {
std::cout << "\nNested AuthenticationError: " << e.Email;
}
}
std::cout << "\nProgram recovered";
}


Встречайте вложенные try-catch с заранее известной глубиной вложенности. Это конечно никуда не годится.

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

Рекурсия решает проблему неопределенного количества уровней вложенности исключений и помогает удобно подряд печатать соответствующие сообщения об ошибках:

void print_exception(const std::exception &e, int level = 0) {
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception &nestedException) {
print_exception(nestedException, level + 1);
} catch (...) {
}
}

void ComplicatedCalculations() try {
// use db
} catch (std::exception &ex) {
std::throw_with_nested(std::runtime_error("Complicated Calculations Error"));
}

void HandlingCalculations() try { ComplicatedCalculations(); } catch (std::exception &ex) {
std::throw_with_nested(std::runtime_error("Handling Calculations Error"));
}

int main() {
try {
HandlingCalculations();
} catch (const std::exception &e) {
print_exception(e);
}
}
// OUTPUT:
// Handling Calculations Error
// Complicated Calculations Error
// Some DB Error


В этом случае на добавление дополнительной информации к исключению вы тратите всего 1 строчку, которая мало чем отличается от возбуждения нового исключения. Плюс в коде логики проекта вы одной строчкой обрабатываете все вложенные исключения. Красота!

Очень нишевая функциональность, которую вы вряд ли встретите в реальных проектах. Однако будет прикольно попробовать ее в своих пет-проектах, если вы не гнушаетесь исключений.

Have a good error handling system. Stay cool.

#cppcore #cpp11
2🔥1211👍8🤣2
​​Volatile
#опытным

Ключевое слово, которое не embedded С++ разработчик вряд ли когда-нибудь встречал в код. Сегодня мы поговорим, для чего оно используется.

Предположим, что у нас есть переменная keyboard_press, память под которую замаплена на память устройства ввода-вывода. Когда нажимается кнопка клавиатуры, изменяется переменная keyboard_press. Оставим сам маппинг за скобками и попробуем написать какую-то детсадовскую логику с переменной keyboard_press:

int keyboard_press = 0;
size_t count_test = 0;

void some_function() {
while(keyboard_press == 0) {
count_test++;
}
// doing stuff
}


Что в ассемблере?

some_function():
mov eax, DWORD PTR keyboard_press[rip]
test eax, eax
jne .L1
.L3: // это кстати пустой бесконечный цикл, куда нельзя попасть и откуда нельзя выбраться
jmp .L3
.L1:
ret
count_test:
.zero 8
keyboard_press:
.zero 4


А где цикл? А где инкремент count_test?


На самом деле код собран с -О3 и компилятор просто выкинул цикл. Он не видит, что в данном коде где-то еще изменяется keyboard_press, поэтому разумно полагает, что мы написали бесконечный цикл без сайдэффектов, который вообще-то ub.

Но keyboard_press может изменяться, просто это никак не понятно по коду программы.

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

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

volatile int keyboard_press = 0;
size_t count_test = 0;

// same


Теперь ассемблер выглядит так:

some_function():
mov eax, DWORD PTR keyboard_press[rip]
test eax, eax
jne .L1
mov rax, QWORD PTR count_test[rip]
add rax, 1
.L3:
mov edx, DWORD PTR keyboard_press[rip]
mov rcx, rax
add rax, 1
test edx, edx
je .L3
mov QWORD PTR count_test[rip], rcx
.L1:
ret



Все, что делает volatile - все операции над переменной становятся видимыми спецэффектами и не могут быть оптимизированы компилятором. Ну и еще операции над volitile переменными не могут переупорядочиваться с другими видимыми спецэффектами в порядке кода программы.

Говорится ли здесь что-нибудь о потоках? Нет! Здесь говорится только об оптимизациях компилятора.

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

Доступ к volatile переменным не атомарный + с их помощью нельзя делать синхронизацию неатомарных переменных между потоками, так как volitile не подразумевает барьеров памяти.

Именно из-за этих ограничений volatile используется в очень узком спектре задач работы с I/O. Во всех остальных случаях в С++ используются атомики.

Don't be optimized out. Stay cool.

#cppcore #multitasking #memory
2❤‍🔥19👍188🔥4
​​Отличия volatile от std::atomic
#опытным

Кратко пробежимся по особенностям volatile переменных и атомиков, чтобы было side-by-side сравнение.

volatile переменные:

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

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

- Физически это значит, что volatile переменные запрещается кэшировать в регистрах и их всегда нужно честно читать из памяти.

- Запрещается реордеринг операций volatile переменных с другими операциями с видимыми спец-эффектами, расположенных выше и ниже по коду.

- Любой другой реордеринг разрешен.

- Операции над такими переменными не являются гарантированно атомарными в том плане, что есть возможность увидеть их промежуточное состояние. В целом, ничто не мешает пометить volatile объект std::unordered_map и конечно же операции над мапой не будут атомарными. Они могут быть атомарными на определенных архитектурах для тривиальных типов с правильным выравниванием памяти, но это никто не гарантирует.

- Запись и чтение volatile переменных не связаны между собой отношением synchronized-with, поэтому на их основе нельзя выстроить межпотоковое отношение happens-before. А это значит, что по стандарту С++ доступ к volatile переменной из разных потоков - это гонка данных и ub.


std::atomic:

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

- По сути, если компилятор докажет, что поток будет всегда читать одно и то же значение атомика, то он может его закэшировать. Если ваш код только читает из memory mapped io, то компилятор теоритически может выкинуть чтение и заменить заранее вычисленным значением. Поэтому атомик нельзя использовать, как замену volatile.

- Вы можете контролировать, какой барьер памяти хотите поставить атомарной операцией, и соответственно можете контролировать реордеринг. Самый сильный порядок предполагает, полный барьер памяти - никакие инструкции до атомика не могут быть переупорядочены ниже по коду и наоборот. Самый слабый порядок не предполагает никаких барьеров.

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

- Запись и чтение атомиков связаны между собой отношением synchronized-with, поэтому на их основе можно построить межпотоковое отношение happens-before. Это значит, что по стандарту операции непосредственно над атомарными переменными не могут приводить к гонке данных.

- При использовании правильных порядков и барьеров памяти вы можете добиться того, что с помощью атомарных переменных вы сможете соединять операции над неатомиками отношением happens-before. Это значит, что атомики можно использовать для корректной синхронизации неатомарных переменных и предотвращения гонки данных над ними.

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

Compare things. Stay cool.

#cppcore #cpp11 #multitasking
21👍14🔥8❤‍🔥1
const vs constexpr переменные
#новичкам

Хоть constexpr появился в С++11, многие так до конца и не понимают смысла этого ключевого слова. Давайте сегодня на базовом уровне посмотрим, чем отличаются const и constexpr, чтобы наглядно увидеть разницу.

Начнем с const. Наш старый добрый друг с первых дней C++ (а также C), может быть применен к объектам, чтобы указать на их неизменяемость. Попытка изменить const переменную напрямую приведет к ошибке компиляции, а через грязные хаки может привести к UB.

И все. Здесь ничего не говорится о том, когда инициализируется такая переменная. Компилятор может оптимизировать код и выполнить инициализацию на этапе компиляции. А может и не выполнить. Но в целом объекты const инициализируются во время выполнения:

// might be optimized to compile-time if compiled decides...
const int importantNum = 42;

std::map<std::string, double> buildMap() { /.../ }
// will be inited at runtime 100%
const std::map<std::string, double> countryToPopulation = buildMap();


const иногда даже может использоваться в compile-time вычислениях, если она имеет интегральный тип и инициализировано compile-time значением. В любых других случаях это не так.

Есть простой способ проверить, может ли переменная быть использована в compile-time вычислениях - попробовать инстанцировать с ней шаблон:

const int count = 3;
std::array<double, count> doubles {1.1, 2.2, 3.3}; // int with literal initializer is OK
// при использовании const переменной в compile-time контексте она инициализируется в compile-time

// but not double:
const double dCount = 3.3;
std::array<double, static_cast<int>(dCount)> moreDoubles {1.1, 2.2, 3.3};
// error: the value of 'dCount' is not usable in a constant expression
// не то, чтобы double нельзя было использовать как constant expression, просто const double - не constant expression.


Отсюда мы приходим к понятию constant expression - выражение, которое может быть вычислено во время компиляции. Такие выражения могут использоваться как нетиповые шаблонные параметры, размеры массивов и в других контекстах, которые требуют compile-time значений. Если такое выражение используется в других вычислениях, которые требуют compile-time значений, то оно гарантировано вычислится в compile-time.

Видно, что просто пометив переменную const нам никто не гарантирует, что ее можно будет использовать в качестве constant expression, кроме пары случаев.

Хотелось бы четко говорить компилятору, что мы собираемся использовать данную переменную в compile-time вычислениях.

И ровно для этой цели можно помечать переменную constexpr. Вы так приказываете компилятору попытаться вычислить переменную в compile-time. Главное, чтобы инициализатор также был constant expression. Помечая переменную constexpr, вы фактически добавляете константность к ее типу.

// fine now:
constexpr double dCount = 3.3; // literal is a constant expression
std::array<double, static_cast<int>(dCount)> doubles2 {1.1, 2.2, 3.3};


Просто const double шаблон не переварил, а constexpr double - спокойно.

constant expression - не обязан быть какого-то рода одной чиселкой. С каждым стандартом constexpr все больше распространяется на стандартные классы и их операции и мы можем даже создать constexpr std::array, вызвать у него метод и все это в compile-time:

constexpr std::array<int, 3> modify() {
std::array<int, 3> arr = {1, 2, 3};
arr[0] = 42; // OK в C++20 (изменение в constexpr)
return arr;
}
constexpr auto arr1 = modify(); // arr == {42, 2, 3}
static_assert(arr1.size() == 3);
static_assert(arr1[0] == 42);


Есть принципиальные ограничения на constexpr объекты. Например, довольно сложно и непонятно, как работать в compile-time с динамическими аллокациями. Поэтому нельзя например создать constexpr объект std::unordered_map. В некоторых случаях с большими оговорками динамические аллокации возможны, но это для другого поста.

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

Don't be confused. Stay cool.
#cpp11
26🔥9👍8
constexpr функции сквозь года
#новичкам

constexpr бывают не только переменные, но и функции. Такие функции могут быть выполнены, как в compile-time, так и в runtime, в зависимости от контекста вызова:
- Если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции.
- Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime.
- Если вы попытаетесь присвоить возвращаемое значение функции с runtime аргументом constexpr переменной, то получите ошибку компиляции.

constexpr int square(int x) {
return x * x;
}

int main() {
constexpr int val = square(5); // compile-time calculations
static_assert(val == 25, "Must be 25"); // compile-time check
int arg = rand() % 25;
int res = square(arg); // runtime calculations
assert(res == arg*arg); // runtime check
constexpr int fail = square(arg); // compile error here!
}


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

В С++11 можно было использовать только однострочные constexpr функции с сильными ограничениями.


constexpr int factorial(int n) {
// ternary operator is allowed, so recursion
return (n <= 1) ? 1 : n * factorial(n - 1);
}


В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:

constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

static_assert(factorial(5) == 120, "Must be 120");


C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:

constexpr auto factorial = [](int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
};

static_assert(factorial(5) == 120, "Must be 120");


C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.

constexpr std::string create_greeting() {
std::string s = "Hello, ";
s += "C++20!";
return s;
}

static_assert(create_greeting() == "Hello, C++20!");


constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.

В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).

Free up time for important things. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥30👍86👎2