Грокаем C++
前往频道在 Telegram
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
显示更多9 383
订阅者
-224 小时
+27 天
+1030 天
帖子存档
9 385
Зачем в pimpl определять специальные методы в cpp?
#опытным
В прошлом посте вы могли заметить, что все специальные методы класса Serializer только объявляются в хэдере, а реализуются в файле исходников. Зачем так делать?
class Serializer {
public:
Serializer();
~Serializer();
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;
//...
private:
std::unique_ptr<struct Impl> pimpl_;
};
Мы ведь привыкли либо вообще не определять никакие специальные методы по правилу нуля, либо просто помечать их default и все. Почему в этот раз так нельзя сделать?
Все дело в нашем особенном указателе pimpl_. Это умный указатель на forward-объявленную структуру. Это значит, что мы не знаем примерно ничего об этой структуре, кроме ее имени. И в этом загвоздка. Компилятор на данный момент не знает, как выглядят ее деструктор и перемещающие методы.
Когда мы явно помечаем специальные методы класса как default, мы явно просим компилятор за нас сгенерировать определение этих методов.
class Serializer {
public:
Serializer();
~Serializer() = default; // Here
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept = default; // Here
Serializer& operator=(Serializer&&) noexcept = default; // Here
//...
private:
std::unique_ptr<struct Impl> pimpl_;
};
То есть мы запрашиваем определение метода в теле класса. На основе чего компилятор будет генерировать их? На основе того, что есть в теле класса.
Но вот беда, в теле класса нет никакой информации о том, что из себя представляет класс Impl. Возьмем тот же деструтор. По дефолту std::unique_ptr вызывает просто delete ptr . А у delete expression есть пометка, что при удалении указателя на неполный тип программа становится ill-formed. Поэтому компиляторы на случай, если вы захотите сгенерировать деструктор std::unique_ptr с неполным типом, ставят такой ассерт времени компиляции: static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type");. А компиляция последнего примера крашнется с соотвествующей ошибкой.
Так что определяйте специальные методы в реализации при использовании pimpl и будет вам счастье.
Don't give impossible tasks. Stay cool.
#cppcore #design9 385
🚀 Почему пользователи платят $30 за простой конвертер картинок? Давайте разберемся.
Кирилл из комьюнити создал простой продукт всего за один месяц, внедрив метод, которой уже принес результат.
Как он это сделал:
1️⃣ Анализ спроса: через поисковые запросы он увидел, что много людей ищут конвертацию «HEIC to JPG».
2️⃣ Фокус на главном: удалил всё лишнее, оставив только функцию конвертации в один клик.
3️⃣ Скорость внедрения: за 30 дней сделал запуск без перфекционизма.
Посты про конвертер (ч.1, ч.2)
Что из этого получилось:
— Доход $500 в месяц с тенденцией роста.
— Более $10K заработано на конвертере.
— $0 на рекламу, всего $40 в месяц на сервер.
Выводы:
— Люди платят за удобство, даже если есть бесплатные аналоги.
— Продукт функционирует автоматически с минимальной поддержкой.
— Быстрая и экономичная проверка идеи может быть успешной.
Результаты из комьюнити билдеров:
— Более 400 запусков по этой методике.
— Некоторые продукты уже набрали от 50К до 100К+ пользователей.
Присоединяйтесь к @its_capitan — следите за процессом разработки, продвижения и узнайте, сколько можно заработать на таких микро-продуктах.
Реклама: ИП Зуев Игорь Владимирович, ИНН: 360408359441, Erid: 2Vtzqv5Bavz
9 385
Modern pimpl идиома
#новичкам
В одном из давнишних постов мы уже обсуждали базовую формулировку и реализацию идиомы pimpl. Это сырой указатель на forward-объявленную структуру.
В современных плюсах, естественно, никто уже так не делает. Сырые указатели уходят в прошлое и старые подходы пересматриваются с применением умных указателей.
Допустим, вы используете какой-то нестандартный формат сериализации данных, например apache avro. Конечно вы хотите написать свою библиотеку сериализации, которая позволить конвертировать данные в/из avro формата. Как должна выглядеть эта библиотека?
Ну скорее всего вы хотите использовать только доменные сущности в интерфейсе библиотеки. В этом случае только эта библиотека зависит от стороннего решения и вы в любой момент смогли, например, изменить формат сериализации, если у вас изменились бизнес требования. Также приятно иметь стабильный интерфейс и линковать стороннее решение только с этой библиотекой.
С такими симптомами ваш терапевт прописал вам однократный прием pimpl идиомы. Выглядеть это может так:
// serializer.hpp
class Serializer {
public:
Serializer();
~Serializer();
// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;
std::vector<uint8_t> serializeOrder(const domain::Order& person);
domain::Order deserializeOrder(const std::vector<uint8_t>& data);
private:
std::unique_ptr<struct Impl> pimpl_; // Smart pointer on forward-declared struct
};
// serializer.cpp
struct Serializer::Impl {
avro::GenericRecord convertToAvro(const domain::Order&);
domain::Order convertFromAvro(const avro::GenericRecord&);
};
Serializer::Serializer()
: pimpl_(std::make_unique<Impl>()) {
}
Serializer::~Serializer() = default;
Serializer::Serializer(Serializer&&) noexcept = default;
Serializer& Serializer::operator=(Serializer&&) noexcept = default;
std::vector<uint8_t> Serializer::serializeOrder(const domain::Person& person) {
auto record = pimpl_->convertToAvro(person);
return {...};
}
// deserializeOrder
Основная идея - все упоминания и детали реализации avro находятся в cpp и не торчат наружу. Это достигается за счет использования forward-declared класса в хэдэре и определение этого класса в сорцах. За счет этого мы имеем стабильный интерфейс, стабильный ABI и сокращенное время линковки. А за счет использования std::unique_ptr у нас тривиальные реализации всех специальных методов.
Примерно так это и выглядит в современных плюсах. Но это еще не конец для pimpl. Есть решение, которое поможем вам избежать динамических аллокаций. Но об этом в другой раз.
Hide details. Stay cool.
#design9 385
Разница между 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.
#cppcore #cpp11 #cpp17
9 385
🖼️Google - Умирает!
На все уже отвечает ChatGPT, но не справляется с кодом!
В этом тебе поможет канал айтишника. Там разработчик с 11+ лет опыта показывает как пользоваться нейросетями и делится полезными статьями:
🖼️Статьи для Python разработки
👩💻Статьи для Frontend разработки
🖼️Статьи для тестировщиков
👩💻Статьи для Apple разработки
👩💻Статьи для Android разработки
👩💻Статьи для PHP
👩💻Статьи для DevOps инженеров
👩💻Статьи для Java разработки
🖼️Статьи для Go разработки
🖼️Статьи для инженеров безопасности
🖼️Статьи для разработчиков AAA игр
👩💻Статьи для C# разработки
🖼️Статьи для C/C++ разработки
Заходите, таких каналов больше нет: @ITVlad
9 385
std::to_chars
#новичкам
В C++17 появилась не только функция для парсинга (std::from_chars), но и её обратная версия — std::to_chars, которая позволяет конвертировать числа (int, float, double и др.) в строки без дополнительных затрат на выделение памяти и поддержку исключений.
std::to_chars_result to_chars(char* first, char* last,
IntegerType value,
int base = 10); // (1)
std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt); // (2)
std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt,
int precision); // (3) (since C++23)
struct to_chars_result {
const char* ptr;
std::errc ec;
};
Единственный случай, когда эта функция может зафейлиться - если вы передали слишком маленький буффер. Тогда ec выставляется в std::errc::value_too_large, а ptr в last.
В чем преимущество функции по сравнению со старой-доброй std::to_string?
💥 Можно задать точность и основание системы счисления.
💥 Отсутствуют динамические аллокации внутри функции.
💥 Возможность использовать любые char буферы, а не только строки.
Вот вам пример работы:
char buffer[20];
int value = 12345;
auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buffer;
std::string_view str_result(buffer, result_length);
std::cout << "Result: " << str_result << "\n"; // "12345"
}
// ------------------------
double pi = 3.1415926535;
char buf[20];
auto result = std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::fixed, 4);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buf;
std::string_view str_result(buf, result_length);
std::cout << "Result: " << str_result << "\n"; // "3.1416"
}
Не всегда проблема точности и аллокаций - это реальная проблема. Но если вы работаете с ограниченной кучей и хотите стандартное средство сериализации числа в буфер - std::to_char как раз для вас.
Be efficient. Stay cool.
#cpp179 385
std::from_chars
#новичкам
С++17 нам принес новую прекрасную функцию парсинга строк в числа - std::from_char.
std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
IntegerType& value, // Куда записать результат
int base = 10 // Система счисления (2-36)
);
std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
FloatType& value, // Куда записать результат
std::chars_format fmt = std::chars_format::general // Формат плавающей точки
);
На самом деле это два семейства перегрузок функций для целых чисел и чисел с плавающей точкой.
Задача функции - максимально быстро, безо всяких накладных расходов на выделение памяти и поддержку исключений, распарсить строку в число арифметического типа.
Функция возвращает структуру std::from_chars_result:
struct from_chars_result {
const char* ptr; // Указатель на первый НЕпрочитанный символ
std::errc ec; // Код ошибки (если успех — std::errc())
};
Если парсинг удался и какая-то часть строки конвертировалась в число, то в ptr находится указатель на первый символ, на котором парсинг завершился. Если вся строка была интерпретирована, как число, то в ptr находится last указатель.
Если парсинг неудался, то ptr равен first, а код ошибки ec выставляется в std::errc::invalid_argument.
"123" → удачно распарсили все → ptr == last (конец строки). "123abc" → распарсили "123" → ptr указывает на 'a'. "abc" → ошибка → ptr == first (начало строки).Примеры работы:
const std::string str = "42abc";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
if (res.ec == std::errc()) {
std::cout << "Value: " << value << "\n"; // 42
std::cout << "Remaining: " << res.ptr << "\n"; // "abc"
}
// ----------------
const std::string str = "xyz";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(res.ec == std::errc::invalid_argument);
assert(res.ptr == str.data()); // ptr остался на начале
К тому же функция может детектировать переполнение:
const std::string str = "99999999999999999999";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(res.ec == std::errc::result_out_of_range);
В чем главный прикол этой функции?
Помимо отсутствия накладных расходов, это последовательный парсинг. Если у вас есть строка с последовательностью чисел, разделенных запятой, то вы просто в цикле можете передвигать нужные указатели и парсить числа одно за другим. Тот же std::stoi выкинул бы исключение и пошел пиво пить:
const std::string str = "123,456,789";
std::vector<int> numbers;
const char* current = str.data();
const char* end = str.data() + str.size();
while (current < end) {
int value;
auto res = std::from_chars(current, end, value);
if (res.ec != std::errc()) {
std::cerr << "Parsing error!\n";
break;
}
numbers.emplace_back(value);
current = res.ptr; // Сдвигаем указатель
// Пропускаем разделитель (запятую)
if (current < end && *current == ',') {
++current;
}
}
for (int num : numbers) {
std::cout << num << " ";
}
// Вывод: 123 456 789
К тому же ее целочисленный вариант с С++23 constexpr, что позволить вам парсить строку в числа даже во время компиляции.
Если вы не любите исключения - std::from_char ваш выбор.
Be efficient. Stay cool.
#cpp17 #cpp239 385
📈Хотите научиться асинхронному программированию на C++ и создавать высокоэффективные приложения?
На открытом уроке вы изучите основы асинхронного программирования с использованием Boost.Asio — популярной библиотеки для C++. Мы разберем, как эффективно обрабатывать сетевые события, не блокируя основной поток, а также как создавать масштабируемые и отзывчивые приложения.
Вы узнаете, как работать с сетевыми соединениями, таймерами и асинхронными операциями, чтобы повысить производительность приложений. Кроме того, вы получите практические советы по безопасному написанию асинхронного кода с используемым API.
👉Запишитесь на вебинар в преддверие старта курса «C++ Developer. Professional» и получите скидку на обучение. Встречаемся 17 июня в 20:00 МСК, регистрация открыта: https://otus.pw/UmaZi/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
9 385
std::mem_fn
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
struct Task {
int execute() {
return 42;
}
};
std::vector<Task> tasks(10);
std::vector<int> results;
Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
results.reserve(tasks.size());
for (auto& task: tasks) {
result.emplace_back(task.execute());
}
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Use standard algorithms where appropriate, instead of writing some own implementation.
Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
std::vector results = tasks |
std::views::transform(&Task::execute) |
std::ranges::to<std::vector>();
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), &Task::execute); // Not working!
Какие варианты у нас есть в такой ситуации?
❗️ std::function
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::function<int(Task&)>(&Task::execute));
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), [](auto& input) { return input.execute(); });
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так (x.*&Item::Foo)(), а вот так: (&Item::Foo)(x). То есть позволяет унифицировать синтаксис вызова указателя на метод класса с синтаксисом вызова обычной функции. С помощью std::mem_fn наш код трансформации выглядит вот так:
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::mem_fn(&Task::execute));
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL9 385
Лямбды без захвата
#опытным
Сегодня немного проясню ситуацию с лямбдами без захвата.
Повторю предыдущий пост, лямбды без захвата - это объекты с определенным оператором приведения к указателю на функцию. Так как лямбды ничего не захватывают, то их замыкания не содержат полей, поэтому, в целом, для их вызова необязателен объект. Но как именно это реализовано в лямбдах? Сейчас и посмотрим.
Давайте взглянем на простую лямбду:
auto fun = [](int i) { return i*2;};
У нее очевидно нет захвата, поэтому она как раз наш абонент. С помощью cppinsights можно посмотреть под капот компилятора и то, во что лямбды и другие сущности превращаются. Примерно вот такую сущность сгенерируют компилятор для этой лямбды:
class __lambda_11_15
{
public:
inline int operator()(int i) const
{
return i * 2;
}
using retType_11_15 = int (*)(int);
inline constexpr operator retType_11_15 () const noexcept
{
return __invoke;
}
private:
static inline /*constexpr */ int __invoke(int i)
{
return __lambda_11_15{}.operator()(i);
}
};
И тут много интересного!
Например, у лямбды без захвата все же генерируется оператор вызова operator().
В добавок к этому определяется оператор приведения к указателю на функцию operator retType_11_15 (), который фактически приводит приватный статический метод класса к указателю. А для переиспользования кода, статический метод на лету конструирует объект и вызывает у него operator().
То есть вот примерно как это работает:
int apply_function(int (*func)(int), int value) {
return func(value); // Вызываем переданную функцию
}
int main() {
auto fun = [](int i) { return i2;};
fun(42);
apply_function(fun, 42);
return 0;
}
Здесь у нас 2 вида использования лямбды: через объект замыкания и через коллбэк в apply_function. Посмотрим, что будет вызываться в каждом конкретном случае:
int main()
{
__lambda_11_15 fun = __lambda_11_15{};
fun.operator()(42);
apply_function(fun.operator __lambda_11_15::retType_11_15(), 42);
return 0;
}
В случае вызова через объект замыкания триггерится operator(), а при передаче в другую функцию - оператора приведения к указателю на функцию.
Зачем два способа вызова? Почему нельзя обойтись просто приведением к указателю на функцию?
Вызывать лямбду через указатель на функцию - это лишить себя основной оптимизации компилятора - инлайнинга. Если передавать лямбду, как полноценный тип замыкания, то компилятор будет знать, как встроить код его operator() внутрь callee, потому что все типы определены на этапе компиляции. А по указателю на функцию можно передать все, что угодно. В простых случаях, как в apply_function, может и все хорошо будет. Но в более сложных - вы лишитесь оптимизации.
Но благо указатели на функции используются преимущественно в сишном апи и по-другому просто не получится. В С++ есть шаблоны и объекты и надобность в использовании указателей на функции практически отпадает.
Надеюсь, теперь вы чуть больше о лямбдах знаете)
Know more. Stay cool.
#cppcore #cpp119 385
std::exchange
#опытным
В прошлом посте мы пасхалкой использовали std::exchange, давайте же разберем эту функцию по-подробнее.
По названию в целом понятно, что она делает - что-то обменивает. Но не как std::swap, меняет значения местами. Все немного хитрее.
Она заменяет старое значение новым и возвращает старое значение. Вот примерная реализация:
template<class T, class U = T>
constexpr // Since C++20
T exchange(T& obj, U&& new_value) {
T old_value = std::move(obj);
obj = std::forward<U>(new_value);
return old_value;
}
Как говорил Константин Владимиров: "единожды научившись использовать std::exchange, вы дальше будете делать std::exchange всю оставшуюся жизнь".
Понятное дело, что это не какой-нибудь std::move, который реально постоянно приходится использовать. Однако важен сам паттерн. Если посмотреть в кодовые базы, то будет куча мест, где можно использовать эту функцию. Приведу пару примеров работы std::exchange, чтобы вы поняли смысл.
Начнем со знакомого:
auto gen = [current = start, step]() mutable {
return std::exchange(current, current + step);
};
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen);
Мы определяем мутабельную лямбду(кстати это один из удачных примеров использования таких лямбд), которая может изменять свои захваченные по значению переменные current и step. Дальше на каждом вызове мы должны вернуть текущее значение current, но перед этим как-то его увеличить. Можно использовать прокси-переменную:
int val = current;
current += step;
return val;
Но зачем, если у нас уже есть готовая и протестированная функция std::exchange? Это прекрасный способ немного уменьшить код и увеличить его читаемость.
Другой пример - генерация чисел Фибоначчи:
auto gen = [current = 0, next = 1]() mutable {
return current = std::exchange(next, current + next);
};
std::vector<int> fib(10);
std::generate(fib.begin(), fib.end(), gen);
for (int i = 0; i < fib.size(); ++i) {
std::cout << fib[i] << " ";
}
// OUTPUT:
// 1 1 2 3 5 8 13 21 34 55
Хорошенько вдумайтесь и осознайте, что здесь происходит. Ну ведь красиво, правда?)
Если у вас есть вектор коллбэков, который вы постепенно копите и в какой-то момент обрабатываете все разом. Коллбэки безопасно выполнять вне лока. Но как их нормально обработать разом, чтобы 100500 раз не дергать мьютексы? На такой случай есть прикольная техника. Нужно под локом получить копию, а обрабатывать ее вне лока.
class Dispatcher {
// ...
// All events are dispatched when we call process
void process() {
const auto tmp = [&] {
std::lock_guard lock{mutex_};
return std::exchange(callbacks_, {});
}();
for (const auto& callback : tmp) {
std::invoke(callback);
}
}
};
И все. Копию(на самом деле перемещенную копию) получаем под локом, а обрабатываем все без замков. Круто!
Ну и конечно, без принципа exchange не обойтись в lock-free программировании. Та же std::atomic_exchange работает ровно по той же логике.
Прикольная функция. Постарайтесь замечать самые банальные кейсы ее применения и со временем вы будет глубже ее понимать и использовать в более интересных ситуациях.
Be elegant. Stay cool.
#cppcore #cpp20 #multitasking #fun9 385
Хотите научиться эффективно отлаживать C++ код и устранять ошибки?
⏺️ На открытом вебинаре вы узнаете:
▸ Как искать и устранять ошибки в C++ с помощью таких инструментов, как assert'ы, логирование и юнит-тесты.
▸ Мы покажем, как использовать отладчик и почему core dump может стать вашим другом.
▸ Также познакомим вас с powerful инструментами, такими как address sanitizer и valgrind, которые помогут найти ошибки в самых сложных случаях.
❗️ Сформируйте свою «аптечку» инструментов и приемов, которые не только помогут вам быстрее находить баги, но и улучшат надежность кода на C++. Этот урок даст вам важные практические знания, которые пригодятся на всех этапах разработки.
📆 Посетите открытый урок 9 июня в 20:00 МСК в преддверие старта курса «C++ Developer» и получите скидку на обучение!
Регистрация уже открыта: https://otus.pw/K3AX/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
9 385
Все вызываемые сущности в С++. Ч2
#опытным
Продолжаем перечислять все сущности, которые можно "вызывать" в С++.
И начинаем с продолжения перечисления всего, что относится к функторам:
👉🏿👉🏿 Лямбды
Там, где говорят про функторы, всегда водятся лямбды. Лямбды - это такие карманные функциональные объекты, которые определяются на ходу. У них также определен соответствующий оператор вызова operator(). Вот как может выглядеть код из предыдущего пункта с помощью лямбды:
int start = 5;
int step = 2;
auto gen = [current = start, step]() mutable {
return std::exchange(current, current + step);
};
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen);
Накидайте огней, если хотите узнать, как 3 строчки из лямбды в предыдущем посте превратились в одну с помощью std::exchange)
👉🏿👉🏿 Объект класса с определенным оператором приведения к указателю на функцию
Солидная часть подписчиков даже не слышала о такой сущности. Но работает это тривиально: при "вызове" такого объекта он кастится к указателю на функцию и уже с его помощью реально делается вызов:
class FunctionObjectCast {
public:
using fun_ptr = int ()(int);
// Оператор приведения к указателю на функцию
operator fun_ptr() {
return &staticMethod;
}
// Статический метод, который будет вызываться через указатель
static int staticMethod(int x) {
return x * 2;
}
};
FunctionObjectCast obj;
obj(42); // cast here
Но даже, если вы ни разу не слышали про функторы такого вида, то с высокой долей вероятности все равно неявно пользовались им. Это конечно же лямбды без захвата.
👉🏿👉🏿 Лямбды без захвата. Именно с помощью оператора приведения к указателю на функцию лямбды без захвата можно маскировать под указатели на функции. Это позволяет использовать их в качестве коллбэков к сишным функциям или колдовать такую магию.
std::vector<int> nums = {5, 3, 9, 1};
std::qsort(nums.data(), nums.size(), sizeof(int),
[](const void* a, const void* b) -> int {
return ((int)a - (int)b);});
std::qsort принимает последним параметром именно указатель на функцию, поэтому конкретно в этом кейсе и срабатывает приведение к этому указателю.
В случае привычного вызова самой лямбды используется operator().
👉🏿 Нестатический метод
struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};
Handler{}.foo(42);
👉🏿 Указатель на нестатический метод. Если вы хотите передать метод класса в качестве коллбэка, то вам необходимо это делать через указатель на метод. Метод класса - это такая же функция, только первым параметром она неявно принимает this. Если делать явный вызов метода через указатель на него, то эта неявная передача становится явной:
struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};
auto non_static_method_ptr = &Handler::foo;
std::invoke(non_static_method_ptr, Handler{}, 42);
Чтобы вызвать метод класса по его указателю через std::invoke нужно вторым параметром передать объект класса, на котором мы хотим вызвать метод. Далее идут аргументы в порядке, определенном в методе.
Замечу, что использования оператора взятия адреса(&) перед именем метода здесь обязательно. Имена нестатических методов не имеют желания неявно приводится к указателям, как имена функций. Вот так нельзя:
std::invoke(Handler::foo, Handler{}, 42);
Это синтаксис для использования статических методов и компилятор будет ругаться.
👉🏿 Указатель на поле класса. Недавно обсуждали, что с помощью std::invoke можно "вызывать" поля класса. Неочевидно, но бывает полезно.
struct Payment {
double amount;
std::string category;
}
Payment p{100500, "confetki"};
std::cout << std::invoke(&Payment:amount, p) << std::endl;
// OUTPUT:
// 100500
Фух. Вроде все. Пишите, если что забыл.
Have all the tools. Stay cool.
#cppcore9 385
Все вызываемые сущности в С++. Ч1
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
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_ptr(42); (*fun_ptr)(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.
#cppcore9 385
Как разработчику или менеджеру в IT запустить свой пет проект?
Сидеть и работать в корпорации – страшно, жизнь-то мимо проходит. Уходить строить бизнес – страшно, а вдруг прогорит.
Один из вариантов – пилить свой проект по вечерам. Ведь многие успешные компании, типа Twitter, начинались как Пет проекты.
Мы не говорим, что пет проект обязательно заработает миллиарды. Но заработать денег больше, чем в найме, вполне реально, а уж опыта получить точно больше можно.
Но есть куча проблем и вопросов:
- Как выбрать идею для пет проекта?
- Что нужно знать про маркетинг?
- Как запуститься и довести до первых продаж не имея бюджета на рекламу?
Михаил Табунов сделал про все это телеграм канал Твой пет проект, в котором пишет для разработчиков и менеджеров про свой опыт – где брать идею, что надо знать про маркетинг, как запускать и искать первые 10 клиентов, и многое другое.
Так что подписывайтесь на Твой пет проект, получайте пользу от практиков рынка.
9 385
Ответ
Мы прям недавно обсуждали, что std::invoke позволяет вызвать указатель на метод класса, это у вас не должно было вызвать вопросов. Самая загвоздка вот тут:
auto fieldPtr = &Data::field;
std::invoke(fieldPtr, Data{});
fieldPtr здесь - это указатель на нестатическое поле класса.
Скорее всего у вас возникли такие вопросы: "Что значит вызвать поле класса?! Это вообще легально?".
И ответ на этот вопрос довольно контринтуитивный. Да, std::invoke помогает единообразно вызвать все похожие на функции сущности. То есть запускать на выполнение код. Но поле класса - это вообще говоря не код, а участок памяти. А указатель на поле класса - это оффсет от начала объекта. На функции это вообще не похоже.
Тем не менее вызов поля класса с помощью std::invoke - легальная операция. И ее результат - значение этого поля.
Странно? Безусловно. Есть ли у этого применения? Конечно!
В C++20 алгоритмах библиотеки ranges есть специальный параметр проекции. Это вызываемая сущность, которая помогает преобразовывать элементы диапазона перед обработкой.
Допустим мы хотим найти в диапазоне объект с максимальным значением определенного поля. Как бы мы это делали без диапазонов:
struct Payment {
double amount;
std::string category;
}
std::vector<Payment>
payments = {{100.0, "food"}, {200.0, "transport"}, {150.0, "food"},
{300.0, "entertainment"}, {50.0, "transport"}, {250.0, "food"},
{120.0, "food"}};
auto max = *std::max_element(
transactions.begin(), transactions.end(),
[](const auto& item1, const auto& item2) { return item1.amount < item2.amount; });
Классика: определяем кастомный компаратор для сравнения элементов. Но заметьте сколько кода повторяется. Не легче ли просто один раз указать, что сравнивать надо по полю amount? И библиотека диапазонов позволяет нам это сделать!
auto max = *std::ranges::max_element(payments, {}, [](const auto& elem){return elem.amount;});
Последний параметр проекции позволяет нам сказать, как нужно преобразовывать элементы последовательности перед сравнениями.
Но это все еще не идеал. Лямбда здесь кажется оверкиллом. Вот здесь-то вызов поля класса и пригождается:
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);
Вот и все. Просто и красиво.
Под капотом алгоритмы рэнджей обязаны использовать std::invoke, чтобы универсально вызывать все переданные коллбэки. Поэтому такой финт ушами работает в них работает. И не работает в привычной STL.
Пользуйтесь диапазонами и проекторами. Это полезные штуки, которые ощутимо упрощают код.
Be laconic. Stay cool.
#cpp20 #STL9 385
Что выведется на консоль в результате попытки компиляции и запуска следующего кода?
9 385
Квиз
#опытным
В тему std::invoke закину вам интересный #quiz. Будем вызывать ведьм мемберы класса.
Что выведется на консоль в результате попытки компиляции и запуска следующего кода?
#include <functional>
#include <iostream>
struct Data {
int memberFunction(int value) {
return value;
}
int field = 42;
};
int main() {
Data data;
auto methodPtr = &Data::memberFunction;
auto fieldPtr = &Data::field;
std::cout << std::invoke(methodPtr, data, std::invoke(fieldPtr, data)) << std::endl;
}
Explore details. Stay cool.9 385
std::invoke
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
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)...);
}
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#cppcore #template9 385
Наша команда Mobile SDK сделала решение, которое позволяет из Flutter-приложения напрямую вызывать C++ код. Под капотом:
➡️ FFI для прямого взаимодействия с C++ кодом ядром;
➡️ Кодогенерируемое API почти полностью аналогично iOS и Android Mobile SDK;
➡️ TextureWidget вместо PlatformView для рендеринга карты;
➡️ Единые виджеты для отображения карты как для Android, так и для iOS.
Если тебе нравится разбираться в архитектуре SDK — заглядывай читать. Детально рассказываем про основу продукта — кодогенератор для генерации платформенного Dart-кода на основе C++ интерфейсов.
现已上线!2025 年 Telegram 研究 — 年度关键洞察 
