Грокаем C++
8.37K 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
Лямбды без захвата
#опытным

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

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

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

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