Например, регистры — это самый быстрый уровень памяти внутри CPU, кэш (L1/L2/L3) снижает latency за счёт локальности данных, RAM хранит рабочее состояние приложений, а SSD/HDD используются для персистентного хранения с существенно более высоким временем доступа.
На картинке — базовая иерархия памяти, взаимодействие CPU — cache — RAM — disk, а также упрощённая модель работы HDD и архитектура SSD.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
Недостатки std::make_shared. Непубличные конструкторы
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
std::invoke
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::forward<Callback>(callback)(std::forward<Args>(args)...);
}
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
class Data {
public:
void memberFunction(int value) {
std::cout << "Data::memberFunction called with value: " << value << "\n";
}
};
Data data;
// Создаем указатель на метод класса
auto methodPtr = &Data::memberFunction;
// Вызываем метод через указатель на объекте
(data.*methodPtr)(42);
Согласитесь, что этот синтаксис отличается от std::forward<Callback>(callback)(std::forward<Args>(args)...);.
Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?
Использовать std::invoke. Это функция буквально создана, чтобы вызывать все, что только можно вызвать. Она конечно же написана на вариабельных шаблонах, чтобы вы могли передать туда все, что душе угодно:
template< class F, class... Args >
std::invoke_result_t<F, Args...> invoke( F&& f, Args&&... args );
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::invoke(std::forward<Callback>(callback), std::forward<Args>(args)...);
}
struct S {
void foo(int i) {}
};
process_and_call(&S::foo, S{}, 42);Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
👀124
Все вызываемые сущности в С++. Ч1
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
👉🏿 Ссылки на функции
Да, и такие тоже есть:
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
👉🏿 Статические методы классов
Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
Уже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
free_function(42);👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
std::invoke(free_function, 42);Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
auto fun_ptr = &free_function;
fun_ptr(42);
(*fun_ptr)(42);
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
void free_function(int a) {
std::println("Free function: {}", a);
}
call_callback(free_function, 42);👉🏿 Ссылки на функции
Да, и такие тоже есть:
auto& fun_ref = free_function;
fun_ref(42);
(*fun_ref)(42); // интересно, что такой синтаксис разрешен
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
void check_name(auto& obj) {
// by passing free_function to check_name, obj become function reference
}
check_name(free_function);👉🏿 Статические методы классов
struct Handler {
static void static_method(int a) {
std::cout << "Static method: " << a << std::endl;
}
};
Handler::static_method(42);Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
auto static_method_ptr = &Handler::static_method;
std::invoke(static_method_ptr, 42);
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
class SequenceGenerator {
int current;
int step;
public:
SequenceGenerator(int start = 0, int step_size = 1)
: current(start), step(step_size) {}
// Оператор вызова без аргументов - возвращает следующее число
int operator()() {
int val = current;
current += step;
return val;
}
};
SequenceGenerator gen;
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen); // заполняем вектор с помощью функтораУже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
👀92
Например, REST задаёт стандарт взаимодействия по HTTP, GraphQL — гибкую выборку данных, а gRPC — эффективную бинарную коммуникацию.
На схеме — основные области: основы (контракты, HTTP, stateless), паттерны (REST, RPC, WebSockets), безопасность (OAuth2, JWT, TLS), а также дизайн и эволюция API.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀155
std::thread
std::thread является частью стандартной библиотеки C++ и предоставляет возможность создания и управления потоками выполнения. Он позволяет запускать функции в отдельных потоках, обеспечивая параллельное выполнение кода.
Обратите внимание, что после создания потока std::thread, вы должны вызвать join() или detach() для корректной обработки завершения потока.
В приведенном примере мы использовали join(), чтобы основной поток дождался завершения потока t1. Если вы вызываете join() после завершения потока, программа может выдать исключение или вызвать неопределенное поведение.
std::thread является частью стандартной библиотеки C++ и предоставляет возможность создания и управления потоками выполнения. Он позволяет запускать функции в отдельных потоках, обеспечивая параллельное выполнение кода.
Обратите внимание, что после создания потока std::thread, вы должны вызвать join() или detach() для корректной обработки завершения потока.
В приведенном примере мы использовали join(), чтобы основной поток дождался завершения потока t1. Если вы вызываете join() после завершения потока, программа может выдать исключение или вызвать неопределенное поведение.
👀102
Сколько раз будет выполняться этот цикл?
Еще один вопрос с подвохом с IT-собеседований. Если бы вы сказали 300, а i был объявлен как int, вы были бы правы. Но поскольку i объявлен как unsigned char, правильный ответ – зацикливание (бесконечный цикл).
Объясняем. Выражение 2 * half_limit будет повышаться до int (на основе правил преобразования C++) и заимеет значение 300. Но так как i – это unsigned char, он пересматривается по 8-битному значению, которое после достижения 255 будет переполняться, поэтому вернется к 0, и цикл будет продолжаться вечно.
Еще один вопрос с подвохом с IT-собеседований. Если бы вы сказали 300, а i был объявлен как int, вы были бы правы. Но поскольку i объявлен как unsigned char, правильный ответ – зацикливание (бесконечный цикл).
Объясняем. Выражение 2 * half_limit будет повышаться до int (на основе правил преобразования C++) и заимеет значение 300. Но так как i – это unsigned char, он пересматривается по 8-битному значению, которое после достижения 255 будет переполняться, поэтому вернется к 0, и цикл будет продолжаться вечно.
👀97
Узнаем время работы программы на С++
Для того, чтобы найти время работы программы, нужно воспользоваться функцией clock(). Прототип функции clock() находится в заголовочном файле <ctime>, который нужно подключить.
Функция clock() возвращает значение времени в миллисекундах (1с = 1000млс). Причём отсчёт времени начинается с момента запуска программы.
Для того, чтобы найти время работы программы, нужно воспользоваться функцией clock(). Прототип функции clock() находится в заголовочном файле <ctime>, который нужно подключить.
Функция clock() возвращает значение времени в миллисекундах (1с = 1000млс). Причём отсчёт времени начинается с момента запуска программы.
👀93
Отличия 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.
#опытным
Кратко пробежимся по особенностям 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.
👀172
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.
#опытным
Ключевое слово, которое не 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.
👀88
Почему твоё резюме никто не смотрит?
А на твои отклики не приходит ответ? Или, что ещё хуже, тебе сразу отказывают?
Давай разберёмся.
Знаешь, как выглядит большинство резюме C++ разработчиков? Примерно так:
Выглядит знакомо? Увы, такие резюме обречены на провал.
Почему?
Парсеры. Машина просто проигнорирует такой текст. А значит, твоё резюме не попадёт в базу крупной компании или рекрутингового агентства.
Ручной отбор. Даже если компания проверяет резюме вручную, оно, скорее всего, окажется в самом конце списка кандидатов. А то и сразу получит отказ.
Конечно, и с таким резюме можно найти работу. Например, его может заметить начинающий HR, которому нужно срочно закрыть вакансию. Но факт остаётся фактом: шансы получить работу с таким резюме гораздо ниже.
А на твои отклики не приходит ответ? Или, что ещё хуже, тебе сразу отказывают?
Давай разберёмся.
Знаешь, как выглядит большинство резюме C++ разработчиков? Примерно так:
Январь 2024 — по настоящее время
ИТЦ "СамаяЛучшаяКампания" — Ведущий программист
- Разработка приложений для нужд производства
- Написание тестов
Май 2023 — Декабрь 2024
ОАО "ОтличнаяКомпания" — Старший инженер-программист
- Создание виджетов на Qt
- Написание документации
- Ведение переговоров с заказчиком
и так далее...
Выглядит знакомо? Увы, такие резюме обречены на провал.
Почему?
Парсеры. Машина просто проигнорирует такой текст. А значит, твоё резюме не попадёт в базу крупной компании или рекрутингового агентства.
Ручной отбор. Даже если компания проверяет резюме вручную, оно, скорее всего, окажется в самом конце списка кандидатов. А то и сразу получит отказ.
Конечно, и с таким резюме можно найти работу. Например, его может заметить начинающий HR, которому нужно срочно закрыть вакансию. Но факт остаётся фактом: шансы получить работу с таким резюме гораздо ниже.
👀100
std::clamp
#новичкам
Иногда у модулей приложения могут быть ограничения по диапазону значений. Например, скорость игрока. Как бы он не замедлялся, скорость не будет меньше нуля, и как бы он не ускорялся, она не будет больше max значения. Или координаты персонажа на ограниченном игровом поле.
И чтобы не городить подобных конструкций:
есть замечательная стандартная функция std::clamp. Она полностью инкапсулирует логику ограничения значения сверху и снизу:
Теперь ограничивать значения переменным проще, чем написать Hello, World.
Не самая широкоизвестная функция, однако круто, что в стандарте есть такие нишевые утилитарные инструменты. Они уже протестированы за вас, максимально обобщены и адекватно названы.
Вряд ли вы заходите в такие глубины стандарта и cppreference, чтобы std::clamp попалась вам на глаза. Поэтому и решил здесь рассказать про нее.
Don't reinvent the wheel. Stay cool.
#новичкам
Иногда у модулей приложения могут быть ограничения по диапазону значений. Например, скорость игрока. Как бы он не замедлялся, скорость не будет меньше нуля, и как бы он не ускорялся, она не будет больше max значения. Или координаты персонажа на ограниченном игровом поле.
И чтобы не городить подобных конструкций:
if (value < min) {
value = min;
}
if (value > max) {
value = max;
}есть замечательная стандартная функция std::clamp. Она полностью инкапсулирует логику ограничения значения сверху и снизу:
template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );
Теперь ограничивать значения переменным проще, чем написать Hello, World.
double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}Не самая широкоизвестная функция, однако круто, что в стандарте есть такие нишевые утилитарные инструменты. Они уже протестированы за вас, максимально обобщены и адекватно названы.
Вряд ли вы заходите в такие глубины стандарта и cppreference, чтобы std::clamp попалась вам на глаза. Поэтому и решил здесь рассказать про нее.
Don't reinvent the wheel. Stay cool.
👀88
Чек-лист: Линейные структуры данных в C++
Линейные структуры данных — фундамент программирования на C++. Правильный выбор структуры может значительно повысить эффективность вашего кода.
🎯 Векторы (std::vector)
✓ Используйте
✓ Применяйте
✓ Доступ по индексу выполняется за O(1) с помощью оператора []
✓ Используйте at() вместо [] для проверки границ массива
🎯 Списки (std::list)
✓ Отдавайте предпочтение при частых вставках/удалениях в середине
✓ Используйте
✓ Помните, что прямой доступ по индексу невозможен — только итерация
✓ Двунаправленные итераторы позволяют двигаться как вперед, так и назад
🎯 Очереди и стеки (std::queue, std::stack)
✓ Стек (LIFO): используйте
✓ Очередь (FIFO): применяйте
✓ Функция
✓ Обе структуры являются адаптерами и построены на других контейнерах
🎯 Массивы (std::array)
✓ Используйте для данных фиксированного размера, известного на этапе компиляции
✓ Более эффективны чем векторы для неизменяемых данных
✓ Поддерживают STL-алгоритмы (sort, find и др.)
✓ Проверяйте границы с функцией
Линейные структуры данных — фундамент программирования на C++. Правильный выбор структуры может значительно повысить эффективность вашего кода.
🎯 Векторы (std::vector)
✓ Используйте
reserve() для предварительного выделения памяти, когда примерно известен размер✓ Применяйте
push_back() для добавления элементов и pop_back() для удаления с конца✓ Доступ по индексу выполняется за O(1) с помощью оператора []
✓ Используйте at() вместо [] для проверки границ массива
🎯 Списки (std::list)
✓ Отдавайте предпочтение при частых вставках/удалениях в середине
✓ Используйте
splice() для эффективного перемещения элементов между списками✓ Помните, что прямой доступ по индексу невозможен — только итерация
✓ Двунаправленные итераторы позволяют двигаться как вперед, так и назад
🎯 Очереди и стеки (std::queue, std::stack)
✓ Стек (LIFO): используйте
push() для добавления и pop() для извлечения✓ Очередь (FIFO): применяйте
push() для добавления и pop() для извлечения✓ Функция
front() позволяет посмотреть первый элемент без удаления✓ Обе структуры являются адаптерами и построены на других контейнерах
🎯 Массивы (std::array)
✓ Используйте для данных фиксированного размера, известного на этапе компиляции
✓ Более эффективны чем векторы для неизменяемых данных
✓ Поддерживают STL-алгоритмы (sort, find и др.)
✓ Проверяйте границы с функцией
at() во избежание ошибок доступа👀204
const для неизменяемых данныхВ C++ использование
const помогает защитить данные от непреднамеренных изменений, делает код понятнее и позволяет компилятору оптимизировать работу программы.const, если переменная, параметр или метод не должны изменяться.Please open Telegram to view this post
VIEW IN TELEGRAM
👀104
Разница между std::stoi+std::to_string и std::from_chars+std::to_chars
#опытным
В C++ есть два основных подхода к конвертации чисел в строки и обратно:
Старомодный — std::stoi, std::to_string (C++11)
Модномолодежный — std::from_chars, std::to_chars (C++17)
В чем принципиальная разница между ними и когда какой подход использовать? Сегодня мы широкими мазками ответим на эти вопросы.
Особенности старомодного подхода:
👉🏿 Основное - это исключения. Все нештатные ситуации обрабатываются с их помощью, что ведет к неким накладным расходам.
👉🏿 Работа в высокоуровневом ООП стиле. Используются классы и возвращаются классы, без всяких сырых буферов.
👉🏿 Нет контроля над парсингом. Нет возможности задать формат, основание системы счисления или точность. Но для большинства кейсов это и не нужно.
👉🏿 Поддержка локалей. Грубо говоря, это механизм для учёта региональных особенностей представления данных. То есть std::stoi, std::to_string реализованы с учетом возможности спецификации локалей и соотвественно изменения результатов конвертации. С локалями возможна такая штука:
// В США (локаль "en_US"):
std::to_string(3.14); // "3.14" (точка как разделитель)
// В Германии (локаль "de_DE"):
std::to_string(3.14); // Может вернуть "3,14" (запятая)!
Естественно, что поддержка такой фичи чего-то да стоит.
Особенности модномолодежного подхода:
👉🏿 Функции std::from_chars, std::to_chars спроектированы быть настолько легкими и быстрыми, насколько это возможно на таком уровне абстракции.
👉🏿 Отсутствие намеренных динамических аллокаций. Только вы решаете, где расположена память по данные.
👉🏿 Отсутствие исключений. Функции возвращает объект ошибки, который явно нужно проверять руками.
👉🏿 Не проверяет локали.
👉🏿 Поддерживают частичный парсинг.
👉🏿 Поддержка явной гарантии round-trip. Если вы запишите в строку число с помощью std::to_chars и прочитаете его с помощью std::from_chars, то вы всегда получите изначальный результат. Главное, чтобы обе функции были вызваны с использованием одинаковой реализации стандартной библиотеки. Но у std::stoi, std::to_string и этого нет.
Если вы работаете в высоконагруженном или ограниченном по производительности окружении, то ваш выбор явно std::from_chars, std::to_chars. Обычно в коде таких приложений отказываются от использования исключений, поэтому проблем с код-стайлом не будет.
Возможность поэтапного парсинга также не оставляет выбора - используйте std::from_chars.
Если вы не паритесь за производительность, любите объекты, вам не нужен частичный парсинг или каждый раз при виде слова exception у вас не начинает идти пена изо рта, то придерживайтесь старого подхода.
Choose the right tool. Stay cool.
#опытным
В C++ есть два основных подхода к конвертации чисел в строки и обратно:
Старомодный — std::stoi, std::to_string (C++11)
Модномолодежный — std::from_chars, std::to_chars (C++17)
В чем принципиальная разница между ними и когда какой подход использовать? Сегодня мы широкими мазками ответим на эти вопросы.
Особенности старомодного подхода:
👉🏿 Основное - это исключения. Все нештатные ситуации обрабатываются с их помощью, что ведет к неким накладным расходам.
👉🏿 Работа в высокоуровневом ООП стиле. Используются классы и возвращаются классы, без всяких сырых буферов.
👉🏿 Нет контроля над парсингом. Нет возможности задать формат, основание системы счисления или точность. Но для большинства кейсов это и не нужно.
👉🏿 Поддержка локалей. Грубо говоря, это механизм для учёта региональных особенностей представления данных. То есть std::stoi, std::to_string реализованы с учетом возможности спецификации локалей и соотвественно изменения результатов конвертации. С локалями возможна такая штука:
// В США (локаль "en_US"):
std::to_string(3.14); // "3.14" (точка как разделитель)
// В Германии (локаль "de_DE"):
std::to_string(3.14); // Может вернуть "3,14" (запятая)!
Естественно, что поддержка такой фичи чего-то да стоит.
Особенности модномолодежного подхода:
👉🏿 Функции std::from_chars, std::to_chars спроектированы быть настолько легкими и быстрыми, насколько это возможно на таком уровне абстракции.
👉🏿 Отсутствие намеренных динамических аллокаций. Только вы решаете, где расположена память по данные.
👉🏿 Отсутствие исключений. Функции возвращает объект ошибки, который явно нужно проверять руками.
👉🏿 Не проверяет локали.
👉🏿 Поддерживают частичный парсинг.
👉🏿 Поддержка явной гарантии round-trip. Если вы запишите в строку число с помощью std::to_chars и прочитаете его с помощью std::from_chars, то вы всегда получите изначальный результат. Главное, чтобы обе функции были вызваны с использованием одинаковой реализации стандартной библиотеки. Но у std::stoi, std::to_string и этого нет.
Если вы работаете в высоконагруженном или ограниченном по производительности окружении, то ваш выбор явно std::from_chars, std::to_chars. Обычно в коде таких приложений отказываются от использования исключений, поэтому проблем с код-стайлом не будет.
Возможность поэтапного парсинга также не оставляет выбора - используйте std::from_chars.
Если вы не паритесь за производительность, любите объекты, вам не нужен частичный парсинг или каждый раз при виде слова exception у вас не начинает идти пена изо рта, то придерживайтесь старого подхода.
Choose the right tool. Stay cool.
👀93