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

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
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 #fun
Лямбды без захвата
#опытным

Сегодня немного проясню ситуацию с лямбдами без захвата.

Повторю предыдущий пост, лямбды без захвата - это объекты с определенным оператором приведения к указателю на функцию. Так как лямбды ничего не захватывают, то их замыкания не содержат полей, поэтому, в целом, для их вызова необязателен объект. Но как именно это реализовано в лямбдах? Сейчас и посмотрим.

Давайте взглянем на простую лямбду:

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 #cpp11
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 #STL
📈Хотите научиться асинхронному программированию на C++ и создавать высокоэффективные приложения?

На открытом уроке вы изучите основы асинхронного программирования с использованием Boost.Asio — популярной библиотеки для C++. Мы разберем, как эффективно обрабатывать сетевые события, не блокируя основной поток, а также как создавать масштабируемые и отзывчивые приложения.

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

👉Запишитесь на вебинар в преддверие старта курса «C++ Developer. Professional» и получите скидку на обучение. Встречаемся 17 июня в 20:00 МСК, регистрация открыта: https://otus.pw/UmaZi/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
​​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 #cpp23
​​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.

#cpp17
🖼️Google - Умирает!

На все уже отвечает ChatGPT, но не справляется с кодом!

В этом тебе поможет канал айтишника. Там разработчик с 11+ лет опыта показывает как пользоваться нейросетями и делится полезными статьями:

🖼Статьи для Python разработки
👩‍💻Статьи для Frontend разработки
🖼️Статьи для тестировщиков
👩‍💻Статьи для Apple разработки
👩‍💻Статьи для Android разработки
👩‍💻Статьи для PHP
👩‍💻Статьи для DevOps инженеров
👩‍💻Статьи для Java разработки
🖼Статьи для Go разработки
🖼️Статьи для инженеров безопасности
🖼️Статьи для разработчиков AAA игр
👩‍💻Статьи для C# разработки
🖼️Статьи для C/C++ разработки

Заходите, таких каналов больше нет: @ITVlad
Please open Telegram to view this post
VIEW IN TELEGRAM
​​Разница между 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
​​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.

#design