🔞 Если принимаете сырой указатель, как параметр - не забудьте проверить его на nullptr. Меньше будете голову ломать при будущем дебаге.
🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.
🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.
Но если очень хочется оставить compareData, то лучше все равно определить оператор, не делать compareData другом класса, и пусть она использует нативный оператор внутри себя без нарушения инкапсуляции.
🔞 Data имеет конструктор от одного аргумента и лучше бы его сделать explicit. В таком случае, чтобы запретить неявные преобразования.
🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.
Все же от Data можно безопасно наследоваться, если не использовать потом наследников полиморфно. А при добавлении виртуального метода можно и самому понять, что надо еще и деструктор соотвествующим сделать.
Мне final кажется оверкиллом для одиночных классов в небиблиотечном коде , но тут кто как привык.
Фух, на этом вроде все.
Вот что вышло по итогу исправлений:
Пишите, если что забыл.
Critique your solution. Stay cool.
#cppcore #OOP #goodpractice #design
🔞 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
Почему важно проверять входные данные
#новичкам
Вы делаете какие-то вычисления и среди них есть целочисленное деление:
Вы уже наслышаны о том, что в С++ есть механизм обработки исключений и просто оборачиваете вашу функцию в try-catch и думаете, что С++ позаботится о том, что вы будете информированы о всех проблемах, которые могут произойти в коде:
Однако помните, что вы пишите на С++. Здесь уровень заботы примерно, как у бати, который вывозит неумеющего плавать сына на середину реки и бросает его в воду с криками "Плыви, сынок!".
Если вдруг вы передадите в fun вторым параметром ноль:
то С++ не пошлет исключение об этой ситуации.
Стандарт явно говорит: целочисленное деление на ноль приводит к неопределенному поведению. У меня например программа просто падает с надписью:
Поэтому нужно проверять входные данные и самим явно кидать исключения или использовать другой способ обработки ошибки:
Тогда на консоль явно выведется ожидаемое
Так что будьте осторожны и проверяйте входные данные.
Stay alert. Stay cool.
#cppcore
#новичкам
Вы делаете какие-то вычисления и среди них есть целочисленное деление:
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
👍32❤12🔥11❤🔥4🤯3
std::to_address
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
Use the right tool. Stay cool.
#cpp20 #cppcore
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции 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
2❤23👍9🔥7😁3
Квиз
Сегодня потенциально самый необычный #quiz на канале. Воистину, C/С++ - это самые интересные языки из существующих. Столько безобразия можно с ними натворить. С одной стороны, это мешает языкам выходить в мейнстрим "патамушта небезопасна". С другой стороны, здесь максимальная свобода творчества и полета фантазии.
У меня для вас всего один вопрос: Что произойдет в результате попытки компиляции и запуска этого С-кода:
Тут даже думать не нужно, если вы не компьютер. Выбирайте то, что подсказывает ваше сердце.
Сhoose with your heart. Stay cool.
Сегодня потенциально самый необычный #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.
1❤9🤣3🤯2⚡1
🤣16❤11🤯7👍2😱2
Ответ
#опытным
Ну что. Пора раскрывать карты.
Правильных ответов здесь несколько(не зря был квиз с множественным ответом) и все зависит от версии компилятора и опций компиляции. Интереснее всего смотреть, что получится при использовании например gcc9 без опций.
А получится вот что. Программа успешно скомпилируется и на консоли появится сообщение:
Да, да. Это она про вас, наши подписчики.
Дело вот в чем. Есть такой варнинг в gcc: warning: 'main' is usually a function [-Wmain].
Погодите, main "обычно" является функцией. Это что, может быть не так?
В С нет или не было прям жесткого требования на тип символа main. Он в целом может быть и массивом:
хоть указателем на функцию:
И вот здесь начинается пространство для экспериментов. Адрес main - это место, с которого код начинается исполняться. Если мы каким-то образом запихаем инструкции ассемблера в массив, то мы сможем выполнить код в такой программе!
Ну а дальше дело техники. Пишем прокси программу:
Компилируем ее и запускаем gdb для дизассемблирования main:
Ну и пожалуйста. Слева четко видим шестнадцатиричные числа, которые и представляют собой тело функции main.
Единственное, что осталось - вывести эти числа в десятиричной форме по 4 байта, как инты:
Делаем из этих чиселок массив и готово! Закодированная программа будет выполняться.
В более новых версиях компиляторов эту лавочку прикрыли, потому что на gcc10 и более такая прога сегфолтится.
Но в любом случае, очень прикольно, что есть такая возможность.
Можете поиграться с кодом на годболте. Также можно почитать статью, в которой автор подробно расписал историю исследования возможности так писать код.
Благодарю @PyXiion за предоставление материалов для этого поста.
Be amazed. Stay cool.
#fun #OS
#опытным
Ну что. Пора раскрывать карты.
Правильных ответов здесь несколько(не зря был квиз с множественным ответом) и все зависит от версии компилятора и опций компиляции. Интереснее всего смотреть, что получится при использовании например 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🔥11❤8👍6
Обогащаем исключение
#новичкам
Еще один простой и известный лайфхак, как можно более эффективно работать с исключениями.
Вот пилишь проект на 100500 строк кода. Запускаешь и вылетает ошибка: "абракадабра, ты хреновый программист". И как понять, откуда это исключение прилетело?
И начинается. Все обкладывается принтами и смотрится после какого принта программа падает. Но можно сделать немного умнее.
Пусть у вас в проекте активно используется библиотека для работы с базой данных. При возникновении непредвиденной ситуации библиотека генерирует исключение. Однако мест, где вы используете базу, много. И вам хотелось бы понимать примерный путь пролетания исключения, чтобы лучше понимать ситуацию для отладки.
В прошлом посте мы просто прокидывали исключение дальше, без добавления в него какой-то информации. Но можно бросить новое исключение и передать туда сообщение об ошибке первого:
В итоге вы на верхнем слое приложения финально обрабатываете все исключения и получаете фактически стек вызовов, которые они пролетают. И уже намного проще найти место конкретной проблемы, особенно если есть информативные логи.
Track your problems to the bottom. Stay cool.
#cppcore
#новичкам
Еще один простой и известный лайфхак, как можно более эффективно работать с исключениями.
Вот пилишь проект на 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
Засовываем исключение в исключение
#опытным
Вы знали, что в плюсах есть вложенные исключения? Такие исключения могут хранить в себе несколько исключений. Сегодня мы посмотрим, что это за зверь такой.
Начнем с применения. В прошлом посте мы создавали новое исключение на основе строки ошибки обрабатываемого исключения. В этом случае нужно писать определенное количество бойлерплейта и теряется информация о типе изначального исключения. Чтобы избежать этих проблем, мы можем бросить новое исключение, которое будет в себе содержать старое:
Теперь исключение, которое вылетит из HandlingCalculations будет на самом деле содержать 3 исключения: от базы данных, от ComplicatedCalculations и от HandlingCalculations.
Вложенные исключения существуют с С++11 и очень интересно устроены. Рассмотрим несколько упрощенные версии сущностей, которые находятся под капотом механизма вложенных исключений. Есть класс std::nested_exception:
Этот класс ответственен за захват текущего исключения с помощью вызова std::current_exception().
Дальше имеется класс, который хранит в себе все множество исключений:
Объекты этого класса наследуются от nested_exception, в котором захвачено старое исключение, и от Except - нового исключения.
Ну и последний компонент - std::throw_with_nested:
При вызове throw_with_nested создается объект Nested_exception на основе переданного типа исключения и, неявно, nested_exception, которых сохраняет в себе указатель на старое исключение.
Получается, что мы при каждом вызове throw_with_nested подмешиваем новое исключение к старому с помощью множественного наследования.
Очень прикольная техника, которая позволяет строить цепочки объектов. Это как тупл, только расширяемый в рантайме.
Это все хорошо и интересно. Прокидывать вложенные исключения мы научились. Но рано или поздно их придется обработать. Как это сделать? Об этом будем говорить в следующем посте.
Inherit knowledge from your ancestor. Stay cool.
#cppcore #cpp11
#опытным
Вы знали, что в плюсах есть вложенные исключения? Такие исключения могут хранить в себе несколько исключений. Сегодня мы посмотрим, что это за зверь такой.
Начнем с применения. В прошлом посте мы создавали новое исключение на основе строки ошибки обрабатываемого исключения. В этом случае нужно писать определенное количество бойлерплейта и теряется информация о типе изначального исключения. Чтобы избежать этих проблем, мы можем бросить новое исключение, которое будет в себе содержать старое:
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
🔥16❤8👍5
Раскрываем вложенное исключение
#опытным
Каша заварилась, пришло время расхлебывать. В прошлом посте мы успешно завернули кучу исключений в одно, теперь нужно как-то все обратно развернуть. Для этого существует функция std::rethrow_if_nested. Работает она вот так:
Так как объект вложенного исключения - это наследник std::nested_exception, то мы имеем право динамически привести указатель переданного в rethrow_if_nested исключения к std::nested_exception. Если каст прошел успешно, то бросается исключение, сохраненное в nested_exception. Если каст провалился, значит
Обычно в статьях по этой теме приводят мягко говоря странные примеры того, как надо обрабатывать вложенные исключения:
Встречайте вложенные try-catch с заранее известной глубиной вложенности. Это конечно никуда не годится.
В реальности при повсеместном использовании вложенных исключений мы не знаем, сколько раз нам нужно сделать rethrow_if_nested. Но при этом мы точно знаем, что если исключение последнее в цепочке, то rethrow_if_nested ничего не сделает. Это же идеальное условие для остановки рекурсии!
Рекурсия решает проблему неопределенного количества уровней вложенности исключений и помогает удобно подряд печатать соответствующие сообщения об ошибках:
В этом случае на добавление дополнительной информации к исключению вы тратите всего 1 строчку, которая мало чем отличается от возбуждения нового исключения. Плюс в коде логики проекта вы одной строчкой обрабатываете все вложенные исключения. Красота!
Очень нишевая функциональность, которую вы вряд ли встретите в реальных проектах. Однако будет прикольно попробовать ее в своих пет-проектах, если вы не гнушаетесь исключений.
Have a good error handling system. Stay cool.
#cppcore #cpp11
#опытным
Каша заварилась, пришло время расхлебывать. В прошлом посте мы успешно завернули кучу исключений в одно, теперь нужно как-то все обратно развернуть. Для этого существует функция 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🔥12❤11👍8🤣2
Volatile
#опытным
Ключевое слово, которое не embedded С++ разработчик вряд ли когда-нибудь встречал в код. Сегодня мы поговорим, для чего оно используется.
Предположим, что у нас есть переменная keyboard_press, память под которую замаплена на память устройства ввода-вывода. Когда нажимается кнопка клавиатуры, изменяется переменная keyboard_press. Оставим сам маппинг за скобками и попробуем написать какую-то детсадовскую логику с переменной keyboard_press:
Что в ассемблере?
А где цикл? А где инкремент count_test?
На самом деле код собран с -О3 и компилятор просто выкинул цикл. Он не видит, что в данном коде где-то еще изменяется keyboard_press, поэтому разумно полагает, что мы написали бесконечный цикл без сайдэффектов, который вообще-то ub.
Но keyboard_press может изменяться, просто это никак не понятно по коду программы.
Теоретически компилятор мог бы увидеть, что мы замапили устройство ввода-вывода на эту переменную. А может и не увидеть. Если маппинг происходит в другой единице трансляции, то точно не увидит. Компилятор технически не может знать всего, что творится в коде. Он оптимизирует какой-то локальный участок кода на основе своих эвристик, которые просто не могут учитывать весь код программы.
Однако компилятор точно видит тип переменной. И на него мы можем повлиять. Вот чтобы отучить компилятор от таких фокусов, нужно пометить keyboard_press ключевым словом volatile.
Теперь ассемблер выглядит так:
Все, что делает volatile - все операции над переменной становятся видимыми спецэффектами и не могут быть оптимизированы компилятором. Ну и еще операции над volitile переменными не могут переупорядочиваться с другими видимыми спецэффектами в порядке кода программы.
Говорится ли здесь что-нибудь о потоках? Нет! Здесь говорится только об оптимизациях компилятора.
Поэтому использовать volatile можно только для обработки сигналов(хэндлер которых вызывается в том же прерванном потоке), либо в тех местах, где вы работаете с переменной строго в одном потоке.
Доступ к volatile переменным не атомарный + с их помощью нельзя делать синхронизацию неатомарных переменных между потоками, так как volitile не подразумевает барьеров памяти.
Именно из-за этих ограничений volatile используется в очень узком спектре задач работы с I/O. Во всех остальных случаях в С++ используются атомики.
Don't be optimized out. Stay cool.
#cppcore #multitasking #memory
#опытным
Ключевое слово, которое не 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👍18❤8🔥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
#опытным
Кратко пробежимся по особенностям 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 инициализируются во время выполнения:
const иногда даже может использоваться в compile-time вычислениях, если она имеет интегральный тип и инициализировано compile-time значением. В любых других случаях это не так.
Есть простой способ проверить, может ли переменная быть использована в compile-time вычислениях - попробовать инстанцировать с ней шаблон:
Отсюда мы приходим к понятию constant expression - выражение, которое может быть вычислено во время компиляции. Такие выражения могут использоваться как нетиповые шаблонные параметры, размеры массивов и в других контекстах, которые требуют compile-time значений. Если такое выражение используется в других вычислениях, которые требуют compile-time значений, то оно гарантировано вычислится в compile-time.
Видно, что просто пометив переменную const нам никто не гарантирует, что ее можно будет использовать в качестве constant expression, кроме пары случаев.
Хотелось бы четко говорить компилятору, что мы собираемся использовать данную переменную в compile-time вычислениях.
И ровно для этой цели можно помечать переменную constexpr. Вы так приказываете компилятору попытаться вычислить переменную в compile-time. Главное, чтобы инициализатор также был constant expression. Помечая переменную constexpr, вы фактически добавляете константность к ее типу.
Просто const double шаблон не переварил, а constexpr double - спокойно.
constant expression - не обязан быть какого-то рода одной чиселкой. С каждым стандартом constexpr все больше распространяется на стандартные классы и их операции и мы можем даже создать constexpr std::array, вызвать у него метод и все это в compile-time:
Есть принципиальные ограничения на constexpr объекты. Например, довольно сложно и непонятно, как работать в compile-time с динамическими аллокациями. Поэтому нельзя например создать constexpr объект std::unordered_map. В некоторых случаях с большими оговорками динамические аллокации возможны, но это для другого поста.
Но в целом, в сочетании с constexpr функциями вы можете делать намного больше, чем просто создавать массивы из даблов.
Don't be confused. Stay cool.
#cpp11
#новичкам
Хоть 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 функции.
В С++11 можно было использовать только однострочные constexpr функции с сильными ограничениями.
В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:
C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:
C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.
constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.
В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).
Free up time for important things. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#новичкам
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👍8❤6👎2
Баланс — залог успеха
Многие программисты пилят свои пет-проекты, но немногие запускают их в условный "прод". Для определенности возьмем сайт, на который вы загружаете pdf-ку и вам возвращается распознанный текст. С момента запуска такой проект претерпевает стандартный набор метаморфоз на пути к успеху:
- Разворачивается один инстанс сервера. Этого хватает, чтобы закрыть потребности первых посетителей.
- Дальше количество юзеров растет, простой облачный сервер не выдерживает нагрузки, приходится докупать к нему ресурсы.
- Команда грамотно настраивает SEO-оптимизацию сайта, он попадает в топ рекомендаций гугла и вот к вам уже огромная туча людей приходит. Нужно ставить несколько серверов.
И вот тут наша остановочка. Когда появляется несколько серверов, нужно каким-то образом распределять к ним запросы, чтобы каждый нагружался равномерно.
А еще могут возникнуть форсмажоры. В сессию студенты кааак начнут резко готовиться к экзаменам, писать электронные конспекты и побегут активно пользоваться нашим сайтом, распознавать старые оцифрованные книжки. Нужно будет динамически добавлять новые серверы и убирать их, когда нагрузка спадет.
Чтобы грамотно и гибко управлять своими серверами, нужны балансировщики нагрузки. Грубо говоря, это единая точка входа для всех запросов в систему, которая решает, кто будет их обрабатывать.
А как он это решает — отдельный разговор. Пару дней назад у Вани Ходора, бэкенд-разработчика Лавки, вышел пост со всесторонним обзором методов балансировки трафика. Он там описал алгоритмы балансировки (round-robin, consistent hashing и другие), да и вообще подробно рассказал про тонкости работы с балансировщиками.
Если вы интересуетесь дизайн-секциями и хотите получше в этой теме разобраться, то будет интересно.
Многие программисты пилят свои пет-проекты, но немногие запускают их в условный "прод". Для определенности возьмем сайт, на который вы загружаете pdf-ку и вам возвращается распознанный текст. С момента запуска такой проект претерпевает стандартный набор метаморфоз на пути к успеху:
- Разворачивается один инстанс сервера. Этого хватает, чтобы закрыть потребности первых посетителей.
- Дальше количество юзеров растет, простой облачный сервер не выдерживает нагрузки, приходится докупать к нему ресурсы.
- Команда грамотно настраивает SEO-оптимизацию сайта, он попадает в топ рекомендаций гугла и вот к вам уже огромная туча людей приходит. Нужно ставить несколько серверов.
И вот тут наша остановочка. Когда появляется несколько серверов, нужно каким-то образом распределять к ним запросы, чтобы каждый нагружался равномерно.
А еще могут возникнуть форсмажоры. В сессию студенты кааак начнут резко готовиться к экзаменам, писать электронные конспекты и побегут активно пользоваться нашим сайтом, распознавать старые оцифрованные книжки. Нужно будет динамически добавлять новые серверы и убирать их, когда нагрузка спадет.
Чтобы грамотно и гибко управлять своими серверами, нужны балансировщики нагрузки. Грубо говоря, это единая точка входа для всех запросов в систему, которая решает, кто будет их обрабатывать.
А как он это решает — отдельный разговор. Пару дней назад у Вани Ходора, бэкенд-разработчика Лавки, вышел пост со всесторонним обзором методов балансировки трафика. Он там описал алгоритмы балансировки (round-robin, consistent hashing и другие), да и вообще подробно рассказал про тонкости работы с балансировщиками.
Если вы интересуетесь дизайн-секциями и хотите получше в этой теме разобраться, то будет интересно.
Telegram
this->notes.
#highload
Балансируем трафик.
https://telegra.ph/Balansiruem-trafik-06-10
Ещё бы как-то жизнь забалансировать...
Балансируем трафик.
https://telegra.ph/Balansiruem-trafik-06-10
Ещё бы как-то жизнь забалансировать...
👍13❤🔥8❤4👎1🔥1👏1
WAT
#новичкам
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Дан простой кусочек кода:
Все просто. Тип 3-х переменных проверяется на константность.
Вопрос: сможете сказать без компилятора какой из трех вариантов нормально соберется, какой выдаст assert, а какой выдаст warning?
Возьмите паузу на подумать.
Ответ будет такой:
ЧЗХ? Там же все константное?
Здесь дело в особенностях константности указателей. Чуть подробнее мы это разбирали в этом посте, но сейчас краткая выжимка.
Бывают константные указатели и указатели на константы. И это разные типы! Первый нельзя передвигать, но можно изменять данные, на которые он указывает. Второй можно передвигать, но данные изменить не получится.
Так вот ассерты проверяют является ли сам указатель константным.
Теперь с constexpr разбираемся. Этот спецификатор подразумевает const. И так как его нельзя применять более одного раза при объявлении переменной, то он применяется к самой "верхушке" типа. То есть
Для
Все примеры довольно просто объяснимы, хотя на первый взгляд лицо деформируется в вопросительный знак. Помните об особенностях константности указателей и будет вам счастье.
Be amazed. Stay cool.
#cppcore
#новичкам
Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Дан простой кусочек кода:
const char* s1 = "First";
constexpr char* s2 = "Second";
constexpr const char* s3 = "Third";
static_assert(std::is_const_v<decltype(s1)>);
static_assert(std::is_const_v<decltype(s2)>);
static_assert(std::is_const_v<decltype(s3)>);
Все просто. Тип 3-х переменных проверяется на константность.
Вопрос: сможете сказать без компилятора какой из трех вариантов нормально соберется, какой выдаст assert, а какой выдаст warning?
Возьмите паузу на подумать.
Ответ будет такой:
static_assert(std::is_const_v<decltype(s1)>); // выдаст ассерт
static_assert(std::is_const_v<decltype(s2)>); // выдаст варнинг
static_assert(std::is_const_v<decltype(s3)>); // нормально скомилится
ЧЗХ? Там же все константное?
Здесь дело в особенностях константности указателей. Чуть подробнее мы это разбирали в этом посте, но сейчас краткая выжимка.
Бывают константные указатели и указатели на константы. И это разные типы! Первый нельзя передвигать, но можно изменять данные, на которые он указывает. Второй можно передвигать, но данные изменить не получится.
Так вот ассерты проверяют является ли сам указатель константным.
s1
- это неконстантный указатель на константу, поэтому срабатывает ассерт.Теперь с constexpr разбираемся. Этот спецификатор подразумевает const. И так как его нельзя применять более одного раза при объявлении переменной, то он применяется к самой "верхушке" типа. То есть
s2
и s3
становятся константными указателями. И для них ассерты не срабатывают.Для
s2
выдается варнинг, потому что мы пытаемся присвоить строковый литерал, который имеет тип const char[], то есть массив константных символов, к указателю на неконстанту. В нормальной ситуации это бы вызвало ошибку компиляции, но такие преобразования возможны в С. И С++ сохраняет здесь совместимость, хоть и стремается этого и генерирует предупреждение о такой опасной ситуации.Все примеры довольно просто объяснимы, хотя на первый взгляд лицо деформируется в вопросительный знак. Помните об особенностях константности указателей и будет вам счастье.
Be amazed. Stay cool.
#cppcore
❤29👍17🤯11🔥3❤🔥2😁2
constexpr vs consteval функции
#опытным
В С++20 добавили новый спецификатор - consteval. Отдельно его интро не особо интересно разбирать, поэтому попробуем в сравнении с constexpr.
consteval может быть применен только к функциям и это ключевое слово заставляет функцию выполняться во время компиляции и возвращать константное выражение. Отсюда и название спецификатора "константное вычисление"(constant evaluation).
Вот пара простых примеров:
Мы можем использовать оба спецификатора constexpr и consteval для того, чтобы инициализировать compile-time константы и обычные переменные.
Однако consteval функции могут вызываться только с constant expression в качестве аргументов. При попытке передачи в них неконстантного выражения будет ошибка компиляции.
Итого:
👉🏿 спецификатор consteval может быть применен только для функций
👉🏿 constexpr может быть применен и для переменных
👉🏿 consteval заставляет компилятор вычислять выражение на этапе компиляции. Если хотя бы один аргумент такой функции не является константным выражением, то будет ошибка компиляции
👉🏿 constexpr функции могут быть вычислены на этапе компиляции, если аргументы являются константными выражениями. Но также они могут быть вычислены в рантайме с аргументами в виде рантайм значений.
Don't be confused. Stay cool.
#cpp20
#опытным
В С++20 добавили новый спецификатор - consteval. Отдельно его интро не особо интересно разбирать, поэтому попробуем в сравнении с constexpr.
consteval может быть применен только к функциям и это ключевое слово заставляет функцию выполняться во время компиляции и возвращать константное выражение. Отсюда и название спецификатора "константное вычисление"(constant evaluation).
Вот пара простых примеров:
consteval int sum_consteval(int a, int b) {
return a + b;
}
constexpr int sum_constexpr(int a, int b) {
return a + b;
}
int main() {
constexpr auto c = sum_consteval(100, 100);
static_assert(c == 200);
constexpr auto c1 = sum_constexpr(100, 100);
static_assert(c1 == 200);
constexpr auto val = 10;
static_assert(sum_consteval(val, val) == 2*val);
int a = 10;
int res = sum_constexpr(a, 10); // fine with constexpr function
int res1 = sum_consteval(10, 10);
// int res2 = sum_consteval(a, 10); // error!
// the value of 'a' is not usable in a constant expression
}
Мы можем использовать оба спецификатора constexpr и consteval для того, чтобы инициализировать compile-time константы и обычные переменные.
Однако consteval функции могут вызываться только с constant expression в качестве аргументов. При попытке передачи в них неконстантного выражения будет ошибка компиляции.
Итого:
👉🏿 спецификатор consteval может быть применен только для функций
👉🏿 constexpr может быть применен и для переменных
👉🏿 consteval заставляет компилятор вычислять выражение на этапе компиляции. Если хотя бы один аргумент такой функции не является константным выражением, то будет ошибка компиляции
👉🏿 constexpr функции могут быть вычислены на этапе компиляции, если аргументы являются константными выражениями. Но также они могут быть вычислены в рантайме с аргументами в виде рантайм значений.
Don't be confused. Stay cool.
#cpp20
👍26🔥5❤4🗿2😁1
constexpr vs constinit
#опытным
constinit - еще один спецификатор, который появился в С++20. Им помечаются глобальные или thread-local переменные для того, чтобы удостоверится в их статической инициализации. Это либо нулевая инициализация, либо константная инициализация. Собственно, это и отображено в самом названии спецификатора.
Тут суть в Static Initialization Order Fiasco. Инициализация глобальных переменных в рантайме зависит от фазы луны и половой активности жаб в Центральной Америке. Мы нем можем гарантировать порядок инициализации глобальных переменных в рантайме и если одна переменная зависит от значения другой, то может произойти много неприятных неожиданностей.
Вот constinit служит гарантией того, что переменная проинициализирована до старта программы, а программистам показывает, что с этой глобальной переменной точно все в порядке.
Интересность ситуёвины состоит в том, что constinit не подразумевает константность объекта! Действительно, мы же можем проинициализировать константным выражение ь неконстантную переменную и это будет валидный код:
constinit - это только про гарантии инициализации в компайл тайме и все! Например:
Мы можем инициализировать global с помощью константных выражений, в том числе и результатами вычислений constexpr функций. Однако сама global не является ни constexpr, ни даже обычной константой. С ее помощью нельзя инициализировать другие constinit переменные, как нельзя использовать ее в качестве шаблонных параметров. Но global можно изменять, как как она не предполагает иммутабельность.
Вы также не можете определить constexpr constinit переменную, потому что будет масло масляное. constexpr и так обеспечивает статическую инициализацию глобальных переменных.
Итого:
👉🏿 constinit переменные в своей базе мутабельные, constexpr - немутабельные.
👉🏿 constinit применяется только к static и thread storage duration объектам. Проще говоря, к разного рода глобальным переменным. constexpr может применяться к локальным переменным.
👉🏿 Оба спецификатора обеспечивают инициализацию глобальных переменных в compile-time и защищают от SIOF.
👉🏿 Эти спецификаторы нельзя использовать в одном выражении.
Don't be confused. Stay cool.
#cpp20
#опытным
constinit - еще один спецификатор, который появился в С++20. Им помечаются глобальные или thread-local переменные для того, чтобы удостоверится в их статической инициализации. Это либо нулевая инициализация, либо константная инициализация. Собственно, это и отображено в самом названии спецификатора.
Тут суть в Static Initialization Order Fiasco. Инициализация глобальных переменных в рантайме зависит от фазы луны и половой активности жаб в Центральной Америке. Мы нем можем гарантировать порядок инициализации глобальных переменных в рантайме и если одна переменная зависит от значения другой, то может произойти много неприятных неожиданностей.
Вот constinit служит гарантией того, что переменная проинициализирована до старта программы, а программистам показывает, что с этой глобальной переменной точно все в порядке.
Интересность ситуёвины состоит в том, что constinit не подразумевает константность объекта! Действительно, мы же можем проинициализировать константным выражение ь неконстантную переменную и это будет валидный код:
static int i = 42;
constinit - это только про гарантии инициализации в компайл тайме и все! Например:
// init at compile time
constexpr int compute(int v) { return vvv; }
constinit int global = compute(10); // compute is invoked at compile-time
// won't work:
// constinit int another = global; // global is a runtime value
int main() {
// but allow to change later...
global = 100;
// global is not constant expression!
// std::array<int, global> arr;
}
Мы можем инициализировать global с помощью константных выражений, в том числе и результатами вычислений constexpr функций. Однако сама global не является ни constexpr, ни даже обычной константой. С ее помощью нельзя инициализировать другие constinit переменные, как нельзя использовать ее в качестве шаблонных параметров. Но global можно изменять, как как она не предполагает иммутабельность.
Вы также не можете определить constexpr constinit переменную, потому что будет масло масляное. constexpr и так обеспечивает статическую инициализацию глобальных переменных.
Итого:
👉🏿 constinit переменные в своей базе мутабельные, constexpr - немутабельные.
👉🏿 constinit применяется только к static и thread storage duration объектам. Проще говоря, к разного рода глобальным переменным. constexpr может применяться к локальным переменным.
👉🏿 Оба спецификатора обеспечивают инициализацию глобальных переменных в compile-time и защищают от SIOF.
👉🏿 Эти спецификаторы нельзя использовать в одном выражении.
Don't be confused. Stay cool.
#cpp20
👍19❤10🔥9❤🔥1
Висячие ссылки в лямбдах
#новичкам
Все знают, что возврат ссылки на локальный объект функции приводит к неопределенному поведению. Однако не всегда так просто можно распознать такие ситуации.
В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.
Лямбда, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой её создали. Как только лямбда покинула скоуп - можно начинать молиться:
Гцц и шланг пишут разный результат на консоль, что напрямую говорит об ub. Можете посмотреть тут. На варнинги об этой ситуации лучше не надеяться, потому что гцц например думает, что в коде все в порядке.
Еще более интересная ситуация с объектами и методами.
Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.
На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать
Так уже чуть проще отловить проблему.
Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.
В случае с захватом this профилактикой может быть синтаксическое ограничение использование методов, возвращающий лямбду, с помощью ref-квалификаторов методов:
Теперь вы не сможете вызвать этот метод на временном объекте, потому что удалена соответствующая перегрузка.
Конечно это вряд ли поможет в многопоточке, но это уже что-то.
Refer to actual things. Stay cool.
#cppcore #cpp11 #cpp20
#новичкам
Все знают, что возврат ссылки на локальный объект функции приводит к неопределенному поведению. Однако не всегда так просто можно распознать такие ситуации.
В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.
Лямбда, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой её создали. Как только лямбда покинула скоуп - можно начинать молиться:
auto make_add_n(int n) {
return [&](int x) {
return x + n; // n will become dangling reference!
};
}
auto add5 = make_add_n(5);
std::cout << add5(5) << std::endl; // UB!
Гцц и шланг пишут разный результат на консоль, что напрямую говорит об ub. Можете посмотреть тут. На варнинги об этой ситуации лучше не надеяться, потому что гцц например думает, что в коде все в порядке.
Еще более интересная ситуация с объектами и методами.
struct Task {
int id;
std::function<void()> GetNotifier() {
return [=]{
std::cout << "notify " << id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}
Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.
На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать
this
в списке захвата:struct Task {
int id;
std::function<void()> GetNotifier() {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};
Так уже чуть проще отловить проблему.
Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.
В случае с захватом this профилактикой может быть синтаксическое ограничение использование методов, возвращающий лямбду, с помощью ref-квалификаторов методов:
struct Task {
int id;
std::function<void()> GetNotifier() && = delete; // forbit call on temporaries
std::function<void()> GetNotifier() & {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};
Теперь вы не сможете вызвать этот метод на временном объекте, потому что удалена соответствующая перегрузка.
Конечно это вряд ли поможет в многопоточке, но это уже что-то.
Refer to actual things. Stay cool.
#cppcore #cpp11 #cpp20
2🔥27👍17❤11
История capture this
#опытным
Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.
С++11
Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается
Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.
С++14
Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:
В остальном все осталось также.
С++17
Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:
В этом коде UB, потому что возвращаем лямбду со ссылкой на несуществующий объект. Поменять это можно захватом объекта по значению:
В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.
С++20
С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.
Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.
Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:
Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.
Know the history. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#опытным
Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.
С++11
Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается
&(*this)
то есть указатель на текущий объект:struct Foo {
int m_x = 0;
void func() {
int x = 0;
//Explicit capture 'this'
[this]() { /*access m_x and x*/ }();
//Implcit capture 'this'
[&]() { /*access m_x and x*/ }();
//Redundant 'this'
[&, this]() { /*access m_x and x*/ }();
//Implcit capture 'this'
[=]() { /*access m_x and x*/ }();
//Error
[=, this]() { }();
}
};
Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.
С++14
Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:
struct Foo {
int m_x = 0;
void func() {
[copy=*this]() mutable {
copy.m_x++;
}();
}
};
В остальном все осталось также.
С++17
Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:
struct Processor {
//Some state data..
std::future<void> process(/*args*/) {
//Pre-process...
//Do the data processing asynchronously
return
std::async(std::launch::async,
[=](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/
//process data
});
}
};
auto caller() {
Processor p;
return p.process(/*args*/);
}
В этом коде UB, потому что возвращаем лямбду со ссылкой на несуществующий объект. Поменять это можно захватом объекта по значению:
struct Processor {
std::future<void> process(/*args*/) {
return
std::async(std::launch::async,
[*this](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/
//process data
});
}
};
В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.
С++20
С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.
Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.
Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:
struct Bagel {
int x = 0;
void func() {
//OK until C++20. Warning in C++20.
[=]() { std::cout << x; }();
//Error/warning until C++20. OK in C++20.
[=, this]() { std::cout << x; }();
}
};
Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.
Know the history. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
🔥14👍10❤8🤯4
Правильно захватываем по ссылке объект в лямбду
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#template #cpp11
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
struct Task {
int id;
std::function<void()> GetNotifier() {
return [&]{
std::cout << "notify " << id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}
Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
struct Task {
int id;
std::function<void()> GetNotifier() {
return [curr = std::shared_ptr<Task>(this)]{
std::cout << "notify " << curr->id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}
Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
curr
будет думать, что только он и его копии будут владеть объектом, а оригинальный объект просто уничтожится и curr
будет ссылаться на невалидный объект. Получили то же самое UB.Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
struct Task : std::enable_shared_from_this<Task> {
int id;
std::function<void()> GetNotifier() {
// Захватываем shared_ptr на текущий объект
return [self = shared_from_this()] {
std::cout << "notify " << self->id << std::endl;
};
}
};
int main() {
auto notify = std::make_shared<Task>(5)->GetNotifier();
notify(); // Теперь безопасно - объект не будет уничтожен
}
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#template #cpp11
2👍93❤19🔥14🤯1