Кейсы применения ref-qualified методов
#опытным
В нескольких предыдущих постах мы говорили про ref-qualified методы и как компилятор выбирает правильную перегрузку. Эта фича многим незнакома и сходу не очень понятно, где ее можно использовать. Давайте сегодня чуть подробнее поговорим о том, где они могут быть реально полезны, чтобы вы вдохновились и использовали такую перегрузку методов чаще.
✅ Разработка библиотек. Довольно очевидно, что разработчикам всяких библиотек нужно учитывать примерно все сценарии использования их классов. Пользователи(безумные) могут скастить объект к константной правой ссылке и методы класса должны работать корректно. Тут очень важно, чтобы тип возвращаемого значения методов соответствовал типу объекта. Пример:
Если объект временный, то возвращаем правую ссылку на мувнутый ресурс. Если объект lvalue, то возвращаем обычную ссылку.
✅ Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометитьразбитым корытом висячей ссылкой. Спасибо @d7d1cd за кейс)
Также прикрепляю ссылочку на быстрый ответ из блога стандарта С++ посвященный этому кейсу.
✅ Оптимизации. Иногда для определенных ссылочных типов мы можем оптимизировать какой-то метод. Например, в С++23 ввели rvalue reference перегрузку для метода substr класса std::basic_string. Мы знаем, что метод substr формирует новую строку, копируя туда рэндж из оригинальной строки. С++23 теперь сделал так, чтобы при вызове метода substr у правых ссылок объект подстроки тырил данные у оригинальной строки и фактически формировался из ее внутреннего буфера. Более подробно можно почитать в пропоузале.
Также, если вы возвращаете из метода легковесный объект, то в перегрузке для rvalue ссылок вы можете возвращать объект по значению. Так вы избавляетесь от избыточной ссылочной семантики и индирекции и , возможно, улучшаете перформанс. Ведь маленькие типы быстрее передавать и возвращать именно по значению:
В общем, в каждом конкретном случае оптимизировать можно по-разному.
Так что ref-qualified методы - это прекрасный инструмент тонкой настройки в руках профессионалов.
Be useful. Stay cool.
#cppcore #optimization #cpp23
#опытным
В нескольких предыдущих постах мы говорили про 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, то возвращаем обычную ссылку.
✅ Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометить
&, чтобы эти методы могли вызываться только у именованных объектов. Ведь если получить ссылку на внутренний ресурс временного объекта, то временный объект уничтожится, а вы останетесь с 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🔥8⚡2❤🔥2
Deducing this
#опытным
Все методы принимают неявный параметр - указатель this на текущий объект. Также мы можем вызывать методы для объектов с разной константностью/ссылочностью. И главное - компилятор знает в момент компиляции вызова метода настоящий тип объекта со всеми квалификаторами. Единственное, что отделяется нас от возможности введения шаблонности - это указательный тип this, который не инкапсулирует в себе информацию о квалификаторах объекта.
И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:
Особенности:
👉🏿 Мы явно указываем параметр this.
👉🏿 Явно указываем тип объекта и его квалификаторы.
👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра.
👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем.
Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:
Вот это бэнгер! Мы деквадруплицировали код!
Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта.
Настоящая магия, причем вне хогвартса!
Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self.
Вот вам пропоузал по этой замечательной фиче. А мы в нескольких следующих постах будем разбирать кейсы, где она может быть применима.
Simplify your life. Stay cool.
#cpp23 #template
#опытным
Все методы принимают неявный параметр - указатель 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
🔥37❤8👍8
Передача объекта в методы по значению
#опытным
Небольшие типы данных, особенно до 8 байт длиной, быстрее передавать в методы или возвращать из методов по значению.
С помощью deducing this мы можем вызывать методы не для ссылки(под капотом которой указатель), а для значения объекта.
Семантика будет ровно такая, как вы ожидаете. Объект скопируется внутрь метода и все операции будут происходить над копией.
Давайте посмотрим на пример:
Здесь используется старая нотация с неявным this.
Посмотрим, какой код может нам выдать компилятор:
Пройдемся по строчкам и посмотрим, что тут происходит:
- первая строчка аллоцирует 40 байт на стеке. 4 байта для объекта tiny_tim, 32 байта теневого пространства для метода uwu и 4 байта паддинга.
- инструкция lea загружает адрес tiny_tim в регистр rcx, в котором метод uwu ожидает свой неявный параметр.
- mov помещает число 42 в поле объекта tiny_tim.
- вызываем функцию-метод uwu
- наконец деаллоцируем памяти и выходим из main
А теперь применим deducing this с параметром по значению и посмотрим на ассемблер:
Ассемблер:
Мы переместили 42 в нужный регистр и сразу же прыгнули в функцию uwu, а не вызвали ее. Поскольку мы не передаем объект в метод по ссылке, нам ничего не нужно аллоцировать на стеке. А значит и деаллоцировать ничего не нужно. Раз нам не нужно за собой подчищать, то можно просто прыгнуть в функцию и не возвращаться оттуда.
Конечно, это искусственный пример, оптимизация есть и мы можем в целом ожидать, то объекты маленьких типов можно быстрее обрабатывать с помощью deducing this.
Optimize yourself. Stay cool.
#cpp23 #optimization #compiler
#опытным
Небольшие типы данных, особенно до 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
Stack Overflow
What is the 'shadow space' in x64 assembly?
I found plenty of topics about this shadow space, but I couldn't find the answer in none of them, so my question is:
How much exactly bytes I need to subtract from the stack pointer, before enteri...
How much exactly bytes I need to subtract from the stack pointer, before enteri...
❤18🔥14👍7❤🔥3
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❤6❤🔥2🤣1
Рекурсивные лямбды. Идеал.
#опытным
Все предыдущие примеры были воркэраундами вокруг неспособности лямбды обращаться к самой себе. Какие-то из них имеют ограничения в использовании, какие-то - накладные расходы.
Но по-настоящему рекурсивные лямбды появились только в С++23 с введением deducing this.
Если лямбда - это класс с методом operator(), значит мы внутрь этого метода можем передать явный this и тогда лямбда сможет вызвать сама себя!
У нас конечно в С++20 есть шаблонные лямбды, но здесь это немножко оверкилл. Поэтому используем автоматический вывод типа с помощью auto aka дженерик лямбду.
У нас была цель, мы к ней шли и, наконец, пришли. Ура, товарищи, ура!
Однако как будто бы слишком много разговоров о сущности, которой пользовались полтора дровосека.
Да, рекурсивные лямбды - это скорее экзотика. Но и у них есть свои юзкейсы. Поговорим о них в следующем посте.
Find true yourself. Stay cool.
#cppcore #cpp23
#опытным
Все предыдущие примеры были воркэраундами вокруг неспособности лямбды обращаться к самой себе. Какие-то из них имеют ограничения в использовании, какие-то - накладные расходы.
Но по-настоящему рекурсивные лямбды появились только в С++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🔥11❤7
Рекурсивные лямбды. Кейсы
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
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
Ответ
Поговорим о том, что не так в коде из предыдущего поста:
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
Поговорим о том, что не так в коде из предыдущего поста:
#include <cstdio>
void bar(char * s) {
printf("%s", s);
}
void foo() {
char s[] =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
#include <print>
#include <string_view>
void bar(std::string_view s) {
std::println("{}", s);
}
void foo() {
constexpr std::string_view s =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
❤27👍16🔥7👎4🤷♂1
std::from_chars
#новичкам
С++17 нам принес новую прекрасную функцию парсинга строк в числа - std::from_char.
На самом деле это два семейства перегрузок функций для целых чисел и чисел с плавающей точкой.
Задача функции - максимально быстро, безо всяких накладных расходов на выделение памяти и поддержку исключений, распарсить строку в число арифметического типа.
Функция возвращает структуру
Если парсинг удался и какая-то часть строки конвертировалась в число, то в ptr находится указатель на первый символ, на котором парсинг завершился. Если вся строка была интерпретирована, как число, то в ptr находится last указатель.
Если парсинг неудался, то ptr равен first, а код ошибки ec выставляется в std::errc::invalid_argument.
Примеры работы:
К тому же функция может детектировать переполнение:
В чем главный прикол этой функции?
Помимо отсутствия накладных расходов, это последовательный парсинг. Если у вас есть строка с последовательностью чисел, разделенных запятой, то вы просто в цикле можете передвигать нужные указатели и парсить числа одно за другим. Тот же std::stoi выкинул бы исключение и пошел пиво пить:
К тому же ее целочисленный вариант с С++23 constexpr, что позволить вам парсить строку в числа даже во время компиляции.
Если вы не любите исключения - std::from_char ваш выбор.
Be efficient. Stay cool.
#cpp17 #cpp23
#новичкам
С++17 нам принес новую прекрасную функцию парсинга строк в числа - std::from_char.
std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
IntegerType& value, // Куда записать результат
int base = 10 // Система счисления (2-36)
);
std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
FloatType& value, // Куда записать результат
std::chars_format fmt = std::chars_format::general // Формат плавающей точки
);
На самом деле это два семейства перегрузок функций для целых чисел и чисел с плавающей точкой.
Задача функции - максимально быстро, безо всяких накладных расходов на выделение памяти и поддержку исключений, распарсить строку в число арифметического типа.
Функция возвращает структуру
std::from_chars_result:struct from_chars_result {
const char* ptr; // Указатель на первый НЕпрочитанный символ
std::errc ec; // Код ошибки (если успех — std::errc())
};Если парсинг удался и какая-то часть строки конвертировалась в число, то в ptr находится указатель на первый символ, на котором парсинг завершился. Если вся строка была интерпретирована, как число, то в ptr находится last указатель.
Если парсинг неудался, то ptr равен first, а код ошибки ec выставляется в std::errc::invalid_argument.
"123" → удачно распарсили все → ptr == last (конец строки).
"123abc" → распарсили "123" → ptr указывает на 'a'.
"abc" → ошибка → ptr == first (начало строки).
Примеры работы:
const std::string str = "42abc";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
if (res.ec == std::errc()) {
std::cout << "Value: " << value << "\n"; // 42
std::cout << "Remaining: " << res.ptr << "\n"; // "abc"
}
// ----------------
const std::string str = "xyz";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(res.ec == std::errc::invalid_argument);
assert(res.ptr == str.data()); // ptr остался на начале
К тому же функция может детектировать переполнение:
const std::string str = "99999999999999999999";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(res.ec == std::errc::result_out_of_range);
В чем главный прикол этой функции?
Помимо отсутствия накладных расходов, это последовательный парсинг. Если у вас есть строка с последовательностью чисел, разделенных запятой, то вы просто в цикле можете передвигать нужные указатели и парсить числа одно за другим. Тот же std::stoi выкинул бы исключение и пошел пиво пить:
const std::string str = "123,456,789";
std::vector<int> numbers;
const char* current = str.data();
const char* end = str.data() + str.size();
while (current < end) {
int value;
auto res = std::from_chars(current, end, value);
if (res.ec != std::errc()) {
std::cerr << "Parsing error!\n";
break;
}
numbers.emplace_back(value);
current = res.ptr; // Сдвигаем указатель
// Пропускаем разделитель (запятую)
if (current < end && *current == ',') {
++current;
}
}
for (int num : numbers) {
std::cout << num << " ";
}
// Вывод: 123 456 789
К тому же ее целочисленный вариант с С++23 constexpr, что позволить вам парсить строку в числа даже во время компиляции.
Если вы не любите исключения - std::from_char ваш выбор.
Be efficient. Stay cool.
#cpp17 #cpp23
1🔥42❤18👍15
Возврат ошибки. std::expected
#опытным
В С++23 появился практически идеальный класс для работы с объектами ошибки - std::expected.
Это та самая обертка над вариантом с приятным интерфейсом, о котором говорилось в прошлом посте.
По сути у std::expected в базовом интерефейсе 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator-> для ленивых со сточенными пальцами;
Преимущества нового типа
✅ Хранит только два типа: значение и ошибка.
✅ Делает код интуитивно понятнее, поскольку для создания ошибки нужно использовать
✅ Предоставляет простой и лаконичный базовый интерфейс: 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator->, кому лень писать названия методов.
С std::expeсted удобно работать, если есть всего один тип результата и один тип ошибки. Работать с std::expected<std::variant<Type1, Type2>, Error> или std::expected<Type, std::variant<Error1, Error2>> не так удобно, как просто с вариантом из трех типов. Если нужно возвращать больше ошибок, то можно пользоваться разными вариантами кодов ошибки от enuma'а до std::error_code или даже просто строкой.
Must have при работе без исключений.
Use a right semantic. Stay cool.
#cpp23
#опытным
В С++23 появился практически идеальный класс для работы с объектами ошибки - std::expected.
Это та самая обертка над вариантом с приятным интерфейсом, о котором говорилось в прошлом посте.
struct Error {
std::string message;
};
std::expected<double, Error> safe_divide(double a, double b) {
if (b == 0.0) { // здесь нужна нормальная проверка на равенство с epsilon
return std::unexpected(Error{"Division by zero"});
}
return a / b;
}
auto div_result = safe_divide(10.0, 2.0);
if (div_result.has_value()) {
std::cout << "Result: " << div_result.value() << std::endl;
} else {
std::cout << "Error: " << div_result.error().message << std::endl;
}
// или с операторами
if (div_result) { // operator bool
std::cout << "Result: " << *div_result << std::endl; // operator*
} else {
std::cout << "Error: " << div_result.error().message << std::endl;
}По сути у std::expected в базовом интерефейсе 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator-> для ленивых со сточенными пальцами;
Преимущества нового типа
std::expected по сравнению с std::variant:✅ Хранит только два типа: значение и ошибка.
✅ Делает код интуитивно понятнее, поскольку для создания ошибки нужно использовать
std::unexpected. Это особенно удобно, когда тип ошибки std::string. В этом случае использование std::unexpected{ "Something bad happens" } позволяет явно обозначить в коде, что мы не просто строку возвращаем, а сообщение об ошибке.✅ Предоставляет простой и лаконичный базовый интерфейс: 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator->, кому лень писать названия методов.
С std::expeсted удобно работать, если есть всего один тип результата и один тип ошибки. Работать с std::expected<std::variant<Type1, Type2>, Error> или std::expected<Type, std::variant<Error1, Error2>> не так удобно, как просто с вариантом из трех типов. Если нужно возвращать больше ошибок, то можно пользоваться разными вариантами кодов ошибки от enuma'а до std::error_code или даже просто строкой.
Must have при работе без исключений.
Use a right semantic. Stay cool.
#cpp23
🔥25❤13👍11
Уплощаем многомерный массив
#опытным
Иногда у вас есть коллекция элементов, для каждого из которых вы выполняете операцию, возвращающую вектор значений:
Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?
С помощью С++20 отображения std::views::join:
Это все сработает и на экране появлятся заветные чиселки.
Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте
Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:
Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.
Don't stuck in a loop. Stay cool.
#cpp20 #cpp23
#опытным
Иногда у вас есть коллекция элементов, для каждого из которых вы выполняете операцию, возвращающую вектор значений:
std::vector<int> Process(const std::string& str);
std::vector<std::string> elems = ...;
auto result_view = elems | std::views::transform([](const std::string& str) {
return Process(str);
})
Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?
С помощью С++20 отображения std::views::join:
std::vector<int> Process(const std::string& str);
std::vector<std::string> elems = ...;
auto result = elems | std::views::transform([](const std::string &str) {
return Process(str);
}) |
std::views::join | std::ranges::to<std::vector>();
std::print("{}", result);
Это все сработает и на экране появлятся заветные чиселки.
Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте
Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:
auto result = elems | std::views::transform([](const auto & elem) {
return Process(elem);
}) |
std::views::join | std::views::as_rvalue |
std::ranges::to<std::vector>();Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.
Don't stuck in a loop. Stay cool.
#cpp20 #cpp23
❤21👍13🔥7
join
#опытным
Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:
И как же сложно того же результата достичь в плюсах!
То делают через потоки:
то через std::accumulate:
Ну вы что! Стандартная строка же себе не может позволить иметь метод join, принимающий коллекцию строк и возвращающий объединенную строку с разделителями. Это же не универсально и никому не надо...
Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:
Можете обмазать все это шаблонами с головы до пят, чтобы получить универсальное решение, либо использовать этот код прям inplace, он и так довольно понятный.
И жизнь стала чуть-чуть счастливее...
Make thing simple. Stay cool.
#cpp23
#опытным
Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:
my_list = ["John", "Peter", "Vicky"]
x = " ".join(my_list)
print(x)
# OUTPUT
# John Peter Vicky
И как же сложно того же результата достичь в плюсах!
То делают через потоки:
std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";
std::ostringstream oss;
oss << vec[0];
for (size_t i = 1; i < vec.size(); ++i) {
oss << delimiter << vec[i];
}
return oss.str();
}то через std::accumulate:
std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";
return std::accumulate(
std::next(vec.begin()), vec.end(),
vec[0],
[&delimiter](const std::string& a, const std::string& b) {
return a + delimiter + b;
}
);
}Ну вы что! Стандартная строка же себе не может позволить иметь метод join, принимающий коллекцию строк и возвращающий объединенную строку с разделителями. Это же не универсально и никому не надо...
Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:
std::string join(const std::vector<std::string> &vec,
const std::string &delimiter) {
return vec | std::views::join_with(delimiter) |
std::ranges::to<std::string>();
}
Можете обмазать все это шаблонами с головы до пят, чтобы получить универсальное решение, либо использовать этот код прям inplace, он и так довольно понятный.
И жизнь стала чуть-чуть счастливее...
Make thing simple. Stay cool.
#cpp23
❤27👍11🔥9😁5
Удобно превращаем enum в число
#опытным
В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.
Но вот представьте себе, что вам где-то нужно получить числовое представление одного из перечислителя. К какому типу кастовать?
Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:
Если вам просто нужно вывести число в поток, то кастуйте к инту, ничего страшного не будет. Однако математические операции над полученным числом могут доставить неприятности, если тип будет не тот и будут использоваться сужающие-расширяющие преобразования.
Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.
С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:
Соответственно, для каста нужно сделать такую штуку:
Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:
Красота!
Know your type. Stay cool.
#cpp11 #cpp23
#опытным
В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.
Но вот представьте себе, что вам где-то нужно получить числовое представление одного из перечислителя. К какому типу кастовать?
Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:
enum class ColorMask : std::uint32_t
{
red = 0xFF,
green = (red << 8),
blue = (green << 8),
alpha = (blue << 8)
};
// std::cout << ColorMask::red << std::endl; // ERROR
std::cout << static_cast<int>(ColorMask::red) << std::endl;
Если вам просто нужно вывести число в поток, то кастуйте к инту, ничего страшного не будет. Однако математические операции над полученным числом могут доставить неприятности, если тип будет не тот и будут использоваться сужающие-расширяющие преобразования.
Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.
С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:
enum e1 {};
enum class e2 {};
enum class e3 : unsigned {};
enum class e4 : int {};
constexpr bool e1_t = std::is_same_v<std::underlying_type_t<e1>, int>;
constexpr bool e2_t = std::is_same_v<std::underlying_type_t<e2>, int>;
constexpr bool e3_t = std::is_same_v<std::underlying_type_t<e3>, int>;
constexpr bool e4_t = std::is_same_v<std::underlying_type_t<e4>, int>;
std::cout
<< "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n';
// OUTPUT
// underlying type for 'e1' is non-int
// underlying type for 'e2' is int
// underlying type for 'e3' is non-int
// underlying type for 'e4' is intСоответственно, для каста нужно сделать такую штуку:
auto num = static_cast<std::underlying_type_t<ColorMask>>(ColorMask::red);
Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:
auto num = std::to_underlying(ColorMask::red);
Красота!
Know your type. Stay cool.
#cpp11 #cpp23
👍21🔥14❤9🥱1
Оборачиваем вспять байты
#новичкам
Когда мы низкоуровнево работаем с сетью, то надо понимать, что в данных, полученных по сети, нужно реверсировать порядок байтов, чтобы правильно интерпретировать значения. Также реверсировать порядок нужно при отправке данных по сети. Это происходит из-за того, что в стеке протоколов TCP/IP принят порядок Big-endian - старший байт хранится по младшему адресу. А на большинстве хостов(десктопов и серверов) - Little-endian: младший байт хранится по младшему адресу.
Соответственно нужны функции для реверсирования байтов. Обычно для этого используют либо компиляторные интринсики:
Либо системное апи:
Либо какое-нибудь библиотечное решение:
Но в С++23 появилась стандартная функция для разворачивания порядка байтов!
Работает она только для интегральных типов и вот ее возможная реализация:
Результат у нее собственно ровно тот, который и ожидается:
Как всегда стандарт запаздывает лет на 10-15-20, но хорошо, что все-таки завезли эту полезную функцию, которую можно кроссплатформенно использовать.
Use standard solutions. Stay cool.
#cpp23
#новичкам
Когда мы низкоуровнево работаем с сетью, то надо понимать, что в данных, полученных по сети, нужно реверсировать порядок байтов, чтобы правильно интерпретировать значения. Также реверсировать порядок нужно при отправке данных по сети. Это происходит из-за того, что в стеке протоколов TCP/IP принят порядок Big-endian - старший байт хранится по младшему адресу. А на большинстве хостов(десктопов и серверов) - Little-endian: младший байт хранится по младшему адресу.
Соответственно нужны функции для реверсирования байтов. Обычно для этого используют либо компиляторные интринсики:
### GCC/Clang
uint16_t swapped16 = __builtin_bswap16(value);
uint32_t swapped32 = __builtin_bswap32(value);
uint64_t swapped64 = __builtin_bswap64(value);
### MSVC:
uint16_t swapped16 = _byteswap_ushort(value);
uint32_t swapped32 = _byteswap_ulong(value);
uint64_t swapped64 = _byteswap_uint64(value);
Либо системное апи:
#include <arpa/inet.h> // Linux/macOS
// или
#include <winsock2.h> // Windows
uint16_t network_to_host16 = ntohs(value);
uint16_t host_to_network16 = htons(value);
uint32_t network_to_host32 = ntohl(value);
uint32_t host_to_network32 = htonl(value);
uint64_t network_to_host64 = ntohll(value);
uint64_t host_to_network64 = htonll(value);
Либо какое-нибудь библиотечное решение:
#include <boost/endian/conversion.hpp>
uint32_t value = 0x12345678;
uint32_t swapped = boost::endian::endian_reverse(value);
uint32_t to_big = boost::endian::native_to_big(value);
uint32_t to_little = boost::endian::native_to_little(value);
Но в С++23 появилась стандартная функция для разворачивания порядка байтов!
template< class T >
constexpr T byteswap( T n ) noexcept;
Работает она только для интегральных типов и вот ее возможная реализация:
template<std::integral T>
constexpr T byteswap(T value) noexcept
{
static_assert(std::has_unique_object_representations_v<T>,
"T may not have padding bits");
auto value_representation = std::bit_cast<std::array<std::byte, sizeof(T)>>(value);
std::ranges::reverse(value_representation);
return std::bit_cast<T>(value_representation);
}
Результат у нее собственно ровно тот, который и ожидается:
template<std::integral T>
void dump(T v, char term = '\n')
{
std::cout << std::hex << std::uppercase << std::setfill('0')
<< std::setw(sizeof(T) * 2) << v << " : ";
for (std::size_t i{}; i != sizeof(T); ++i, v >>= 8)
std::cout << std::setw(2) << static_cast<unsigned>(T(0xFF) & v) << ' ';
std::cout << std::dec << term;
}
int main()
{
static_assert(std::byteswap('a') == 'a');
std::cout << "byteswap for U16:\n";
constexpr auto x = std::uint16_t(0xCAFE);
dump(x);
dump(std::byteswap(x));
std::cout << "\nbyteswap for U32:\n";
constexpr auto y = std::uint32_t(0xDEADBEEFu);
dump(y);
dump(std::byteswap(y));
std::cout << "\nbyteswap for U64:\n";
constexpr auto z = std::uint64_t{0x0123456789ABCDEFull};
dump(z);
dump(std::byteswap(z));
}
// OUTPUT
// byteswap for U16:
// CAFE : FE CA
// FECA : CA FE
// byteswap for U32:
// DEADBEEF : EF BE AD DE
// EFBEADDE : DE AD BE EF
// byteswap for U64:
// 0123456789ABCDEF : EF CD AB 89 67 45 23 01
// EFCDAB8967452301 : 01 23 45 67 89 AB CD EF
Как всегда стандарт запаздывает лет на 10-15-20, но хорошо, что все-таки завезли эту полезную функцию, которую можно кроссплатформенно использовать.
Use standard solutions. Stay cool.
#cpp23
❤28👍12😁8🔥5
Атрибуты лямбды
#опытным
В прошлом посте код с картинки реально компилируется и, если вы не поняли, что это за чертовщина, то следующие несколько постов будут для вас.
В С++11 у нас появилась возможность указывать атрибуты для функции. Например:
Вы можете, например, пометить возвращаемое значение функции, как то, которое нельзя игнорировать, и компилятор даст вам по сопатке, если вы его все же заигнорите.
Ну это функции. А как же лямбды? Хочется и для них указывать атрибуты.
И атрибуты для возвращаемого значения лямбды завезли в С++23. Выглядит это так:
После скобок для захвата вы указываете список атрибутов в квадратных скобках. Выглядит интересно. Не очень элегантно, но интересно.
Одни скажут: "усложнение синтаксиса!". Другие скажут, что давно пора лямбды подтягивать ко всем возможностям обычных функций.
Тут как бы все просто: не хотите - не используйте. У лямбды и так полно опциональных обвесок, одним больше, одним меньше. Можно определить шаблонную лямбду и обвесить ее всякими концептами с trailing return type. И это будет страшный зверь. Можно сделать отдельный пост, как может выглядеть ультимативная лямбда.
Ну а если вы хотите немного больше синтаксически говорить кодом, то теперь можете использовать атрибуты для лямбд.
Don't ignore. Stay cool.
#cpp23
#опытным
В прошлом посте код с картинки реально компилируется и, если вы не поняли, что это за чертовщина, то следующие несколько постов будут для вас.
В С++11 у нас появилась возможность указывать атрибуты для функции. Например:
[[nodiscard]] int ComplicatedCompute() {
return 2*2;
}
ComplicatedCompute();
// warning: ignoring return value of 'int ComplicatedCompute()',
// declared with attribute nodiscardВы можете, например, пометить возвращаемое значение функции, как то, которое нельзя игнорировать, и компилятор даст вам по сопатке, если вы его все же заигнорите.
Ну это функции. А как же лямбды? Хочется и для них указывать атрибуты.
И атрибуты для возвращаемого значения лямбды завезли в С++23. Выглядит это так:
auto complicated_compute = [] [[nodiscard]] () { return 2 * 2; };
complicated_compute();
// warning: ignoring return value of 'main()::<lambda()>',
// declared with attribute 'nodiscard'После скобок для захвата вы указываете список атрибутов в квадратных скобках. Выглядит интересно. Не очень элегантно, но интересно.
Одни скажут: "усложнение синтаксиса!". Другие скажут, что давно пора лямбды подтягивать ко всем возможностям обычных функций.
Тут как бы все просто: не хотите - не используйте. У лямбды и так полно опциональных обвесок, одним больше, одним меньше. Можно определить шаблонную лямбду и обвесить ее всякими концептами с trailing return type. И это будет страшный зверь. Можно сделать отдельный пост, как может выглядеть ультимативная лямбда.
Ну а если вы хотите немного больше синтаксически говорить кодом, то теперь можете использовать атрибуты для лямбд.
Don't ignore. Stay cool.
#cpp23
❤21👍9🔥8😁3