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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
26%
ошибка компиляции
21%
Call on const lvalue reference\nCall on const lvalue reference
53%
Call on lvalue reference\nCall on const lvalue reference
🔥62👍21🤓1
Второй пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1

void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2

void foo() && = delete; //3

void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
🔥4👍321
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
54%
ошибка компиляции
46%
Call on const lvalue reference\nCall on const rvalue reference
🔥4👍321
Третий пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
// void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
const_cast<const SomeClass&&>(lvalue).foo();
}
👍4🔥321
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
35%
ошибка компиляции
28%
Call on const lvalue reference\nCall on rvalue reference
37%
Call on const lvalue reference\nCall on const lvalue reference
🔥8👍42❤‍🔥1
Кейсы применения ref-qualified методов
#опытным

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

Разработка библиотек. Довольно очевидно, что разработчикам всяких библиотек нужно учитывать примерно все сценарии использования их классов. Пользователи(безумные) могут скастить объект к константной правой ссылке и методы класса должны работать корректно. Тут очень важно, чтобы тип возвращаемого значения методов соответствовал типу объекта. Пример:

template <typename T>
class optional {

constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}

constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};


Если объект временный, то возвращаем правую ссылку на мувнутый ресурс. Если объект lvalue, то возвращаем обычную ссылку.

Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометить &, чтобы эти методы могли вызываться только у именованных объектов. Ведь если получить ссылку на внутренний ресурс временного объекта, то временный объект уничтожится, а вы останетесь с разбитым корытом висячей ссылкой. Спасибо @d7d1cd за кейс)

struct Vector {
int & operator[](size_t index) & { // notice & after arguments
return vec[index];
}
std::vector<int> vec;
};

Vector v;
v.vec = {1, 2, 3, 4};
v[1]; // ok
Vector{{1, 2, 3, 4}}[1]; // compile error


Также прикрепляю ссылочку на быстрый ответ из блога стандарта С++ посвященный этому кейсу.


Оптимизации. Иногда для определенных ссылочных типов мы можем оптимизировать какой-то метод. Например, в С++23 ввели rvalue reference перегрузку для метода substr класса std::basic_string. Мы знаем, что метод substr формирует новую строку, копируя туда рэндж из оригинальной строки. С++23 теперь сделал так, чтобы при вызове метода substr у правых ссылок объект подстроки тырил данные у оригинальной строки и фактически формировался из ее внутреннего буфера. Более подробно можно почитать в пропоузале.

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

struct Vector {
int operator[](size_t index) && { // notice & after arguments
return vec[index];
}
std::vector<int> vec;
};


В общем, в каждом конкретном случае оптимизировать можно по-разному.

Так что ref-qualified методы - это прекрасный инструмент тонкой настройки в руках профессионалов.

Be useful. Stay cool.

#cppcore #optimization #cpp23
20👍9🔥82❤‍🔥2
Перегружаем деструктор
#новичкам

Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных.

Но можно ли перегружать деструктор класса?

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

По поводу дополнительных параметров деструктора.

Деструкторы стековых переменных вызываются неявно при выходе из скоупа. В языке просто нет инструментов, чтобы сообщить компилятору, как надо удалить объект. Способ только один. Удаление объектов, аллоцированных на стеке, ничем не должно идейно отличаться от удаления автоматических переменных. Поэтому и операторы delete и delete[] не принимают никаких аргументов.

Единственный вариант остается - это передавать дополнительные параметры при явном вызове деструктора. Однако кейсы применимости явного вызова деструктора и так сильно ограничены. Добавлять в стандарт перегрузку деструкторов, чтобы на этом строилась какая-то логика - излишне. И если вам уж захотелось построить какую-то логику на удалении, то можно ее вынести в статический метод destroy.

Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом.

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

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

Have a deeper understanding. Stay cool.

#memory #cppcore
27👍15🔥7😁41
auto аргументы функций
#опытным

Проследим историю с возможностью объявлять аргументы функций, как auto.

До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов

Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:

auto print = [](auto& x){std::cout << x << std::endl;};
print(42);
print(3.14);


Это круто повысило вариативность лямбд, предоставив им некоторые плюшки шаблонов.

У обычных функции, тем не менее, так и остались обычные шаблонные параметры.

Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:

void sum(auto a, auto b)
{
    auto result = a + b;
    std::cout << a << " + " << b << " = " << result << std::endl;
}

sum(1, 3);
sum(3.14, 42);
sum(std::string("123"), std::string("456));
// OUTPUT:
// 1 + 3 = 4
// 3.14 + 42 = 45.14
// 123 + 456 = 123456


Если для лямбд это было необходимым решением из-за того, что их не хотели делать шаблонными(хотя в С++20 их уже можно делать такими), то auto параметры обычных функций призваны немного упростить шаблонную логику там, где не нужно использовать непосредственно тип шаблонного параметра. Так сказать, шаблоны на чилле и расслабоне.

Осталось только добавить, что параметры auto работают по принципу выведения типов для шаблонов, а не по принципу выведения типов auto переменных.

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

Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно.

Hide unused details. Stay cool.

#cpp11 #cpp14 #cpp20 #template
🔥31👍163👎2❤‍🔥11
Как думаете, нужен ли С++ стандартный сборщик мусора?
👎102🤣37😁14👍102
Проблемы ref-qualified методов
#опытным

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

Но один из примеров в том посте выбивается из общей массы. Еще раз посмотрим на него:

template <typename T>
class optional {
// version of value for non-const lvalues
constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

// version of value for const lvalues
constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

// version of value for non-const rvalues... are you bored yet?
constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}

// you sure are by this point
constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};


Это примерно то, как метод value класса std::variant был введен в стандарт С++17.
Мягко говоря, есть ощущение, что код дублируется. А если не считать мува, то вообще квадруплицируется.

Это вот стандартная штука, когда функции отличаются немного и их нельзя объединить в одну.

В таких случаях обычно помогают шаблоны. А учитывая, что у нас для левых ссылок нет мува, а для правых - есть, очень сильно напрашиваются универсальные ссылки и шаблонный std::forward.

Но тут шаблон вообще никак не вписывается. Методы же не принимают даже никаких аргументов. Какой шаблонный параметр сюда вообще вписывается?

Ну вообще говоря, методы принимают неявный аргумент this....

To be continued.

Intrigue people. Stay cool.

#cppcore
🔥21👍741
Deducing this
#опытным

Все методы принимают неявный параметр - указатель this на текущий объект. Также мы можем вызывать методы для объектов с разной константностью/ссылочностью. И главное - компилятор знает в момент компиляции вызова метода настоящий тип объекта со всеми квалификаторами. Единственное, что отделяется нас от возможности введения шаблонности - это указательный тип this, который не инкапсулирует в себе информацию о квалификаторах объекта.

И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:

struct cat {
std::string name;

void print_name(this cat& self) {
std::cout << name; //invalid
std::cout << this->name; //also invalid
std::cout << self.name; //all good
}
void print_name(this const cat& self) {
std::cout << self.name;
}
void print_name(this cat&& self) {
std::cout << self.name;
}
void print_name(this const cat&& self) {
std::cout << self.name;
}
};


Особенности:

👉🏿 Мы явно указываем параметр this.

👉🏿 Явно указываем тип объекта и его квалификаторы.

👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра.

👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем.

Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:

template <typename T>
struct optional {
// One version of value which works for everything
template <class Self>
constexpr auto&& value(this Self&& self) {
if (self.has_value()) {
return std::forward<Self>(self).m_value;
}
throw bad_optional_access();
}
};


Вот это бэнгер! Мы деквадруплицировали код!

Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта.

Настоящая магия, причем вне хогвартса!

Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self.

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

Simplify your life. Stay cool.

#cpp23 #template
🔥378👍8
Передача объекта в методы по значению
#опытным

Небольшие типы данных, особенно до 8 байт длиной, быстрее передавать в методы или возвращать из методов по значению.

С помощью deducing this мы можем вызывать методы не для ссылки(под капотом которой указатель), а для значения объекта.

Семантика будет ровно такая, как вы ожидаете. Объект скопируется внутрь метода и все операции будут происходить над копией.

Давайте посмотрим на пример:

struct just_a_little_guy {
int how_small;
int uwu();
};

int main() {
just_a_little_guy tiny_tim{42};
return tiny_tim.uwu();
}


Здесь используется старая нотация с неявным this.

Посмотрим, какой код может нам выдать компилятор:

sub     rsp, 40                           
lea rcx, QWORD PTR tiny_tim$[rsp]
mov DWORD PTR tiny_tim$[rsp], 42
call int just_a_little_guy::uwu(void)
add rsp, 40
ret 0


Пройдемся по строчкам и посмотрим, что тут происходит:

- первая строчка аллоцирует 40 байт на стеке. 4 байта для объекта tiny_tim, 32 байта теневого пространства для метода uwu и 4 байта паддинга.
- инструкция lea загружает адрес tiny_tim в регистр rcx, в котором метод uwu ожидает свой неявный параметр.
- mov помещает число 42 в поле объекта tiny_tim.
- вызываем функцию-метод uwu
- наконец деаллоцируем памяти и выходим из main

А теперь применим deducing this с параметром по значению и посмотрим на ассемблер:

struct just_a_little_guy {
int how_small;
int uwu(this just_a_little_guy);
};


Ассемблер:

mov     ecx, 42                           
jmp static int just_a_little_guy::uwu(this just_a_little_guy)


Мы переместили 42 в нужный регистр и сразу же прыгнули в функцию uwu, а не вызвали ее. Поскольку мы не передаем объект в метод по ссылке, нам ничего не нужно аллоцировать на стеке. А значит и деаллоцировать ничего не нужно. Раз нам не нужно за собой подчищать, то можно просто прыгнуть в функцию и не возвращаться оттуда.

Конечно, это искусственный пример, оптимизация есть и мы можем в целом ожидать, то объекты маленьких типов можно быстрее обрабатывать с помощью deducing this.

Optimize yourself. Stay cool.

#cpp23 #optimization #compiler
18🔥14👍7❤‍🔥3
Deducing this и CRTP
#опытным

У 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🔥76❤‍🔥2🤣1
Неочевидное преимущество шаблонов
#новичкам

Давайте немного разбавим рассказ о фичах 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👍952👎1
Рекурсивные лямбды. Невозможно?
#новичкам

Лямбды по сути - функциональные объекты. Можем ли мы вызвать лямбду внутри самой себя? То есть существуют ли рекурсивные лямбды?

int main() {
auto factorial = [&factorial](int n) {
return n > 1 ? n * factorial(n - 1) : 1;
};
return factorial(5);
}


Вот мы пытаемся с помощью лямбды посчитать факториал числа. В чем здесь проблема?

Фактически данный код значит, что компилятор должен сгенерировать замыкание и в поля этого замыкания поместить ссылку на само замыкание:

class lkdlkhbahbahkl_danfaksdf_lamba
{
public:
int operator()(int n) const
{
return n > 1 ? n * factorial(n - 1) : 1;
}
private:
???? factorial;
};


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

В общем влипли мы в то, что рекурсивные лямбды невозможны из-за рекурсии.

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

Don't close on yourself. Stay cool.

#cppcore
🔥26👍135😁3
Больше нет сил

Ребят, плохие новости. У нас больше нет возможности вести канал. Денис лидит 2 команды и строит себе сам дачу, а я пилю курс на Яндекс Практикум и подрабатываю курьером в самокате. В одного уже все руки в дырках от гвоздей, у второго ноги сточились. * Если у кого есть хорошая бригада строителей в Нижнем Новгороде или остеопат - пишите в личку.

В общем, на канал времени не остается совсем. Чем дольше мы его пилим, тем больше понимаем, что не вывозим. Это нормально, это жизнь. Я вон 20 лет лет трехметровым был, но плита жизни придавила и уполовинила. А теперь я маленький и очень толстый.

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

Но есть и радостные новости - тому, кто наберет больше всех сердечек на любом своем комментрии под этим постом, мы передадим права на владение каналом. Уверен, что у вам небезразлично будущее Грокаем С++, поэтому лайкайте достойных людей.

На этом все. Спасибо за этот путь, он был бесценен....

Stay alert. Stay cool.
😭205😁95🤣33🐳16👍13😢11
Рекурсивные лямбды. Хакаем систему
#опытным

Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.

Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.

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
Рекурсивные лямбды. Идеал.
#опытным

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

Но по-настоящему рекурсивные лямбды появились только в С++23 с введением deducing this.

Если лямбда - это класс с методом operator(), значит мы внутрь этого метода можем передать явный this и тогда лямбда сможет вызвать сама себя!

auto factorial = [](this auto&& self, int n) {
if (n <= 1) return 1;
return n * self(n - 1);
};


У нас конечно в С++20 есть шаблонные лямбды, но здесь это немножко оверкилл. Поэтому используем автоматический вывод типа с помощью auto aka дженерик лямбду.

У нас была цель, мы к ней шли и, наконец, пришли. Ура, товарищи, ура!

Однако как будто бы слишком много разговоров о сущности, которой пользовались полтора дровосека.
Да, рекурсивные лямбды - это скорее экзотика. Но и у них есть свои юзкейсы. Поговорим о них в следующем посте.

Find true yourself. Stay cool.

#cppcore #cpp23
👍26😁17🔥117
Рекурсивные лямбды. Кейсы
#опытным

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

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🔥1461😁1
Mutable
#новичкам

Это ключевое слово - один из самых темных уголков С++. И не то, чтобы очень важный уголок. Вы вполне ни разу могли с ним не сталкиваться. Но тем не менее по какой-то причине интервьютеры часто задают вопрос: "для чего предназначен mutable?". Ответит человек или нет особо никак не показывает его навыки программиста, лишь знание узких мест языка. Но раз такие вопросы задают, то вы должны быть готовы к ответу на них. Поэтому и родился этот пост.

Проблема вот в чем. Есть константный объект. Как вы знаете, поля константного объекта запрещено изменять. Но это довольно сильное ограничение. Да, не хотелось бы, чтобы инвариаты класса менялись. Однако помимо комплекса полей класса, представляющих собой инвариант класса, в объекте могут храниться другие поля, которые не входят в этот инвариант.

И вот мы имеем дело с тем, что нам хочется иметь семантическую константность, когда защищаем от изменения только те поля, которые должны быть неизменными в константном объекте. Но по дефолту нам дана синтаксическая константность, которая запрещает изменения любых нестатических полей.

class ThreadSafeLogger {
std::atomic<int> call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Error! Changing class field in const member-function
// logging
}
};


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

Что же делать?

Вот тут как раз, mutable Валера, настало твое время.

Помечая ключевым словом mutable поле класса вы разрешаете менять его в константных методах:

class ThreadSafeLogger {
mutable std::atomic<int> call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Works fine
// logging
}
};


Теперь мы можем изменять счетчик даже в константном методе.

В целом, на это все о функциональности этого ключевого слова.

В каких кейсах его можно применять?

Сбор статистики вычислений в объекте. Пример выше как раз об этом. Для сбора статистики могут использоваться и более сложные сущности, типа оберток над известными системами мониторинга(аля prometheus).

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

Кэш. Результаты предыдущих вычислений никак не влияют на инвариант класса, поэтому внутренний кэш класса можно пометить mutable, чтобы кэш можно было использовать в константных методах.

class SomeComputingClass {
mutable std::unordered_map<Key, Result> cache;
public:
Result compute(const Key& key) const {
if (!cache.contains(key)) {
cache[key] = /* actual computing */;
}
return cache[key];
}
};


Из популярного все. Если кто знает узкий кейсы применения mutable, просим пройти в чат.

Ну все, никакой гадкий интервьюер вас не завалит. Ваше кунг-фу теперь сильнее его кунг-фу.

Surprise your enemy. Stay cool.

#cppcore #interview
229🔥17👍13❤‍🔥1