Deducing this и CRTP
#опытным
У deducing this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:
Вам ничего это не напоминает? CRTP конечно.
Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:
За счет шаблонного параметра Derived, который должен быть точным типом наследника, мы можем безопасно кастануть this к указателю на наследника и вызывать у него любые методы.
Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:
Ну вот. У нас только один шаблонный метод. Но для пользователя он ничем не отличается от обычного нешаблонного метода.
Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами.
Make things more elegant. Stay cool.
#template #cpp23
#опытным
У deducing this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:
struct Machine {
template <typename Self>
void print(this Self&& self) {
self.print_name();
}
};
struct Car : public Machine {
std::string name;
void print_name() {
std::cout << "Car\n";
}
};
Car{}.print(); // Выведется "Car"Вам ничего это не напоминает? CRTP конечно.
Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:
template <typename Derived>
struct add_postfix_increment {
Derived operator++(int) {
auto& self = static_cast<Derived&>(*this);
Derived tmp(self);
++self;
return tmp;
}
};
struct some_type : add_postfix_increment<some_type> {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};
За счет шаблонного параметра Derived, который должен быть точным типом наследника, мы можем безопасно кастануть this к указателю на наследника и вызывать у него любые методы.
Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:
struct add_postfix_increment {
template <typename Self>
auto operator++(this Self&& self, int) {
auto tmp = self;
++self;
return tmp;
}
};
struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};Ну вот. У нас только один шаблонный метод. Но для пользователя он ничем не отличается от обычного нешаблонного метода.
Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами.
Make things more elegant. Stay cool.
#template #cpp23
👍28❤7🔥7❤🔥2🤣1
Неочевидное преимущество шаблонов
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
Как видите, здесь много
Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
void qsort( void *ptr, std::size_t count, std::size_t size, /* c-compare-pred */* comp );
extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);
Как видите, здесь много
void * указателей на void. В том числе с помощью него достигается полиморфизм в С(есть еще макросы, но не будем о них).Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
[](const void* x, const void* y)
{
const int arg1 = *static_cast<const int*>(x);
const int arg2 = *static_cast<const int*>(y);
const auto cmp = arg1 <=> arg2;
if (cmp < 0)
return -1;
if (cmp > 0)
return 1;
return 0;
}
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
50🔥35👍9❤5⚡2👎1
Рекурсивные лямбды. Хакаем систему
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
std::function<int(int)> factorial = [&factorial](int n) -> int {
return (n) ? n * factorial(n-1) : 1;
};Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
auto factorial = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto i = factorial(7, factorial);Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
class __lambda_24_20
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(int n, type_parameter_0_0 && factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial(n - 1, factorial);
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<__lambda_24_20 &>(int n, __lambda_24_20 & factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial.operator()(n - 1, factorial);
}
#endif
};
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
auto factorial_impl = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto factorial = [&](int n) { return factorial_impl(n, factorial_impl); };
auto i = factorial(7);Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
using factorial_t = int(*)(int);
static factorial_t factorial = [](int n) {
if (n <= 1) return n;
return n * factorial(n - 1);
};
auto i = factorial(7);
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
❤38🔥17👍12
Рекурсивные лямбды. Кейсы
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
Тут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
struct Leaf { };
struct Node;
using Tree = std::variant<Leaf, Node*>;
struct Node {
Tree left;
Tree right;
};
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{}
using Lambdas::operator()...;
};
int main()
{
Leaf l1;
Leaf l2;
Node nd{l1, l2};
Tree tree = &nd;
int num_leaves = std::visit(Visitor(
[](Leaf const&) { return 1; },
[](this const auto& self, Node* n) -> int {
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
auto printTuple = [](const auto& tuple) constexpr {
auto impl = []<size_t idx>(this const auto& self, const auto& t) constexpr {
if constexpr (idx < std::tuple_size_v<std::decay_t<decltype(t)>>) {
std::cout << std::get<idx>(t) << " ";
self.template operator()<idx+1>(t); // Рекурсивный вызов
}
};
impl.template operator()<0>(tuple);
};
std::tuple<int, double, std::string> tp{1, 2.0, "qwe"};
printTuple(tp);
// Output:
// 1 2 qweТут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
auto listFiles = [](const std::filesystem::path& dir) {
std::vector<std::string> files;
auto traverse = [&](this const auto& self, const auto& path) {
for (const auto& entry : std::filesystem::directory_iterator(path)) {
if (entry.is_directory()) {
self(entry.path());
} else {
files.push_back(entry.path().string());
}
}
};
traverse(dir);
return files;
};Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
👍28🔥14❤6⚡1😁1
std::forward_like
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
template< class T >
constexpr T&& forward(std::remove_reference_t<T>& t ) noexcept;
template< class T >
constexpr T&& forward(std::remove_reference_t<T>&& t ) noexcept;
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
return forward<decltype(obj)>(obj).member;
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
struct adapter {
std::deque<std::string> container;
auto&& operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>(self.container[i]);
} };Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
4🔥22👍7❤4😁2
Идеальная передача из лямбды
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
Ну а передача копии вообще никогда не была проблемой:
Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таковзакон стандарт.
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
auto callback = [message=get_message(), &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}Ну а передача копии вообще никогда не была проблемой:
auto callback = [message=get_message(), &scheduler]() {
// some preparetions
scheduler.submit(message);
}Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
auto callback = [message=get_message(), &scheduler](this auto &&self) {
return scheduler.submit(std::forward_like<decltype(self)>(message));
};Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таков
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
2🔥21👍8❤5😁1
Частичная специализация шаблонов функций
#опытным
Во многих образовательных ресурсах на русском и английском языке я видел, как рассказывают про частичную специализацию шаблонов функций. Ресурсы конечно не очень авторитетные, но если читать про шаблоны в стандарте, то голова вспухнет. Давайте сейчас проясним этот момент.
Частичной специализации шаблонов функции не существует. И точка!
То, что называют выдают за нее - это обычная перегрузка шаблонных функций.
Звучит, как пустяковая проблема. Какая разница, как назвать молоток, если им все равно можно забить гвоздь?
Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.
Шаблонные функции, как и обычные функции, можно перегружать. Логично было принести этот функционал в шаблоны, ибо перегрузки могут быть вообще не связаны ни одним общим параметром.
Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.
Частичная специализация же характерна только для шаблонов классов и переменных.
И в специализированном классе мы должны показать с помощью треугольных скобок, для какой подгруппы типов мы специализируем шаблон. И это обязательно. Специализация должна показать, что она именно специализация какого-то общего шаблона. Ведь перегрузок шаблонов классов не бывает:
С этим разобрались. Давайте пофантазирует, что будет, если бы мы могли частично специализировать шаблон функции, при этом оставили бы дефолтный механизм перегрузки:
Ну и как компилятору выбирать между T-p-overload и T-p-specialization?
Частичная специализация шаблонов функций вводила бы неоднозначность или дополнительную сложность в порядок разрешения перегрузок функции. Да и перегрузка полностью покрывает функциональность частичной специализации. Поэтому ее не существует.
На самом деле без понимания того, какими способами можно изменить поведение шаблона, нельзя нормально понять, как компилятор выбирает правильного кандидата для вызова. Это тонкости, но на высоком уровне тонкости все больше и больше роляют.
Don't be confused. Stay cool.
#template #cppcore
#опытным
Во многих образовательных ресурсах на русском и английском языке я видел, как рассказывают про частичную специализацию шаблонов функций. Ресурсы конечно не очень авторитетные, но если читать про шаблоны в стандарте, то голова вспухнет. Давайте сейчас проясним этот момент.
Частичной специализации шаблонов функции не существует. И точка!
То, что называют выдают за нее - это обычная перегрузка шаблонных функций.
Звучит, как пустяковая проблема. Какая разница, как назвать молоток, если им все равно можно забить гвоздь?
Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.
Шаблонные функции, как и обычные функции, можно перегружать. Логично было принести этот функционал в шаблоны, ибо перегрузки могут быть вообще не связаны ни одним общим параметром.
void f(int) { std::cout << "int-overload" << std::endl; };
void f(int*){ std::cout << "int-p-overload" << std::endl; }
template<class T> void f(T) { std::cout << "T-overload" << std::endl; };
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }
Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.
Частичная специализация же характерна только для шаблонов классов и переменных.
template<typename T>
class Foo {};
template<typename T>
class Foo<T*> {};
И в специализированном классе мы должны показать с помощью треугольных скобок, для какой подгруппы типов мы специализируем шаблон. И это обязательно. Специализация должна показать, что она именно специализация какого-то общего шаблона. Ведь перегрузок шаблонов классов не бывает:
template <typename T1>
struct Foo<T1> {};
// Так нельзя делать, это несвязанные шаблоны
template <typename T1, typename T2>
struct Foo<T1,T2> {};
С этим разобрались. Давайте пофантазирует, что будет, если бы мы могли частично специализировать шаблон функции, при этом оставили бы дефолтный механизм перегрузки:
template<class T> void f(T) { std::cout << "T-overload" << std::endl; };
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }
template<class T> void f<T*>(T*){std::cout << "T-p-specialization" << std::endl;}Ну и как компилятору выбирать между T-p-overload и T-p-specialization?
Частичная специализация шаблонов функций вводила бы неоднозначность или дополнительную сложность в порядок разрешения перегрузок функции. Да и перегрузка полностью покрывает функциональность частичной специализации. Поэтому ее не существует.
На самом деле без понимания того, какими способами можно изменить поведение шаблона, нельзя нормально понять, как компилятор выбирает правильного кандидата для вызова. Это тонкости, но на высоком уровне тонкости все больше и больше роляют.
Don't be confused. Stay cool.
#template #cppcore
1❤22👍12🔥11
Перегружаем шаблоны классов
#опытным
В прошлом посте я говорил, что нельзя перегружать шаблоны классов.
В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.
Но как и практически любое ограничение в С++, его можно хакнуть.
Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.
Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.
Вот такие фокусы.
Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:
Но все равно это хороший инструмент, которым можно пользоваться.
Hack the boundaries. Stay cool.
#template #cppcore
#опытным
В прошлом посте я говорил, что нельзя перегружать шаблоны классов.
В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.
Но как и практически любое ограничение в С++, его можно хакнуть.
Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.
template <typename... T>
struct Foo;
template <typename T1>
struct Foo<T1> {};
template <typename T1, typename T2>
struct Foo<T1,T2> {};
Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.
Вот такие фокусы.
Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:
template <typename T, int N>
struct Foo<T, N> {}; // forbidden
Но все равно это хороший инструмент, которым можно пользоваться.
Hack the boundaries. Stay cool.
#template #cppcore
1🔥22👍12❤🔥3❤2
std::move_if_noexcept
#опытным
В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.
Вот реализация этой функции в gcc:
Две части: условия мува и сам мув.
Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.
Условие move_if_noexcept_cond истинно только в одном случае: когда мув-конструктор бросающий и есть копирующий конструктор. Получается, что это единственная ситуация, когда мува не произойдет. Во всех остальных случаях значение скастуется к правой ссылке.
Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.
Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:
То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.
В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.
Live legally. Stay cool.
#template #cppcore #STL
#опытным
В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.
Вот реализация этой функции в gcc:
template<typename _Tp>
struct __move_if_noexcept_cond
: public _and<_not<is_nothrow_move_constructible<_Tp>>,
is_copy_constructible<_Tp>>::type { };
template<typename _Tp>
[[nodiscard,gnu::always_inline]]
constexpr
__conditional_t<__move_if_noexcept_cond<_Tp>::value, const _Tp&, _Tp&&>
move_if_noexcept(_Tp& __x) noexcept
{ return std::move(__x); }
Две части: условия мува и сам мув.
Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.
Условие move_if_noexcept_cond истинно только в одном случае: когда мув-конструктор бросающий и есть копирующий конструктор. Получается, что это единственная ситуация, когда мува не произойдет. Во всех остальных случаях значение скастуется к правой ссылке.
Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.
Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:
struct NoMove1
{
// prevents implicit declaration of default move constructor;
// however, the class is still move-constructible because its
// copy constructor can bind to an rvalue argument
NoMove1(const NoMove1&) {}
};
static_assert(std::is_move_constructible_v<NoMove1>); // Here
static_assert(!std::is_trivially_move_constructible_v<NoMove1>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove1>);
struct NoMove2
{
// Not move-constructible since the lvalue reference
// can't bind to the rvalue argument
NoMove2(NoMove2&) {}
};
static_assert(!std::is_move_constructible_v<NoMove2>); // And here
static_assert(!std::is_trivially_move_constructible_v<NoMove2>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove2>);
То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.
В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.
Live legally. Stay cool.
#template #cppcore #STL
❤23👍11🔥8🤯1
decltype(auto) vs auto&&. Прокси объекты и висячие ссылки.
#опытным
В прошлом посте мы видели, что разница в использовании decltype(auto) и auto&& при выводе типа возвращаемого значения функции проявляется только при возврате объектов, на которые явно не навесили ссылки. Иногда мы можем это отследить глазами и потенциально использовать более простую версию вывода типа. Однако не все мы можем отследить глазами.
Доступ к элементам std::deque всегда возвращает честную левую ссылку. И это почти полностью справедливо для std::vector, кроме одного исключения.
Это std::vector<bool>. Эта специализация вектора возвращает не честную ссылку на объект типа bool, в временный proxy объект. Дело в том, что тип bool занимает как минимум 1 байт, так как это минимально адресуемая ячейка памяти. Но логически он хранит всего 1 бит информации. Если бы мы могли как-то по-хитрому хранить булевы значения, чтобы каждое из них занимало всего 1 бит, то мы бы уменьшили потребление памяти как минимум в 8 раз! Именно это и делается в специализации для bool. Там булевы значения хранятся в виде битов более вместительного типа(int), а для получения доступа к значениям используется proxy объект reference, который неявно приводится bool.
Посмотрим, к чему приводит эта маленькая особенность:
Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.
При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.
Proxy объекты не так часто используются. Один из основных кейсов - это доступ к элементам многомерных структур. Однако при проектировании и написании кода, которым будут пользоваться другие люди, нужно учитывать такие вещи и писать в первую очередь безопасный код.
Спасибо @thonease за предоставления исходного кода)
По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).
Be safe. Stay cool.
#cppcore #template
#опытным
В прошлом посте мы видели, что разница в использовании decltype(auto) и auto&& при выводе типа возвращаемого значения функции проявляется только при возврате объектов, на которые явно не навесили ссылки. Иногда мы можем это отследить глазами и потенциально использовать более простую версию вывода типа. Однако не все мы можем отследить глазами.
Доступ к элементам std::deque всегда возвращает честную левую ссылку. И это почти полностью справедливо для std::vector, кроме одного исключения.
Это std::vector<bool>. Эта специализация вектора возвращает не честную ссылку на объект типа bool, в временный proxy объект. Дело в том, что тип bool занимает как минимум 1 байт, так как это минимально адресуемая ячейка памяти. Но логически он хранит всего 1 бит информации. Если бы мы могли как-то по-хитрому хранить булевы значения, чтобы каждое из них занимало всего 1 бит, то мы бы уменьшили потребление памяти как минимум в 8 раз! Именно это и делается в специализации для bool. Там булевы значения хранятся в виде битов более вместительного типа(int), а для получения доступа к значениям используется proxy объект reference, который неявно приводится bool.
Посмотрим, к чему приводит эта маленькая особенность:
template<typename Container, typename Index>
auto&& processAndAccess(Container& c, Index i) {
// do something
// ...
return c[i];
}
std::vector<int> v = {1, 2, 3};
// OK - returns int&
processAndAccess(v, 1) = 3;
std::vector<bool> v2 = {true, false, false};
// NOT OK - returns vector<bool>::reference&& which is a dangling reference
processAndAccess(v2, 1) = true;
Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.
При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.
Proxy объекты не так часто используются. Один из основных кейсов - это доступ к элементам многомерных структур. Однако при проектировании и написании кода, которым будут пользоваться другие люди, нужно учитывать такие вещи и писать в первую очередь безопасный код.
Спасибо @thonease за предоставления исходного кода)
По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).
Be safe. Stay cool.
#cppcore #template
🔥18👍12❤9