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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Компилятору достаточно лишь правильно положить аргументы функции в правильные регистры, согласно calling conventions. Какие аргументы нужно подготовить компилятор знает заранее, так как сигнатура виртуальных методов одинакова для всех переопределенных вариантов. Остается лишь call'ьнуть нужный указатель и происходит виртуальный вызов.

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

Know whats under the hood. Stay cool.

#compiler #cppcore
24👍14🔥9
​​Наследие Cfront. Манглирование
#новичкам

В С нет перегрузки функций, поэтому там люди вынуждены каким-то образом руками разделять имена функций.

Вместо

void print(int x);
void print(float x);


Там пишут:

void print_int(int x);
void print_float(float x);


Плюс линкеры работают с именами символов. А имя функции в С не включает в себя параметры. Поэтому во всей программе не может быть двух функций с одинаковыми именами. Линкер их банально не различит и выдаст ошибку множественного определения.

А в С++ была перегрузка и надо было каким-то образом плюсовые перегруженные функции превращать в неперегруженные сишные при трансляции кода. Для этого было придумано декорирование имен или name mangling.

Самый простой способ - добавлять к конце функции ее параметры в закодированном виде: ИмяФункции_ТипыПараметров:

void draw(int x, int y);
void draw(double x, double y);

// Cfront mangling:
draw_i_i // draw(int, int)
draw_d_d // draw(double, double)


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

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

👉🏿 2 разных класса могут иметь методы с одинаковым названием. Так как при трансляции в С это были просто свободные функции, манглинг должен учитывать и имя класса:

void Circle::paint(Color c);
void Square::paint();
Rectangle::~Rectangle();
Shape::~Shape();

// transform into

void Circle_paint_Color(struct Circle* this, struct Color c);
void Square_paint(struct Square* this);
void Rectangle_dtor(struct Rectangle* this);
void Shape_dtor(struct Shape* this);


👉🏿 Есть же еще и пространства имен. Они помогают разграничить скоуп существования имен. И названия пространства имен тоже манглировались в имена функций:

namespace Graphics {
class Canvas {
void clear();
};
}

// transform into

void Graphics_Canvas_clear(struct Canvas* this)


👉🏿 В С++ когда-то появились шаблоны. Шаблон всегда инстанцируется с каким-то типом. И чтобы различать эти инстанциации, Cfront манглировал типы шаблонных параметров в полное имя типа:

template<typename T>
class Stack {
void push(T value);
};

Stack<int> stack;

// transforms into

Stack_int_push_i(int value);


Конкретные преобразованные имена из поста могут быть не такими, какими их генерировал Cfront, но главное уловить идею.

В современных компиляторах тоже делается манглинг имен, чтобы линкер не ругался на одинаковые символы:


void Circle::rotate(int);

// transforms into

_ZN6Circle6rotateEi

// Разбор:
_Z - префикс C++
N - вложенное имя
6Circle - длина=6, "Circle"
6rotate - длина=6, "rotate"
E - конец аргументов
i - тип int


Шаблоны, неймспейсы, noexcept и const квалификаторы - все вшивается в имя символа.

Вы также можете вручную управлять манглингом: включать и выключать его:

extern "C" void c_function();   // Без манглинга: _c_function
extern "C++" void cpp_function(); // С манглингом: _Z10cpp_functionv


Это нужно для совместимости ABI интерфейсов, предоставляемых библиотеками.

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

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
22👍21🔥13
Новый год

Вот и подходит к своему завершению 2025 год.

Самое время подвести его итоги. Но я не буду перечислять список полезных или популярных постов. Кто захочет, то найдет. Я хочу поделиться состоянием(вайбом для зумеров) от завершающего свой последний аккорд года.

⚡️ Меня радует, что качество постов наконец-то меня удовлетворяет. Не по обертке(всегда бывают косяки), а по сути, структурированности и целостности. Можете ради интереса вернуться к первым постам и проследить эволюцию стиля. Количество юморесок и степень свободы изложения заметно сократились в угоду системности. Это логичная проф деформация, когда каждый раз при написании постов делаешь инъекцию 3-х кубиков стандарта внутривенно. Это несомненно позитивные изменения. Но теперь хочется, чтобы маятник вновь качнулся в сторону развлекательного аспекта. Все-таки мы здесь покекать пришли, а не плюсы учить. Или у вас не так?)

⚡️ Канал свел меня с прекрасными людьми, с которыми у нас началось сотрудничество по части плюсов и не только. Я очень вырос благодаря этим связям и не могу наудивляться, насколько у нас здесь клевые ребята собрались.

⚡️ Очень радует, что подписчики все чаще предлагают какие-то идеи для постов, особенно лично. Благодаря некоторым появились даже целые рубрики. Видно, что наше коммьюнити живое и активно развивается. У нас можно и поболтать за жизнь, и похоливарить на тему языков, и узнать что-то новенькое от крутых спецов. Есть более профессиональные чаты для гурманов и заядлых сомелье С++, но мне кажется, что определенную нишу мы заняли.

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

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

Желаю вам в будущем году плотно и потно трудиться. Легко не будет, особенно начинающим, но у вас все получится. Идите к своим целям на всех парах. Верю в вас всех и каждого.

С Новым годом, друзья! С новым счастьем!☃️🎊🎉🎄
2392🎄22👍1613❤‍🔥3👎1🗿1
​​Праздник Баг к нам приходит

Врываемся в новый год с поучительных историй.

На дворе 31 декабря 2к21 года. В России все нарезают салаты, готовятся веселиться и еще не знают, какой год их ждет впереди. На западе тоже все еще празднуют, хотя Рождество уже позади.

В первые минуты января сверкает салют, хлопаются хлопушки, шампанское переливается через края бокалов и перестает доставляться вся электронная почта, за обработку и доставку которой отвечал Microsoft Exchange Server.

Вот так в одну секунду по всему миру куча организаций перестала получать свою почту. В журнале событий сервера можно было найти такую запись: «Процесс сканирования FIP-FS Scan Process не прошел инициализацию. Ошибка: 0x8004005. Подробности ошибки: «Неопределенная ошибка» или «Код ошибки: 0x80004005». Описание ошибки: «Не удается преобразовать "2201010001" в длинное число».

Оказывается, что Exchange сохранял в журнале проверок антивируса даты в виде int32. Максимальным значением может быть 2147483647. У числа может быть всего 10 десятичных разрядов: первые 2 числа - последние 2 цифры года, дальше month/day/time. Для 2021 все было ок. Но чтобы представить первую секунду 2022 года нужно число 2201010001. А это уже перебор. Конвертация навернулась и полетели ошибки.

Эту проблему тогда обозвали Y2K22 bug, по аналогии с багом 2000 года(Y2K). В 20-м веке люди тоже часто обрабатывали только последние 2 цифры даты и в момент миллениума года сбрасывались просто до нуля.

Стоит ли говорить, что малварьный модель FIP-FS был скорее всего написан на С/C++?

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

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

А вы сталкивались с подобными ошибками? Расскажите в комментах.

Celebrate the holiday. Stay cool.

#fun
👍2310🔥6😁4🫡3
​​Наследие Cfront. Компоновка
#новичкам

Продолжаем серию постов с прошлого года. Для тех, кто не в теме: пост про Cfront тут, начало серии тут.

С++ оказывал влияние не только на компиляторы, но и на линковщики.

Если в первых версиях языка и компилятора Cfront Бьерн сфокусировался на ООП и полиморфизме, то дальше были введены шаблоны и inline функции.

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

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

Поначалу для решения этой проблемы использовались разные хаки: от макросов и магии с именами до ручной инстанциации шаблонов в одном cpp файле.

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

Также в С++ появились глобальные объекты. А глобальные объекты требуют своей инициализации (то есть выполнения кода) до main. В сижке такого нет, там исполнение пользовательского кода начинается с вызова main. Кстати поэтому в С нет SIOF.

Поэтому приходилось извращаться. Формировать массив конструкторов глобальных объектов и вызывать его первой инструкцией main. Разрушение происходило в конце main с помощью массива деструкторов.

Но это костыль и нужно было нормальное решение. Результатом совместной работы Cfront и линковщиков стали секции .ctors/.dtors в объектных и бинарных файлах. Там находятся информация о том, какие глобальные пользовательские объекты есть в коде. Код конструкторов и деструкторов объектов из этих секций выполняется до и после main соответственно.

Таким было наследие Cfront. The end.

Have a legacy. Stay cool.

#compiler
👍27🔥118
​​Cppfront
#опытным

Есть интересный проект у Герба Саттера - компилятор Cppfront. По аналогии с Cfront, который компилировал С++ в С, Cppfront компилирует экспериментальный синтаксис Cpp2 в наш привычный C++.

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

Если просто: Герб и другие активные деятели С++ из комитета придумали Cpp2 и написали препроцессор, который преобразует новый синтаксис в наш привычный С++.

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

С появлением Cppfront, если кто-то хочет завести в стандарт С++ какую-то новую функциональность(например pattern matching), то ее можно относительно быстро реализовать в Сpp2 и поиграться с ней на практике. Коммьюнити может почелленджить решение и его использование, прочекать граничные и сложные случаи и тд.

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

И это будет работать на любой платформе, где есть С++ компилятор!

Простой пример кода на Cpp2:

#include <iostream>
#include <string>

name: () -> std::string = {
s: std::string = "world";
decorate(s);
return s;
}

decorate: (inout s: std::string) = {
s = "[" + s + "]";
}

auto main() -> int {
std::cout << "Hello " << name() << "\n";
}


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

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

Вот ссылочка на гитхаб и на доку.

Help to evolve. Stay cool.

#compiler
🔥31👍1511
​​Время жизни и range-based for
#новичкам

Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


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

Перед ответом экскурс в стандарт. Есть у вас range-based for:

for(const auto& item: <range>) {
process(item);
}


Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:

auto&& range = <range>;
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
process(item);
}


Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.

std::vector<int> generate() {
return {1, 2, 3, 4, 5};
}
for (const auto& item: generate()) {
...
}
// range-based for transforms into
auto&& range = generate();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
...
}

В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.

А вот во что преобразуется цикл из самого первого примера поста:

auto&& range = generateData().items();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
int x = *it;
process(x);
}


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

Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

auto data = generateData();
for (int x : data.items()) {
process(x);
}


Другие решения рассмотрим в следующих постах.

Avoid dangling references. Stay cool.

#cppcore #cpp11
33🔥23👍8😁5
Инициализация внутри range-based for
#новичкам

Решение из предыдущего поста не совсем идеальное. Посмотрите сами:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

auto data = generateData();
for (int x : data.items()) {
process(x);
}


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

Было бы идеально инициализировать data прям внутри скоупа цикла, как это делается в условных операторах. И с С++20 это стало возможным:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (auto data = generateData(); int x : data.items()) {
process(x);
}


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

Кстати. А задавались вы вопросом "нужны ли инструкции инициализации в цикле while?"

В if есть, в во всех вариантах цикла for есть. Для единообразия нужно и в while.

Как думаете?

На самом деле это будет избыточно. while+инициализация выглядела бы так:

while(size_t i = 0; some_condition) {...}


Это легко заменяется на классический for без указания инструкций, которые после каждой итерации выполняются:

for(size_t i = 0; some_condition;) {...}


Даже короче получилось. Поэтому инициализация в while не нужна.

Forget about garbage. Stay cool.

#cpp20
25👍14🔥6
​​Продлеваем жизнь временного объекта range based for
#опытным

На самом деле у проблемы в этом коде:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


есть еще более простое решение.

Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.

Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:

using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t1) { return t1; } // всегда возвращает висячую ссылку
T g();

void foo()
{
for (auto e : f1(g())) {} // OK: время жизни возвращаемого значения g() продлено
for (auto e : f2(g())) {} // UB: локальный объект t1 функции f2 все равно разрушается при выходе из скоупа f2
}


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

Вроде круто, но задумайтесь на секунду.

UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.

То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.

А как вы думаете: полезное изменение?

👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.

Solve problems. Stay cool.

#cpp23
👍351410🔥5
​​Предотвращаем висячие ссылки
#опытным

Давайте снова взглянем на этот пример:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:

auto& vec = generateData().items();


Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.

Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.

Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.

Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.

Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.

Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?

И это действительно решает проблему.

Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}

~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.

Это также прекрасно решает проблему.

Prevent misuse. Stay cool.

#cpp11 #cpp20 #cpp23 #goodpractice
🔥2013👍9🤯3
​​split
#опытным

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

Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:

text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']


А как сделать простейшую вещь разделить строку на С++?

Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:

std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four


Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.

Поэтому люди городили свои огороды через find, стримы и прочее.

Но хочется чего-то родного.. Чего-то стандартного...

И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:

auto range = text | std::views::split(' ');

for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three four


Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:

auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]


Правда здесь уже нужен С++23 с его std::ranges::to.

У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:

std::string text = "one two  three   four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]


Все примеры можете найти здесь.

И жить стала еще чуть прекрасней)

Never too late. Stay cool.

#cpp20 #cpp23
🔥38👍119🤯2👎1
​​Прелести рэнджей
#опытным

Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.

Возьмем пример из прошлого поста:

std::string text = "one two three four";
std::vector<std::string_view> strs;
boost::split(strs, text, boost::is_any_of(" "));


Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.

И на самом деле мы получаем кучу аллокаций.

Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.

Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.

Но можно поступить проще и воспользоваться ренджами!

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

Но у рэнджей это скорее преимущество.

Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.

Мы просто пишем:

auto range = text | std::views::split(' ');


И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.

С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:

auto range = text | std::views::split(' ') | std::views::drop(2) | std::views::take(1);


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

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

if (range.empty()) {
std::cerr << "There are less than three words in text";
}


Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:

std::string_view str{*range.begin()};
std::cout << str << std::endl;
// OUTPUT: three


И здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.

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

Be expressive. Stay cool.

#cpp20
👍27🔥149😁3
std::to_array
#опытным

std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):

std::array<int, 4> arr = {1, 2, 3, 4};


либо надеяться на CTAD и не передавать никаких параметров:

std::array arr = {1, 2, 3, 4};


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

И вот здесь всплавает тот самый нюанс.

Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:

std::array<std::string_view> arr = {"La", "Bu", "Bu"};


и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.

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

У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:

namespace detail {
template <class T, std::size_t N, std::size_t... I>
constexpr std::array<std::remove_cv_t<T>, N>
to_array_impl(T (&a)[N], std::index_sequence<I...>) {
return {{a[I]...}};
}
} // namespace detail

template <class T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T (&a)[N]) {
return detail::to_array_impl(a, std::make_index_sequence<N>{});
}


Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.

Применяется std::to_array так:

constexpr auto names = std::to_array<std::string_view>({"Goku", "Luffy", "Ichigo", "Gojo", "Joseph", "L"});


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

Automate your tools. Stay cool.

#cpp20
26👍15🔥9🥱21
​​WAT
#опытным

Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.

Делаем вот так:

constexpr std::array array = {"I", "love", "C++"};

int main() {
if (auto iter = std::ranges::find(array, "C++"); iter == std::end(array)) {
assert(false && "comptime arg");
}
// let's go with runtime now
if (setenv("RUNTIME", array[2], 0) != 0) {
assert(false && "setenv");
}
char *runtime_str = getenv("RUNTIME");
assert(strcmp(runtime_str, array[2]) == 0 && "equal strings");

if (auto magick_iter = std::ranges::find(array, runtime_str);
magick_iter == std::end(array)) {
assert(false && "runtime arg");
}
}


Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.

Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.

После получаем значение этой переменной и сравниваем ее с оригиналом.

Ну и в конце ищем среди массива строку эту runtime_str.

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

Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."

WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?

Дьявол кроется в деталях.

Вспоминаем школьную математеку CTAD. Какой тип элементов массива выведется?

Правильно, const char *.

А как std::ranges::find сравнивает такие элементы?

Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.

Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.

Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.

А вот последний ассерт просто говорит о том, что указатель runtime_str не был найден в массиве, потому что там нет такого адреса.

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

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

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

Express your wishes precisely. Stay cool.

#cppcore #cpp17
21👍10😁7🔥5👎1
​​Как получить длину строкового литерала?
#опытным

Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод size:

size_t length = std::string("Hello, subscribers!").size();


Ну или на худой конец вызовем strlen:

size_t length = strlen("Hello, subscribers!");


Но я считаю, это не по-современному.

С++ давно идет в сторону расширения возможностей вычислений в compile-time. Поэтому если что-то можно вычислить во время компиляции, то это нужно сделать именно там! Ни грамма лишнего времени вычислений не потратим.

Давайте посмотрим, как можно найти длину строкового литерала во время компиляции.

1️⃣ Кастомщина. Хочешь что-то сделать хорошо, сделай это сам. Не факт, что получится хорошо, но ты старался:

template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return N - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");


Реальный тип строкового литерала не const char *, а константный массив символов. Поэтому через шаблон мы можем подтянуть размер массива через NTTP-параметр шаблона и вернуть его наружу.

2️⃣ Используем sizeof. Этот оператор возвращает длину массива во время компиляции. Единственное, что он считает терминирующий символ, поэтому все равно вокруг него надо обертку писать, чтобы единичку нигде не потерять:

template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return sizeof(str) - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");


Эх, а так хотелось готового get-to-go решения. Погодите...

3️⃣ Обернуть не в строку, а в string_view и вызвать метод size(). Конструкторы вьюхи изначально с С++17 были constexpr, как и сам метод size(), поэтому просто берем и пишем:

constexpr size_t len = std::string_view("Hello, subscribers!").size();


Просто, работает из коробки и знакомо всем.

4️⃣ Да зачем что-то менять в коде, это для слабаков. Поменяем стандарт и все заработает в compile-time! Ну точнее конструктор std::string и метод size() в С++20 теперь тоже constexpr:

constexpr size_t len = std::string("Hello, subscribers!").size();


Пысы: я не просто вызываю какие-то функции в надежде, что они выполнятся в compile-time. Тот факт, что len - constexpr переменная, требует, чтобы компилятор вычислил выражение справа во время компиляции.

5️⃣ Тот пункт, который и вдохновил на написание этого поста. Все пункты выше либо надо было самим реализовывать, либо вот какие-то обертки, чтобы хакнуть систему и на самом деле не работать с литералами.

Но не так плюсы бедны на стандартные решения. Есть стандартная С++17 функция std::char_traits<char>::length. Она может работать в compile-time, имеет явную семантику вычисления длины и работает чисто с c-style строками:

constexpr size_t len = std::char_traits<char>::length("Hello, subscribers!");


Красиво? Ну а что вы от плюсов хотели?) Зато из коробки работает.

6️⃣ Пользовательские литералы. Еще один неординарный способ. С С++11 мы имеем возможность превращать численные и строковые литералы в пользовательские объекты с помощью дописывания суффикса. Прикольно же писать:

constexpr auto length = "Hello, subscribers!"_len;


Коротко и понятно. Для этого нужно лишь определить оператор преобразования:

constexpr size_t operator"" _len(const char* str, size_t n) {
return n;
}


и теперь вы свободны от угнетения оберток.

Если есть еще идеи, кидайте в комменты, будет интересно.

Don't be oppressed. Stay cool.

#cpp11 #cpp17 #cpp20
🔥43👍168
Пользовательские литералы
#новичкам

Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.

В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:

👉🏿 Целочисленные: 5, 42, 0xFF.

👉🏿 С плавающей точкой: 3.14, 6.02e23.

👉🏿 Символьные: 'a', '\n'.

👉🏿 Строковые: "Hello, world!".

👉🏿 Логические: true, false.

👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя - nullptr.

Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.

Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.

А мы живем все-таки в мире объектов
. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.

Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.

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

Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!

Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:

class Temperature {
private:
double kelvin; // for precicion and consistency

explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};

Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}

{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}


Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.

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

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

Extend your capabilities. Stay cool.

#cppcore #cpp11
24👍12🔥11🤯2🤷‍♂1🤪1
​​Пользовательские литералы. А зачем?
#опытным

В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.

Поехали:

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

auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;


Меньше деталей, больше фокуса на происходящем.

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

double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree


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

Вот шобы такого не было, можно использовать соответствующие литералы:

class Radian {...};

Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}

SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error


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

Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:

using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char *>);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);


Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.

🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:

template<size_t N>
struct FixedString {
char data[N];

constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}

constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};

template <FixedString str>
class Class {};

Class<"Hello World!"> cl;


И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.

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

Be useful. Stay cool.

#cppcore #cpp11 #cpp20
👍149🔥7😁1
Привычно возводим в степень

Спасибо, @Ivaneo, за любезно предоставленную идею для поста.

Как же бесило в универе, что в С/С++ нет нормального оператора возведения в степень, приходится использовать библиотечную std::pow. В других же языках такое есть. Например в питоне и рубях это оператор **(2 ** 3), а в Lua и Julia - оператор ^(2 ^ 3).

В С++ мы такого в общем виде получить, к сожалению, не можем. Но можем сделать даже лучше в очень определенном сценарии.

И в этом нам помогут пользовательские литералы. Смотрите сами:

long double operator ""_²(long double d)
{
return d * d;
}

int main()
{
auto d = 2.0_²;
std::cout << d << "\n";
}
// OUTPUT:
// 4


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

А если еще и суффикс убрать:

long double operator ""²(long double d)
{
return d * d;
}

int main()
{
auto d = 2.0²;
std::cout << d << "\n";
}


То будет вообще огонь! Прям как в школе учили.

Да, суффиксы без андерскора запрещено использовать, так как они зарезервированы для стандарта. Но тем не менее это работает с варнингами на gcc и msvc, но уже не собирается на кланге.

Забавный примерчик. Жаль, что это может работать только для литералов и не распространяется на все переменные.

Provide better solutions. Stay cool.

#fun
24🤣22👍11🔥6😁3🗿1
Забавный факт про std::unordered_map
#опытным

std::unoredered_map обязана работать на базе хэш-таблицы, чтобы удовлетворить требованиям по асимптотической сложности ее операций.

А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранит связный список элементов, у которых одинаковый ключ.

При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):

std::unoredered_map<std::string, int> map = ...;
for (const auto& [key, value]: map) {
...
}


Но это не единственный способ итерироваться по мапе!

У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:

local_iterator begin( size_type n );
local_iterator end( size_type n );


Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!

std::unordered_map<std::string, int> word_count = {
{"AI", 5}, {"evil", 7}, {"banana", 3},
{"date", 2}, {"elderberry", 4}
};

// Iterate over backets
for (size_t i = 0; i < word_count.bucket_count(); ++i) {
std::cout << "Bucket " << i << " ("
<< word_count.bucket_size(i) << " elements): ";

// Iterate inside certain backet
for (auto it = word_count.begin(i); it != word_count.end(i); ++it) {
std::cout << "[" << it->first << ":" << it->second << "] ";
}
std::cout << std::endl;
}


Вывод:

Bucket 0 (0 elements): 
Bucket 1 (0 elements):
Bucket 2 (2 elements): [date:2] [evil:7]
Bucket 3 (0 elements):
Bucket 4 (0 elements):
Bucket 5 (2 elements): [elderberry:4] [banana:3]
Bucket 6 (0 elements):
Bucket 7 (0 elements):
Bucket 8 (0 elements):
Bucket 9 (0 elements):
Bucket 10 (0 elements):
Bucket 11 (1 elements): [AI:5]
Bucket 12 (0 elements):


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

Inspect your solutions. Stay cool.

#cpp11
🔥40😁158👍6🤯2❤‍🔥1
​​Стандартные пользовательские литералы. Строковые
#новичкам

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

Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без underscore'а приятнее визуально.

Вторая особенность - нужно обязательно указывать using namespace std::literals помимо включения нужных хэдэров. Кастомный оператор - это по сути обычная функция. И при вызове функции из какого-то пространства имен(а все стандартное лежит как минимум в неймспейсе std) мы должны перед именем функции указать это пространство. Но как вы это сделаете с оператором? Да никак. Поэтому явно нужно использовать в своем коде неймспейс. Он общий для всех стандартных операторов, но есть еще и подпространства под конкретные их группы.

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

Строковые кастомные литералы

Интересно, что для них операторы принимают 2 параметра: указатель и длину:

( const char*, std::size_t )


Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.

Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:

1️⃣ std::string:
constexpr std::string operator""s(const char* str, std::size_t len);

using namespace std::literals;
auto str = "Hello, World!"s;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string>);


2️⃣ std::string_view:
constexpr std::string_view
operator ""sv(const char* str, std::size_t len) noexcept;

using namespace std::literals;
auto str = "Hello, World!"sv;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string_view>);


Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.

У них у обоих есть одна особенность. Так как размер строки передается в оператор и этот размер потом используется для создания объекта, то есть некоторые отличия при создании объектов через конструктор и через оператор:

void print_with_zeros(const auto note, const std::string& s) {
std::cout << note;
for (const char c : s)
c ? std::cout << c : std::cout << "₀";
std::cout << " (size = " << s.size() << ")\n";
}
int main() {
using namespace std::string_literals;

std::string s1 = "abc\0\0def";
std::string s2 = "abc\0\0def"s;
print_with_zeros("s1: ", s1);
print_with_zeros("s2: ", s2);
}

// OUTPUT:
// s1: abc (size = 3)
// s2: abc₀₀def (size = 8)


Во втором случае получилась строка длиннее, чем в первом. Почему?

Для s1 вызывается конструктор от одного аргумента:

basic_string( const CharT* s, const Allocator& alloc = Allocator() );


Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки.

Для s2 вызывается конструктор от двух аргументов:

basic_string( const CharT* s, size_type count,
const Allocator& alloc = Allocator() );


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

Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.

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

See the difference. Stay cool.

#cpp11 #cpp17
28🔥12👍8😁7🤔1
​​Стандартные пользовательские литералы. Числовые
#новичкам

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

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

Мы все привыкли писать: или 34мин. И начиная с С++14 мы примерно так и можем оперировать временем. Есть операторы преобразования целых и дробных чисел в годы, дни, часы, минуты, секунды, миллисекунды, микросекунды и наносекунды:

using namespace std::literals;

auto ns = 100ns; // наносекунды
auto us = 100us; // микросекунды
auto ms = 100ms; // миллисекунды
auto s = 100s; // секунды
auto min = 100min; // минуты
auto h = 24h; // часы

auto d = 42d; // дни
auto y = 12y; // года


Вы также можете верхнюю шестерку операторов использовать совместно в операциях:

auto time = 1h + 30min + 90s;


Они совместимы и в результате получается объект общего типа(какой конкретно зависит от реализации, но скорее всего std::chrono::seconds в данном случае)

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

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

std::promise<void> promise;
auto future = promise.get_future();

dispatcher_->schedule([&] { promise.set_value(); });

EXPECT_EQ(future.wait_for(100ms), std::future_status::ready);


если по истечению 100ms у фьючи не будет статуса "готово", то тест падает.

Интересный факт: оператор суффикс s конфликтует своим именем с оператором преобразования к строке. Но проблема решается автоматически разным типом аргументов. Никаких реальных конфликтов, обычная перегрузка:

std::string_literals::operator"" s(const char*, size_t)
std::chrono_literals::operator"" s(unsigned long long)


Ну и еще есть литералы для комплексных чисел:

using namespace std::literals;

auto c1 = 1.0 + 2.0i; // std::complex<double>(1.0, 2.0)
auto c2 = 3.0i; // std::complex<double>(0.0, 3.0)
auto c3 = 4.0if; // std::complex<float>(0.0f, 4.0f)
auto c4 = 5.0il; // std::complex<long double>(0.0L, 5.0L)


Работает там примерно так же, как в математике.

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

Be useful. Stay cool.

#cpp14
25😁17🔥12👍31