std::mem_fn
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
Какие варианты у нас есть в такой ситуации?
❗️ std::function
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
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:
Сработает ли такой код?
Он не соберется. Будет ошибка:
Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:
До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:
Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.
Be explicit. Stay cool.
#cppcore #cpp11 #cpp20
#новичкам
Бывают ситуации, когда вы хотите зарегистрировать коллбэк, в котором будет выполняться метод текущего класса.
Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку 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
👍23❤5❤🔥4🔥2👏1
std::to_address
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
Use the right tool. Stay cool.
#cpp20 #cppcore
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции 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
2❤24👍10🔥7😁3
constexpr функции сквозь года
#новичкам
constexpr бывают не только переменные, но и функции. Такие функции могут быть выполнены, как в compile-time, так и в runtime, в зависимости от контекста вызова:
- Если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции.
- Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime.
- Если вы попытаетесь присвоить возвращаемое значение функции с runtime аргументом constexpr переменной, то получите ошибку компиляции.
Пройдемся по стандартам языка и посмотрим, как в них изменялись constexpr функции.
В С++11 можно было использовать только однострочные constexpr функции с сильными ограничениями.
В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:
C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:
C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.
constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.
В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).
Free up time for important things. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#новичкам
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👍12❤6👎2
constexpr vs consteval функции
#опытным
В С++20 добавили новый спецификатор - consteval. Отдельно его интро не особо интересно разбирать, поэтому попробуем в сравнении с constexpr.
consteval может быть применен только к функциям и это ключевое слово заставляет функцию выполняться во время компиляции и возвращать константное выражение. Отсюда и название спецификатора "константное вычисление"(constant evaluation).
Вот пара простых примеров:
Мы можем использовать оба спецификатора constexpr и consteval для того, чтобы инициализировать compile-time константы и обычные переменные.
Однако consteval функции могут вызываться только с constant expression в качестве аргументов. При попытке передачи в них неконстантного выражения будет ошибка компиляции.
Итого:
👉🏿 спецификатор consteval может быть применен только для функций
👉🏿 constexpr может быть применен и для переменных
👉🏿 consteval заставляет компилятор вычислять выражение на этапе компиляции. Если хотя бы один аргумент такой функции не является константным выражением, то будет ошибка компиляции
👉🏿 constexpr функции могут быть вычислены на этапе компиляции, если аргументы являются константными выражениями. Но также они могут быть вычислены в рантайме с аргументами в виде рантайм значений.
Don't be confused. Stay cool.
#cpp20
#опытным
В С++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🔥6❤4🗿2😁1
constexpr vs constinit
#опытным
constinit - еще один спецификатор, который появился в С++20. Им помечаются глобальные или thread-local переменные для того, чтобы удостоверится в их статической инициализации. Это либо нулевая инициализация, либо константная инициализация. Собственно, это и отображено в самом названии спецификатора.
Тут суть в Static Initialization Order Fiasco. Инициализация глобальных переменных в рантайме зависит от фазы луны и половой активности жаб в Центральной Америке. Мы нем можем гарантировать порядок инициализации глобальных переменных в рантайме и если одна переменная зависит от значения другой, то может произойти много неприятных неожиданностей.
Вот constinit служит гарантией того, что переменная проинициализирована до старта программы, а программистам показывает, что с этой глобальной переменной точно все в порядке.
Интересность ситуёвины состоит в том, что constinit не подразумевает константность объекта! Действительно, мы же можем проинициализировать константным выражение ь неконстантную переменную и это будет валидный код:
constinit - это только про гарантии инициализации в компайл тайме и все! Например:
Мы можем инициализировать 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
#опытным
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
👍24❤10🔥9❤🔥1
Висячие ссылки в лямбдах
#новичкам
Все знают, что возврат ссылки на локальный объект функции приводит к неопределенному поведению. Однако не всегда так просто можно распознать такие ситуации.
В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.
Лямбда, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой её создали. Как только лямбда покинула скоуп - можно начинать молиться:
Гцц и шланг пишут разный результат на консоль, что напрямую говорит об ub. Можете посмотреть тут. На варнинги об этой ситуации лучше не надеяться, потому что гцц например думает, что в коде все в порядке.
Еще более интересная ситуация с объектами и методами.
Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.
На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать
Так уже чуть проще отловить проблему.
Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.
В случае с захватом this профилактикой может быть синтаксическое ограничение использование методов, возвращающий лямбду, с помощью ref-квалификаторов методов:
Теперь вы не сможете вызвать этот метод на временном объекте, потому что удалена соответствующая перегрузка.
Конечно это вряд ли поможет в многопоточке, но это уже что-то.
Refer to actual things. Stay cool.
#cppcore #cpp11 #cpp20
#новичкам
Все знают, что возврат ссылки на локальный объект функции приводит к неопределенному поведению. Однако не всегда так просто можно распознать такие ситуации.
В 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👍19❤11🤯1
История capture this
#опытным
Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.
С++11
Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается
Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.
С++14
Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:
В остальном все осталось также.
С++17
Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:
В этом коде UB, потому что возвращаем лямбду со ссылкой на несуществующий объект. Поменять это можно захватом объекта по значению:
В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.
С++20
С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.
Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.
Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:
Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.
Know the history. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#опытным
Проследим историю захвата 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👍11❤10🤯6
starts_with, ends_with
#новичкам
До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.
В нем появляются стандартные методы
Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.
Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.
Примерчик:
Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.
В общем, полезные штуки, которые помогают заменить громоздкие кастомные проверки через find на использование выразительных функций.
Be expressive. Stay cool.
#cpp20 #STL
#новичкам
До (и включая) 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🔥31❤12👍8❤🔥3🐳2
std::type_identity
#опытным
Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:
В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:
Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:
Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.
Можно было сделать 3 отдельных параметра:
Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.
Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:
Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:
При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.
В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.
Также type_identity можно использовать для того, чтобы запретить вывод типов и заставить пользователя явно прописывать шаблонные параметры. Это может быть важно для точной передачи типа:
Тоже самое для вариадиков:
Прикольный инструмент для тонкой настройки вашего шаблонного кода.
Спасибо @d7d1cd за идею для поста)
Turn off deduction when it is not needed. Stay cool.
#template #cpp20
#опытным
Не так давно мы разбирали функцию 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
9❤29👍19🔥10🤯2