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

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
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
26👍16🔥11❤‍🔥2
Capture this
#новичкам

Бывают ситуации, когда вы хотите зарегистрировать коллбэк, в котором будет выполняться метод текущего класса.

Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку graceful shutdown. Для этого получаем объект обработчика сигнала и регистрируем коллбэк на SIGINT и SIGTERM:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [](){
Shutdown();
});
}

void Shutdown() {...}
};


Сработает ли такой код?

Он не соберется. Будет ошибка: 'this' was not captured for this lambda function. Методу Shutdown нужен объект, на котором его нужно вызвать.

Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [this](){
// this->Shutdown(); - don't need this syntax
Shutdown();
});
}

void Shutdown() {...}
};


До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:

SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [= /*& also works*/](){ // works
Shutdown();
});


Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.

Be explicit. Stay cool.

#cppcore #cpp11 #cpp20
👍235❤‍🔥4🔥2👏1
​​std::to_address
#опытным

В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.

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

Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение: obj.operator->(). Однако для простых указателей это не прокатит: они не классы и у них нет методов. Да и не прокатит для любых других объектов, у которых не определен этот оператор. Что делать?

Использовать C++20 функцию std::to_address! Вот ее примерная реализация:

template<class T>
constexpr T* to_address(T* p) noexcept {
static_assert(!std::is_function_v<T>);
return p;
}

template<class T>
constexpr auto to_address(const T& p) noexcept {
if constexpr (requires{ std::pointer_traits<T>::to_address(p); })
return std::pointer_traits<T>::to_address(p);
else
return std::to_address(p.operator->());
}


То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().

Обычно эта функция требуется для вызова сишного апи в обобщенном коде:

void c_api_func(const int*);

template<typename T>
void call_c_api_func(T && obj) {
c_api_func(std::to_address(obj));
}

std::vector<int> data{10, 20, 30};
call_c_api_func(data.begin()); // works
auto ptr = std::make_unique<int>(42);
call_c_api_func(ptr); // works
call_c_api_func(ptr.get()); // also works


Use the right tool. Stay cool.

#cpp20 #cppcore
224👍10🔥7😁3
constexpr функции сквозь года
#новичкам

constexpr бывают не только переменные, но и функции. Такие функции могут быть выполнены, как в compile-time, так и в runtime, в зависимости от контекста вызова:
- Если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции.
- Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime.
- Если вы попытаетесь присвоить возвращаемое значение функции с runtime аргументом constexpr переменной, то получите ошибку компиляции.

constexpr int square(int x) {
return x * x;
}

int main() {
constexpr int val = square(5); // compile-time calculations
static_assert(val == 25, "Must be 25"); // compile-time check
int arg = rand() % 25;
int res = square(arg); // runtime calculations
assert(res == arg*arg); // runtime check
constexpr int fail = square(arg); // compile error here!
}


Пройдемся по стандартам языка и посмотрим, как в них изменялись constexpr функции.

В С++11 можно было использовать только однострочные constexpr функции с сильными ограничениями.


constexpr int factorial(int n) {
// ternary operator is allowed, so recursion
return (n <= 1) ? 1 : n * factorial(n - 1);
}


В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:

constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

static_assert(factorial(5) == 120, "Must be 120");


C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:

constexpr auto factorial = [](int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
};

static_assert(factorial(5) == 120, "Must be 120");


C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.

constexpr std::string create_greeting() {
std::string s = "Hello, ";
s += "C++20!";
return s;
}

static_assert(create_greeting() == "Hello, C++20!");


constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.

В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).

Free up time for important things. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥31👍126👎2
​​constexpr vs consteval функции
#опытным

В С++20 добавили новый спецификатор - consteval. Отдельно его интро не особо интересно разбирать, поэтому попробуем в сравнении с constexpr.

consteval может быть применен только к функциям и это ключевое слово заставляет функцию выполняться во время компиляции и возвращать константное выражение. Отсюда и название спецификатора "константное вычисление"(constant evaluation).

Вот пара простых примеров:

consteval int sum_consteval(int a, int b) {
return a + b;
}

constexpr int sum_constexpr(int a, int b) {
return a + b;
}

int main() {
constexpr auto c = sum_consteval(100, 100);
static_assert(c == 200);

constexpr auto c1 = sum_constexpr(100, 100);
static_assert(c1 == 200);

constexpr auto val = 10;
static_assert(sum_consteval(val, val) == 2*val);

int a = 10;
int res = sum_constexpr(a, 10); // fine with constexpr function
int res1 = sum_consteval(10, 10);

// int res2 = sum_consteval(a, 10); // error!
// the value of 'a' is not usable in a constant expression
}


Мы можем использовать оба спецификатора constexpr и consteval для того, чтобы инициализировать compile-time константы и обычные переменные.

Однако consteval функции могут вызываться только с constant expression в качестве аргументов. При попытке передачи в них неконстантного выражения будет ошибка компиляции.

Итого:

👉🏿 спецификатор consteval может быть применен только для функций

👉🏿 constexpr может быть применен и для переменных

👉🏿 consteval заставляет компилятор вычислять выражение на этапе компиляции. Если хотя бы один аргумент такой функции не является константным выражением, то будет ошибка компиляции

👉🏿 constexpr функции могут быть вычислены на этапе компиляции, если аргументы являются константными выражениями. Но также они могут быть вычислены в рантайме с аргументами в виде рантайм значений.

Don't be confused. Stay cool.

#cpp20
👍27🔥64🗿2😁1
constexpr vs constinit
#опытным

constinit - еще один спецификатор, который появился в С++20. Им помечаются глобальные или thread-local переменные для того, чтобы удостоверится в их статической инициализации. Это либо нулевая инициализация, либо константная инициализация. Собственно, это и отображено в самом названии спецификатора.

Тут суть в Static Initialization Order Fiasco. Инициализация глобальных переменных в рантайме зависит от фазы луны и половой активности жаб в Центральной Америке. Мы нем можем гарантировать порядок инициализации глобальных переменных в рантайме и если одна переменная зависит от значения другой, то может произойти много неприятных неожиданностей.

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

Интересность ситуёвины состоит в том, что constinit не подразумевает константность объекта! Действительно, мы же можем проинициализировать константным выражение ь неконстантную переменную и это будет валидный код:

static int i = 42;


constinit - это только про гарантии инициализации в компайл тайме и все! Например:

// init at compile time
constexpr int compute(int v) { return vvv; }
constinit int global = compute(10); // compute is invoked at compile-time

// won't work:
// constinit int another = global; // global is a runtime value

int main() {
// but allow to change later...
global = 100;

// global is not constant expression!
// std::array<int, global> arr;
}


Мы можем инициализировать global с помощью константных выражений, в том числе и результатами вычислений constexpr функций. Однако сама global не является ни constexpr, ни даже обычной константой. С ее помощью нельзя инициализировать другие constinit переменные, как нельзя использовать ее в качестве шаблонных параметров. Но global можно изменять, как как она не предполагает иммутабельность.

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

Итого:

👉🏿 constinit переменные в своей базе мутабельные, constexpr - немутабельные.

👉🏿 constinit применяется только к static и thread storage duration объектам. Проще говоря, к разного рода глобальным переменным. constexpr может применяться к локальным переменным.

👉🏿 Оба спецификатора обеспечивают инициализацию глобальных переменных в compile-time и защищают от SIOF.

👉🏿 Эти спецификаторы нельзя использовать в одном выражении.

Don't be confused. Stay cool.

#cpp20
👍2410🔥9❤‍🔥1
​​Висячие ссылки в лямбдах
#новичкам

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

В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.

Лямбда, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой её создали. Как только лямбда покинула скоуп - можно начинать молиться:

auto make_add_n(int n) {
return [&](int x) {
return x + n; // n will become dangling reference!
};
}

auto add5 = make_add_n(5);
std::cout << add5(5) << std::endl; // UB!


Гцц и шланг пишут разный результат на консоль, что напрямую говорит об ub. Можете посмотреть тут. На варнинги об этой ситуации лучше не надеяться, потому что гцц например думает, что в коде все в порядке.

Еще более интересная ситуация с объектами и методами.

struct Task {
int id;

std::function<void()> GetNotifier() {
return [=]{
std::cout << "notify " << id << std::endl;
};
}
};

int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}


Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.

На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать this в списке захвата:

struct Task {
int id;

std::function<void()> GetNotifier() {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


Так уже чуть проще отловить проблему.

Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.

В случае с захватом this профилактикой может быть синтаксическое ограничение использование методов, возвращающий лямбду, с помощью ref-квалификаторов методов:

struct Task {
int id;
std::function<void()> GetNotifier() && = delete; // forbit call on temporaries

std::function<void()> GetNotifier() & {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


Теперь вы не сможете вызвать этот метод на временном объекте, потому что удалена соответствующая перегрузка.

Конечно это вряд ли поможет в многопоточке, но это уже что-то.

Refer to actual things. Stay cool.

#cppcore #cpp11 #cpp20
2🔥28👍1911🤯1
История capture this
#опытным

Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.

С++11

Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается &(*this) то есть указатель на текущий объект:

struct Foo {
int m_x = 0;

void func() {
int x = 0;

//Explicit capture 'this'
[this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[&]() { /*access m_x and x*/ }();

//Redundant 'this'
[&, this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[=]() { /*access m_x and x*/ }();

//Error
[=, this]() { }();
}
};


Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.

С++14

Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:

struct Foo {
int m_x = 0;

void func() {
[copy=*this]() mutable {
copy.m_x++;
}();
}
};


В остальном все осталось также.

С++17

Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:

struct Processor {
//Some state data..

std::future<void> process(/*args*/) {
//Pre-process...
//Do the data processing asynchronously
return
std::async(std::launch::async,
[=](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

auto caller() {
Processor p;
return p.process(/*args*/);
}


В этом коде UB, потому что возвращаем лямбду со ссылкой на несуществующий объект. Поменять это можно захватом объекта по значению:

struct Processor {
std::future<void> process(/*args*/) {
return
std::async(std::launch::async,
[*this](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.

С++20

С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.

Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.

Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:

struct Bagel {
int x = 0;
void func() {
//OK until C++20. Warning in C++20.
[=]() { std::cout << x; }();

//Error/warning until C++20. OK in C++20.
[=, this]() { std::cout << x; }();
}
};


Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.

Know the history. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥16👍1110🤯6
​​starts_with, ends_with
#новичкам

До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.

В нем появляются стандартные методы std::string/std::string_view .starts_with() и .ends_with():

constexpr bool starts_with(string_view sv) const noexcept;
constexpr bool starts_with(CharT c ) const noexcept;
constexpr bool starts_with(const CharT* s ) const;

constexpr bool ends_with(string_view sv )const noexcept;
constexpr bool ends_with(CharT c ) const noexcept;
constexpr bool ends_with(const CharT* s ) const;


Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.

const std::string url { "https://isocpp.org" };

// string literals
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";

// a single char:
if (url.starts_with('h') && url.ends_with('g'))
std::cout << "letters matched!\n";


Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.

Примерчик:

const std::vector<std::string> tokens { 
"<header>",
"<h1>",
"Hello World",
"</h1>",
"<p>",
"This is my super cool new web site.",
"</p>",
"<p>",
"Have a look and try!",
"</p>",
"</header>"
};

auto text = tokens |
std::views::filter([](const std::string& s) {
if (s.starts_with("<") || s.ends_with(">"))
return false;

return true;
});

for (const auto& str : text)
std::cout << str << std::endl;

// OUTPUT:
// Hello World
// This is my super cool new web site.
// Have a look and try!


Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.

В общем, полезные штуки, которые помогают заменить громоздкие кастомные проверки через find на использование выразительных функций.

Be expressive. Stay cool.

#cpp20 #STL
13🔥3112👍8❤‍🔥3🐳2
​​std::type_identity
#опытным

Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:

double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}


В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:

template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );


Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:

auto bounded = std::clamp(42, 3.14, 69.f); // ERROR!


Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.

Можно было сделать 3 отдельных параметра:

template<class T1, class T2, class T3>
constexpr const T& clamp( const T1& v, const T2& lo, const T3& hi );


Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.

Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:

template<class T>
struct type_identity { using type = T; };

template< class T >
using type_identity_t = type_identity<T>::type;


Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:

template <class T>
auto bound(T num, typename std::type_identity<T>::type low, typename std::type_identity<T>::type high) {
return std::clamp(num, low, high);
}

auto bounded = bound(25.5, 20, 25);


При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.

В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.

Также type_identity можно использовать для того, чтобы запретить вывод типов и заставить пользователя явно прописывать шаблонные параметры. Это может быть важно для точной передачи типа:

template<class T>
void foo(typename std::type_identity<T>::type arg) {}

foo<int>(42); // T жёстко задаётся как int
// foo(42); // Ошибка: вывод T невозможен!


Тоже самое для вариадиков:

template <typename... Ts>
void process(typename std::type_identity<std::tuple<Ts...>>::type data) {
}

process<int, double>(std::tuple{1, 2.0}); // OK
process(std::tuple{1, 2.0}); // ERROR, не указаны типы шаблонных параметров


Прикольный инструмент для тонкой настройки вашего шаблонного кода.

Спасибо @d7d1cd за идею для поста)

Turn off deduction when it is not needed. Stay cool.

#template #cpp20
929👍19🔥10🤯2