Обогащаем исключение
#новичкам
Еще один простой и известный лайфхак, как можно более эффективно работать с исключениями.
Вот пилишь проект на 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
❤29👍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🤣1
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
❤20👍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
🔥29👍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
❤28👍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
👍18❤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🔥26👍16❤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
🔥13👍8❤7🤯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👍85❤19🔥14🤯1
Динамический полиморфизм. ООP-style
#новичкам
Полиморфизм - это способность кода единообразно обрабатывать разные сущности. И хоть термин "полиморфизм" называют принципом ООП, это понятие в широком смысле выходит за границы этой парадигмы. Любая конструкция языка, которая позволяет единообразно управлять разными сущностями проявляет полиморфные свойства. В этом и следующих постах постараемся по верхам раскрыть сущности, реализующие полиморфизм в С++ в широком смысле.
Но раз уж заговорили про об ООП, давайте для начала поговорим понятие про полиморфизм в рамках ООП.
Если мы говорим про ООП, значит где-то рядом тусуются классы и их иерархии. Полиморфизм в объектно-ориентированном программировании - один из основных его принципов. Это свойство, позволяющее объектам разных классов обрабатываться одинаково, используя общий интерфейс. При этом поведение разное в зависимости от конкретного типа объекта. Реализации интерфейсов у всех классов разные. И решение о вызове того или иного конкретного метода принимается во время выполнения программы.
Для работы работы динамического полиморфизма нужен: базовый класс, пара наследников и виртуальные методы:
У нас есть интерфейс ITask и виртуальный метод Execute. Два других класса наследуются от ITask и переопределяют метод Execute. В задаче FileDeleteTask удаляется файл по заданному пути из файловой системы. В задаче S3FileUploadTask файл загружается в удаленное хранилище S3.
Заметим, у этих задач общий интерфейс(их можно выполнить), но они совершают разные действия.
Теперь мы можем использовать эти задачи:
У нас есть 2 продюсера, которые кладут задачи в очередь, и воркер, который выполнятся задачи из очереди.
В очереди хранятся уникальные указатели на базовый класс ITask. Это значит, что она может хранить объекты любых наследников интерфейса ITask.
Теперь самое важное: воркеру не нужно знать, какую конкретно задачу он сейчас достанет из очереди и какой конкретно продюсер ее туда положил. Единственное, что важно - общий интерфейс. Он позволяет единообразно выполнить задачи разных типов, даже не зная их исходный тип.
В этом и суть: абстрагироваться от конкретной реализации и верхнеуровнево определить, как себя должен вести объект.
Но динамический полиморфизм не ограничивается полиморфизмом подтипов. Для него вообще иерархия классов не нужна. И в следующих постах посмотрим, что еще в С++ позволяет реализовать полиморфное поведение.
Extract common traits. Stay cool.
#OOP #cppcore
#новичкам
Полиморфизм - это способность кода единообразно обрабатывать разные сущности. И хоть термин "полиморфизм" называют принципом ООП, это понятие в широком смысле выходит за границы этой парадигмы. Любая конструкция языка, которая позволяет единообразно управлять разными сущностями проявляет полиморфные свойства. В этом и следующих постах постараемся по верхам раскрыть сущности, реализующие полиморфизм в С++ в широком смысле.
Но раз уж заговорили про об ООП, давайте для начала поговорим понятие про полиморфизм в рамках ООП.
Если мы говорим про ООП, значит где-то рядом тусуются классы и их иерархии. Полиморфизм в объектно-ориентированном программировании - один из основных его принципов. Это свойство, позволяющее объектам разных классов обрабатываться одинаково, используя общий интерфейс. При этом поведение разное в зависимости от конкретного типа объекта. Реализации интерфейсов у всех классов разные. И решение о вызове того или иного конкретного метода принимается во время выполнения программы.
Для работы работы динамического полиморфизма нужен: базовый класс, пара наследников и виртуальные методы:
struct ITask {
virtual void Execute() = 0;
virtual ~ITask() = default;
};
struct FileDeleteTask : public ITask {
std::string path_;
FileDeleteTask(const std::string &path) : path_(path) {}
void Execute() override {
std::filesystem::remove(path_);
std::cout << "Deleted: " << path_ << std::endl;
}
};
struct S3FileUploadTask : public ITask {
std::string bucket_;
std::string path_;
std::shared_ptr<S3Client> client_;
S3FileUploadTask(const std::string &bucket, const std::string &path, const std::shared_ptr<S3Client> &client)
: bucket_{bucket}, path_{path}, client_{client} {}
void Execute() override {
client_->Upload(bucket_, path_);
std::cout << "Uploaded: " << bucket_ << ", pathL " << path_ << std::endl;
}
};
У нас есть интерфейс ITask и виртуальный метод Execute. Два других класса наследуются от ITask и переопределяют метод Execute. В задаче FileDeleteTask удаляется файл по заданному пути из файловой системы. В задаче S3FileUploadTask файл загружается в удаленное хранилище S3.
Заметим, у этих задач общий интерфейс(их можно выполнить), но они совершают разные действия.
Теперь мы можем использовать эти задачи:
void Producer1(const std::string &bucket, const std::vector<std::string> &paths,
const std::shared_ptr<S3Client> &client, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<S3FileUploadTask>(bucket, path, client));
}
void Producer2(const std::vector<std::string> &paths, std::deque<std::unique_ptr<ITask>> &tasks) {
for (const auto &path : paths)
tasks.emplace_back(std::make_unique<FileDeleteTask>(path));
}
void Worker(std::deque<std::unique_ptr<ITask>> &tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
task.pop_front();
task->Execute();
}
}
У нас есть 2 продюсера, которые кладут задачи в очередь, и воркер, который выполнятся задачи из очереди.
В очереди хранятся уникальные указатели на базовый класс ITask. Это значит, что она может хранить объекты любых наследников интерфейса ITask.
Теперь самое важное: воркеру не нужно знать, какую конкретно задачу он сейчас достанет из очереди и какой конкретно продюсер ее туда положил. Единственное, что важно - общий интерфейс. Он позволяет единообразно выполнить задачи разных типов, даже не зная их исходный тип.
В этом и суть: абстрагироваться от конкретной реализации и верхнеуровнево определить, как себя должен вести объект.
Но динамический полиморфизм не ограничивается полиморфизмом подтипов. Для него вообще иерархия классов не нужна. И в следующих постах посмотрим, что еще в С++ позволяет реализовать полиморфное поведение.
Extract common traits. Stay cool.
#OOP #cppcore
1❤25👍9🔥8⚡1
Динамический полиморфизм. std::function
#новичкам
В прошлом посте поговорили, что динамический полиморфизм реализуется не только через иерархии классов и виртуальные методы.
Есть другой прекрасный инструмент - std::function. Это обертка над всеми callable объектами, которая позволяет их единообразоно вызывать. Никаких иерархий, только функциональные объекты.
Теперь в очереди хранятся какие-то вызываемые объекты. Воркеру не важно, что это за объекты. Главное, что продюсеры могут разные функциональные объекты положить в один и тот же контейнер, попутно обернув их в std::function и тем самым полностью обезличив их. А легитимность такого мува достигается за счет того, что эти объекты имеют единый интерфейс - их можно вызвать без аргументов и не получить никакого возвращаемого значения.
Уже сейчас можно заметить, что для динамического полиморфизма нужно какого-то рода type erasure(стирание типов). Структура, которая хранит полиморфные объекты, не должна иметь полную информации о конкретном типе этих объектов. Объекты лишь должны иметь какой-то общий интерфейс. И тогда тип неважен: мы можем оперировать объектами через этот общий интерфейс.
std::function довольно интересно внутри устроен. После верхнеуровневого разговора про все полиморфизмы, вернемся к нему.
Но в плюсах есть еще много примеров полиморфизма времени выполнения, о которых поговорим в следующий раз.
Extract common traits. Stay cool.
#cpp11
#новичкам
В прошлом посте поговорили, что динамический полиморфизм реализуется не только через иерархии классов и виртуальные методы.
Есть другой прекрасный инструмент - std::function. Это обертка над всеми callable объектами, которая позволяет их единообразоно вызывать. Никаких иерархий, только функциональные объекты.
void Worker(std::deque<std::function<void()>>& tasks) {
while (!tasks.empty()) {
auto task = std::move(tasks.front());
tasks.pop_front();
task(); // call callable
}
}
void Producer1(const std::string& bucket, const std::vector<std::string>& paths,
const std::shared_ptr<S3Client>& client, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
client_->Upload(bucket, path);
std::cout << "Uploaded: " << bucket << ", path " << path << std::endl;
});
}
void Producer2(const std::vector<std::string>& paths, std::deque<std::function<void()>>& tasks) {
for (const auto& path: paths)
tasks.emplace_back([&]{
std::remove(path.c_str());
std::cout << "Deleted: " << path << std::endl;
});
}
Теперь в очереди хранятся какие-то вызываемые объекты. Воркеру не важно, что это за объекты. Главное, что продюсеры могут разные функциональные объекты положить в один и тот же контейнер, попутно обернув их в std::function и тем самым полностью обезличив их. А легитимность такого мува достигается за счет того, что эти объекты имеют единый интерфейс - их можно вызвать без аргументов и не получить никакого возвращаемого значения.
Уже сейчас можно заметить, что для динамического полиморфизма нужно какого-то рода type erasure(стирание типов). Структура, которая хранит полиморфные объекты, не должна иметь полную информации о конкретном типе этих объектов. Объекты лишь должны иметь какой-то общий интерфейс. И тогда тип неважен: мы можем оперировать объектами через этот общий интерфейс.
std::function довольно интересно внутри устроен. После верхнеуровневого разговора про все полиморфизмы, вернемся к нему.
Но в плюсах есть еще много примеров полиморфизма времени выполнения, о которых поговорим в следующий раз.
Extract common traits. Stay cool.
#cpp11
❤21👍14🔥6
This media is not supported in your browser
VIEW IN TELEGRAM
2 августа Яндекс проведет конференцию C++ Zero Cost Conf для разработчиков
Конференция пройдет в двух странах и трех городах: в Москве (офлайн + онлайн), Белграде (офлайн + онлайн) и Санкт-Петербурге (только офлайн).
В программе:
— «i, j, k и шаблоны: вспоминаем линейную алгебру», Ваня Ходор, Яндекс Лавка.
— «Hardening: текущий статус и перспективы развития», Роман Русяев и Юрий Грибов, Huawei.
— «Алиасинг памяти в компиляторе и в вашей программе», Константин Владимиров и Владислав Белов, Syntacore.
Полную программу выступлений по городам можно посмотреть на сайте.
Для участников в Москве пройдёт воркшоп по системе непрерывного профилирования Perforator с демонстрацией работы и локального использования, кейс-лаб по надёжности сервисов, а также код-гольф с решением задача на C++. Во всех городах проведут код-ревью с поиском и исправлением ошибок в коде.
Регистрация
Конференция пройдет в двух странах и трех городах: в Москве (офлайн + онлайн), Белграде (офлайн + онлайн) и Санкт-Петербурге (только офлайн).
В программе:
— «i, j, k и шаблоны: вспоминаем линейную алгебру», Ваня Ходор, Яндекс Лавка.
— «Hardening: текущий статус и перспективы развития», Роман Русяев и Юрий Грибов, Huawei.
— «Алиасинг памяти в компиляторе и в вашей программе», Константин Владимиров и Владислав Белов, Syntacore.
Полную программу выступлений по городам можно посмотреть на сайте.
Для участников в Москве пройдёт воркшоп по системе непрерывного профилирования Perforator с демонстрацией работы и локального использования, кейс-лаб по надёжности сервисов, а также код-гольф с решением задача на C++. Во всех городах проведут код-ревью с поиском и исправлением ошибок в коде.
Регистрация
🔥15❤7👍3👎2
Динамический полиморфизм: указатели на функции и void указатели
#новичкам
C++ - разжиревший отпрыск С, поэтому в нем имеется возможность для динамического полиморфизма пользоваться сишными инструментами.
И два основных сишных инструмента дин полиморфизма - указатели на функции и void указатели.
Функции работают с аргументами и каждое имя функции при компиляции соответствует адресу этой функции в памяти. Даже если 2 функции имеют разные адреса, но одинаковый набор и порядок аргументов, в низкоуровневом коде они вызываются абсолютно единообразно. Поэтому есть такая сущность, как указатели на функции. Они могут хранить адреса любых функций с наперед заданной сигнатурой:
В коде выше с помощью одного указателя вызываются 2 разные функции. Полиморфизм? Вполне! Только вот примерчик давайте по-серьезнее возьмем:
std::bsearch - функция, которая выполняет алгоритм бинарного поиска и возвращает либо найденный элемент, либо нулевой указатель, если элемента не было в массиве. Причем он может проводить поиск в массивах разных типов по разным правилам!
Это достигается за счет использования указателя на функцию-компаратор и void указателя. К нему могут неявно приводиться указатели на любые типы, поэтому он не знает, на какой конкретный тип он указывает. Но ему это и не надо. Тот, кто имеет информацию о правильном типе(компаратор) может обратно привести void * к указателю на этот тип и работать уже с нормальным объектом.
Единственная сложность - нужен дополнительный параметр size, с помощью которого задается байтовый размер типа элемента массива.
Ну и давайте все это применим:
Есть два массива: интов и даблов. Для выполнения бинарного поиска для этих типов нужны абсолютно разные компараторы: как минимум даблы нельзя сравнивать втупую.
std::bsearch на этапе компиляции не знает, с какими типами и компараторами он будет работать. Все решения принимаются в рантайме. Но он умеет по-разному находить элементы в массивах разных типов. Именно поэтому bsearch использует инструменты именно динамического полиморфизма.
Act independently of input. Stay cool.
#cppcore #goodoldc
#новичкам
C++ - разжиревший отпрыск С, поэтому в нем имеется возможность для динамического полиморфизма пользоваться сишными инструментами.
И два основных сишных инструмента дин полиморфизма - указатели на функции и void указатели.
Функции работают с аргументами и каждое имя функции при компиляции соответствует адресу этой функции в памяти. Даже если 2 функции имеют разные адреса, но одинаковый набор и порядок аргументов, в низкоуровневом коде они вызываются абсолютно единообразно. Поэтому есть такая сущность, как указатели на функции. Они могут хранить адреса любых функций с наперед заданной сигнатурой:
int x2(int i) {
return i * 2;
}
int square(int i) {
return i * i;
}
using IntFuncPtr = int (*)(int);
IntFuncPtr func_ptr;
// Вызываем x2 через указатель
func_ptr = x2;
std::cout << "x2(5) = " << func_ptr(5) << std::endl;
// Вызываем square через указатель
func_ptr = square;
std::cout << "square(5) = " << func_ptr(5) << std::endl;
В коде выше с помощью одного указателя вызываются 2 разные функции. Полиморфизм? Вполне! Только вот примерчик давайте по-серьезнее возьмем:
void *bsearch(const void *key, const void *ptr, std::size_t count,
std::size_t size, /* c-compare-pred */ *comp);
void *bsearch(const void *key, const void *ptr, std::size_t count,
std::size_t size, /* compare-pred */ *comp);
extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);
std::bsearch - функция, которая выполняет алгоритм бинарного поиска и возвращает либо найденный элемент, либо нулевой указатель, если элемента не было в массиве. Причем он может проводить поиск в массивах разных типов по разным правилам!
Это достигается за счет использования указателя на функцию-компаратор и void указателя. К нему могут неявно приводиться указатели на любые типы, поэтому он не знает, на какой конкретный тип он указывает. Но ему это и не надо. Тот, кто имеет информацию о правильном типе(компаратор) может обратно привести void * к указателю на этот тип и работать уже с нормальным объектом.
Единственная сложность - нужен дополнительный параметр size, с помощью которого задается байтовый размер типа элемента массива.
Ну и давайте все это применим:
int compare_doubles(const void *a, const void *b) {
static constexpr double EPSILON = 1e-9;
double diff = *(double *)b - *(double *)a;
if (std::fabs(diff) < EPSILON) {
return 0;
}
return (diff > 0) ? 1 : -1;
}
int compare_ints(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
double double_arr[] = {5.5, 4.4, 3.3, 2.2, 1.1};
size_t double_size = sizeof(double_arr) / sizeof(double_arr[0]);
int int_arr[] = {10, 20, 30, 40, 50};
size_t int_size = sizeof(int_arr) / sizeof(int_arr[0]);
// Поиск в массиве double
double double_key = 3.30000000001; // Почти 3.3
double *double_res = (double *)std::bsearch(
&double_key, double_arr, double_size,
sizeof(double), compare_doubles);
// тут надо проверить на nullptr, но опустим это
std::cout << "Found double: " << *double_res << std::endl;
// Поиск в массиве int
int int_key = 30;
int *int_res =
(int *)std::bsearch(&int_key, int_arr, int_size,
sizeof(int), compare_ints);
std::cout << "Found int: " << *int_res << std::endl;
Есть два массива: интов и даблов. Для выполнения бинарного поиска для этих типов нужны абсолютно разные компараторы: как минимум даблы нельзя сравнивать втупую.
std::bsearch на этапе компиляции не знает, с какими типами и компараторами он будет работать. Все решения принимаются в рантайме. Но он умеет по-разному находить элементы в массивах разных типов. Именно поэтому bsearch использует инструменты именно динамического полиморфизма.
Act independently of input. Stay cool.
#cppcore #goodoldc
❤14👍10🔥4🤩2💯2
Что может помочь плюсисту сделать свой код чище и лучше? Только вредные советы, только хардкор! 🔥
Если вы ищете книгу, где собраны нетривиальные кейсы по плюсам, да еще и с юмором и мемами, то «Вредные советы для С++ программистов» — для вас.
Автор — Андрей Карпов, сооснователь компании PVS-Studio по разработке статического анализатора кода. В книге программист с двадцатилетним опытом рассказывает, как точно не стоит писать код. Каждый совет сопровождается подробным разбором с объяснением сложных и неочевидных аспектов C++.
«Вредные советы для С++ программистов» подойдут новичкам для изучения реальных примеров с пояснениями. А профессионалы и опытные разработчики смогут погрузиться в тонкости C++ и оценить иронию автора. Прокачивайте навыки, учитесь избегать распространенных ловушек и посмотрите на программирование под новым углом!
Хорошие новости: полную версию книги можно получить в PDF-файле одним из двух способов:
🔵 подпишитесь на дайджест статей от PVS-Studio;
🔵 подпишитесь на Telegram-бот.
Все подробности по ссылке, там же вы сможете скачать демоверсию «Вредных советов».
А еще больше полезной информации о буднях разработчика и нестандартных кейсах вы сможете узнать из авторского канала Андрея Карпова «Бестиарий программирования». Читайте и подписывайтесь!
Хорошего дня и чистого кода!❤
Если вы ищете книгу, где собраны нетривиальные кейсы по плюсам, да еще и с юмором и мемами, то «Вредные советы для С++ программистов» — для вас.
Автор — Андрей Карпов, сооснователь компании PVS-Studio по разработке статического анализатора кода. В книге программист с двадцатилетним опытом рассказывает, как точно не стоит писать код. Каждый совет сопровождается подробным разбором с объяснением сложных и неочевидных аспектов C++.
«Вредные советы для С++ программистов» подойдут новичкам для изучения реальных примеров с пояснениями. А профессионалы и опытные разработчики смогут погрузиться в тонкости C++ и оценить иронию автора. Прокачивайте навыки, учитесь избегать распространенных ловушек и посмотрите на программирование под новым углом!
Хорошие новости: полную версию книги можно получить в PDF-файле одним из двух способов:
Все подробности по ссылке, там же вы сможете скачать демоверсию «Вредных советов».
А еще больше полезной информации о буднях разработчика и нестандартных кейсах вы сможете узнать из авторского канала Андрея Карпова «Бестиарий программирования». Читайте и подписывайтесь!
Хорошего дня и чистого кода!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤5🔥5