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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Висячие ссылки в лямбдах
#новичкам

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

В C++11 появились лямбда-выражения, а вместе с ними ещё один способ прострелить себе причинное место.

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

auto make_add_n(int n) {
return [&](int x) {
return x + n; // n will become dangling reference!
};
}

auto add5 = make_add_n(5);
std::cout << add5(5) << std::endl; // UB!


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

Еще более интересная ситуация с объектами и методами.

struct Task {
int id;

std::function<void()> GetNotifier() {
return [=]{
std::cout << "notify " << id << std::endl;
};
}
};

int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}


Что же здесь может провиснуть? Никаких локальных объектов в методе GetNotifier нет.

На самом деле провиснет сам объект, на котором вызывается GetNotifier. Мы его аккуратненько и довольно неявненько захватили через копию указателя this. До С++ 20 мы могли захватывать this вот так по значению и такую проблему будет очень сложно дебагать. Ситуация чуть улучшилась в С++20, мы теперь обязаны указывать this в списке захвата:

struct Task {
int id;

std::function<void()> GetNotifier() {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


Так уже чуть проще отловить проблему.

Как это лечить? Если у вас объект класса провисает, то тут поможет только профилактика и рефакторинг.

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

struct Task {
int id;
std::function<void()> GetNotifier() && = delete; // forbit call on temporaries

std::function<void()> GetNotifier() & {
return [this]{
std::cout << "notify " << id << std::endl;
};
}
};


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

Конечно это вряд ли поможет в многопоточке, но это уже что-то.

Refer to actual things. Stay cool.

#cppcore #cpp11 #cpp20
2🔥28👍1911🤯1
История capture this
#опытным

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

С++11

Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается &(*this) то есть указатель на текущий объект:

struct Foo {
int m_x = 0;

void func() {
int x = 0;

//Explicit capture 'this'
[this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[&]() { /*access m_x and x*/ }();

//Redundant 'this'
[&, this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[=]() { /*access m_x and x*/ }();

//Error
[=, this]() { }();
}
};


Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.

С++14

Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:

struct Foo {
int m_x = 0;

void func() {
[copy=*this]() mutable {
copy.m_x++;
}();
}
};


В остальном все осталось также.

С++17

Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:

struct Processor {
//Some state data..

std::future<void> process(/*args*/) {
//Pre-process...
//Do the data processing asynchronously
return
std::async(std::launch::async,
[=](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

auto caller() {
Processor p;
return p.process(/*args*/);
}


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

struct Processor {
std::future<void> process(/*args*/) {
return
std::async(std::launch::async,
[*this](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

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

С++20

С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.

Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.

Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:

struct Bagel {
int x = 0;
void func() {
//OK until C++20. Warning in C++20.
[=]() { std::cout << x; }();

//Error/warning until C++20. OK in C++20.
[=, this]() { std::cout << x; }();
}
};


Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.

Know the history. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥16👍1110🤯6
​​starts_with, ends_with
#новичкам

До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.

В нем появляются стандартные методы std::string/std::string_view .starts_with() и .ends_with():

constexpr bool starts_with(string_view sv) const noexcept;
constexpr bool starts_with(CharT c ) const noexcept;
constexpr bool starts_with(const CharT* s ) const;

constexpr bool ends_with(string_view sv )const noexcept;
constexpr bool ends_with(CharT c ) const noexcept;
constexpr bool ends_with(const CharT* s ) const;


Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.

const std::string url { "https://isocpp.org" };

// string literals
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";

// a single char:
if (url.starts_with('h') && url.ends_with('g'))
std::cout << "letters matched!\n";


Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.

Примерчик:

const std::vector<std::string> tokens { 
"<header>",
"<h1>",
"Hello World",
"</h1>",
"<p>",
"This is my super cool new web site.",
"</p>",
"<p>",
"Have a look and try!",
"</p>",
"</header>"
};

auto text = tokens |
std::views::filter([](const std::string& s) {
if (s.starts_with("<") || s.ends_with(">"))
return false;

return true;
});

for (const auto& str : text)
std::cout << str << std::endl;

// OUTPUT:
// Hello World
// This is my super cool new web site.
// Have a look and try!


Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.

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

Be expressive. Stay cool.

#cpp20 #STL
13🔥3112👍8❤‍🔥3🐳2
​​std::type_identity
#опытным

Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:

double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}


В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:

template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );


Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:

auto bounded = std::clamp(42, 3.14, 69.f); // ERROR!


Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.

Можно было сделать 3 отдельных параметра:

template<class T1, class T2, class T3>
constexpr const T& clamp( const T1& v, const T2& lo, const T3& hi );


Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.

Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:

template<class T>
struct type_identity { using type = T; };

template< class T >
using type_identity_t = type_identity<T>::type;


Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:

template <class T>
auto bound(T num, typename std::type_identity<T>::type low, typename std::type_identity<T>::type high) {
return std::clamp(num, low, high);
}

auto bounded = bound(25.5, 20, 25);


При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.

В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.

Также type_identity можно использовать для того, чтобы запретить вывод типов и заставить пользователя явно прописывать шаблонные параметры. Это может быть важно для точной передачи типа:

template<class T>
void foo(typename std::type_identity<T>::type arg) {}

foo<int>(42); // T жёстко задаётся как int
// foo(42); // Ошибка: вывод T невозможен!


Тоже самое для вариадиков:

template <typename... Ts>
void process(typename std::type_identity<std::tuple<Ts...>>::type data) {
}

process<int, double>(std::tuple{1, 2.0}); // OK
process(std::tuple{1, 2.0}); // ERROR, не указаны типы шаблонных параметров


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

Спасибо @d7d1cd за идею для поста)

Turn off deduction when it is not needed. Stay cool.

#template #cpp20
929👍19🔥10🤯2
Как итерироваться в обратном порядке?
#новичкам

Кто часто решал задачки на литкоде поймут проблему. Есть вектор и надо проитерироваться по нему с конца. Ну пишем:

std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = vec.size() - 1; i >= 0; --i) {
std::cout << i << ": " << vec[i] << '\n';
}


В чем проблема этого кода?

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

В большинстве задач можно написать тип int и все будет работать. Но все-таки size() возвращает size_t и будет происходить сужающее преобразование. В реальных проектах нужно избегать этого и сегодня мы посмотрим, как безопасно итерироваться в обратном порядке.

Использовать свободную функцию ssize() из C++20:

std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = std::ssize(vec) - 1; i >= 0; --i) {
std::cout << vec[i] << '\n';
}


Ее можно применить к вектору и она вернет значение типа std::ptrdiff_t. В первом приблежении это знаковый аналог std::size_t, который позволяет вычислять расстояние между двумя указателями, даже для очень больших массивов.

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

Использовать обратные итераторы:

std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto it = std::rbegin(vec); it != std::rend(vec); ++it)
std::cout << *it << '\n';


Тут все довольно очевидно и безопасно.

Однако cppcore гайдлайны говорят нам, что нужно предпочитать использовать range-based-for циклы обычным for'ам. Чтож, давайте пойдем в эту сторону.

Написать свой легковесный адаптер для итерирования в обратном порядке:

template <typename T>
class reverse {
private:
T &iterable_;

public:
explicit reverse(T &iterable) : iterable_{iterable} {}
auto begin() const { return std::rbegin(iterable_); }
auto end() const { return std::rend(iterable_); }
};

std::vector vec{1, 2, 3, 4, 5};
for (const auto &elem : reverse(vec))
std::cout << elem << '\n';


Делаем тонкую обертку над любым итерируемым объектом(в рабочем коде нужно всяких концептов навесить, чтобы было прям по-красоте) и элегантно итерируемся по контейнеру.

А ренджи для кого придумали? Они для этой задачи подходят идеально:

for (const auto& elem : vec | std::views::reverse)
std::cout << elem << '\n';

// или без пайпов

for (const auto& elem : std::ranges::reverse_view(vec))
std::cout << elem << '\n';


Рэнджи из C++20 предоставляют кучу удобных адаптеров для работы с контейнерами. В сущности std::views::reverse или std::ranges::reverse_view делает примерно то же самое, что и мы сами написали в третьем пункте.

Можно совсем упороться и применить алгоритмы ренждей:

std::ranges::copy(vec | std::views::reverse, 
std::ostream_iterator<int>( std::cout,"\n" ));

// или c лямбдой

std::ranges::for_each(vec | std::views::reverse,
[](const auto& elem) {
std::cout << elem << '\n';
});


Бывает, что индексы элементов все-таки нужны внутри цикла. Но это решается с помощью std::ranges::iota_view. Оставляем реализацию этого решения для домашних изысканий.

Have a large toolkit. Stay cool.

#cppcore #cpp20 #STL
37👍23🔥11👎3👀2❤‍🔥1
​​Оператор, бороздящий просторы вселенной
#новичкам

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

struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};


Однако иногда структуры требуется сравнивать и с помощью других операторов: >, ==, !=, >=, <=. В итоге полноценный набор операторов сравнения для Time выглядит так:

struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) const noexcept {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}

bool operator==(const Time& other) const noexcept {
return std::tie(hours, minutes, seconds) == std::tie(other.hours, other.minutes, other.seconds);
}

bool operator<=(const Time& other) const noexcept { return !(other < *this); }
bool operator>(const Time& other) const noexcept { return other < *this; }
bool operator>=(const Time& other) const noexcept { return !(*this < other); }
bool operator!=(const Time& other) const noexcept { return !(*this == other); }
};


Попахивает зловонным бойлерплейтом.

Недавно увидел мем, где девочка 8-ми лет, которая изучает питон, спрашивает отца: "папа, а если компьютер знает, что здесь пропущено двоеточие, почему он сам не может его поставить?". И батя такой: "Я не знаю, дочка, я не знаю ...".

Здесь вот похожая ситуация. Компилятор же умеет сравнивать набор чисел в лексикографическом порядке. Какого хрена он не может сделать это за нас?

Начиная с С++20 может!

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

struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) const = default;
bool operator==(const Time& other) const = default;
bool operator<=(const Time& other) const = default;
bool operator>(const Time& other) const = default;
bool operator>=(const Time& other) const = default;
bool operator!=(const Time& other) const = default;
};


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

Тут же нам на помощью приходит еще одна фича С++20 - трехсторонний оператор сравнения или spaceship operator. Теперь код выглядит так:

struct Time {
int hours;
int minutes;
int seconds;

// Один оператор вместо шести!
auto operator<=>(const Time& other) const = default;
};


Spaceship потому что похож на космический корабль, имхо прям имперский истребитель из далекой-далекой.

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

Conquer your space. Stay cool.

#cppcore #cpp20
42👍23🔥12
Spaceship оператор. Детали 1
#новичкам

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

Ну для начала: наличие определенного spaceship оператора гарантирует вам наличие всех 6 операций сравнения:

struct Time {
int hours;
int minutes;
int seconds;

// Spaceship operator (генерирует все 6 операторов сравнения)
auto operator<=>(const Time& other) const = default;
};

Time t1{10, 30, 15}; // 10:30:15
Time t2{9, 45, 30}; // 09:45:30
Time t3{10, 30, 15}; // 10:30:15

assert(t1 > t2); // 10:30:15 > 09:45:30
assert(!(t1 < t2)); // 10:30:15 не < 09:45:30
assert(t1 == t3); // 10:30:15 == 10:30:15
assert(t1 != t2); // 10:30:15 != 09:45:30
assert(t1 <= t3); // 10:30:15 <= 10:30:15
assert(t1 >= t2); // 10:30:15 >= 09:45:30


Это уже прекрасно, но это еще не все!

Обратите внимание на сигнатуру spaceship operator. Зачем там нужен auto?

Вот теперь объясненяем, почему это называется оператор трехстороннего сравнения.

Он возвращает объект, который содержит информацию о результате сравнения:

Time t1{10, 30, 15};  // 10:30:15
Time t2{9, 45, 30}; // 09:45:30

// Можно использовать и сам spaceship operator напрямую
auto cmp = t1 <=> t2;
if (cmp > 0) {
std::cout << "t1 is later than t2\n";
} else if (cmp < 0) {
std::cout << "t1 is earlier than t2\n";
} else {
std::cout << "t1 is the same as t2\n";
}
// OUTPUT:
// t1 is later than t2


Если результат сравнения >0, то первый операнд больше второго. И так далее по аналогии.

Тип возвращаемого значения у оператора один из этих трех:
- std::strong_ordering
- std::weak_ordering
- std::partial_ordering

Что они значат - тема отдельного разговора, но каждый из них может находится в одном из 3-х состояний: less, greater, equal. Это можно использовать, например, для проверки возвращаемых значений системных вызовов:

constexpr int strong_ordering_to_int(const std::strong_ordering& o)
{
if (o == std::strong_ordering::less) return -1;
if (o == std::strong_ordering::greater) return 1;
return 0;
}

char buffer[256];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));

// Сравниваем результат read() с нулём через <=>
switch (strong_ordering_to_int(bytes_read <=> 0)) {
case 1:
std::cout << "Read " << bytes_read << " bytes: "
<< std::string(buffer, bytes_read) << "\n";
break;
case 0:
std::cout << "End of file reached (0 bytes read)\n";
break;
case -1:
perror("read failed");
return 1;
}


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

Be universal. Stay cool.

#cppcore #cpp20
32👍15🔥101
​​Какой день будет через месяц?
#новичкам

Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. chrono появилась так-то в С++11. Но и даже с ее появлением жить стало лишь немногим легче.

Например, простая задача: "Прибавить к текущей дате 1 месяц".

В С++11 у нас есть только часы и точки на временной линии. Тут просто дату-то получить сложно. Есть конечно сишная std::localtime, можно мапулировать отдельными полями std::tm(днями, минутами и тд), но придется конвертировать сишные структуры времени в плюсовые, да и можно нарваться на трудноотловимые ошибки, если попытаться увеличить на 1 месяц 30 января.

Как прибавить к дате месяц? +30 дней или +1 месяц не канает. А если февраль? А если високосный год?

В общем стандартного решения нет... Или есть?

В С++20 в библиотеку chrono завезли кучу полезностей. А в частности функционал календаря. Теперь мы можем манипулировать отдельно датами и безопасно их изменять.

Например, чтобы получить сегодняшнюю дату и красиво ее вывести на консоль достаточно сделать следующее:

std::chrono::year_month_day current_date =
std::chrono::floor<std::chrono::days>(
std::chrono::system_clock::now());
std::cout << "Today is: " << current_date << '\n';
// Today is: 2025-09-10


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

Если вам нужно задать определенный формат отображения - не проблема! Есть std::format:

std::cout << "Custom: "
<< std::format("{:%d.%m.%Y}", current_date)
<< '\n';
// Custom: 10.09.2025


С помощью std::chrono::year_month_day можно удобно манипулировать датами и, главное, делать это безопасно. Что будет если я к 29 января прибавлю месяц?

auto date = std::chrono::year_month_day{
std::chrono::year(2004), std::chrono::month(1), std::chrono::day(29)};
std::cout << "Date: " << date << "\n";

std::chrono::year_month_day next_month = date + std::chrono::months{1};
std::chrono::year_month_day next_year_plus_month =
date + std::chrono::years{1} + std::chrono::months{1};

std::cout << "Next month: " << next_month << "\n";
std::cout << "Next year plus month: " << next_year_plus_month << "\n";

// OUTPUT:
// Date: 2004-01-29
// Next month: 2004-02-29
// Next year plus month: 2005-02-29 is not a valid date


А будет все хорошо, если год високосный. Но если нет, то библиотека нам явно об этом скажет.

В общем, крутой фиче-сет, сильно облегчает работу со стандартными временными точками.

Take your time. Stay cool.

#cpp11 #cpp20
24👍17🔥6😁3
​​Сколько времени сейчас в Москве?
#новичкам

Как в стандартных плюсах работать с временными зонами? Да никак до С++20. Приходилось использовать разные сторонние решения.

Но стандарт развивается и у нас теперь есть возможность работать с зонами в чистом С++!

Появился класс std::chrono::zoned_time, который представляет собой пару из временной метки и временной зоны. Создать зонированное время можно так:

auto now = std::chrono::zoned_time{std::chrono::current_zone(), std::chrono::system_clock::now()};


Функция std::chrono::current_zone() позволяет получить локальную временную зону.

Можно также передать имя зоны:

auto msw_time = std::chrono::zoned_time{"Europe/Moscow", std::chrono::system_clock::now()};


И это все прекрасно работает с std::format, который позволяет информацию о временной точки настолько подробно, насколько это возможно:

std::string get_time_string(const std::chrono::zoned_time<std::chrono::system_clock::duration>& zt) {
return std::format("{:%Y-%m-%d %H:%M:%S %Z}", zt);
}

std::string get_detailed_time_string(const std::chrono::zoned_time<std::chrono::system_clock::duration>& zt) {
return std::format("{:%A, %d %B %Y, %H:%M:%S %Z (UTC%z)}", zt);
}

std::cout << "Current time: " << get_time_string(now) << std::endl;
std::cout << "Detailed: " << get_detailed_time_string(now) << std::endl;

std::cout << "Time in Moscow: " << get_time_string(msw_time) << std::endl;
std::cout << "Detailed: " << get_detailed_time_string(msw_time) << std::endl;

// OUTPUT:
// Current time: 2025-09-11 17:50:48.035852842 UTC
// Detailed: Thursday, 11 September 2025, 17:50:48.035852842 UTC (UTC+0000)
// Time in Moscow: 2025-09-11 20:50:48.041000112 MSK
// Detailed: Thursday, 11 September 2025, 20:50:48.041000112 MSK (UTC+0300)


Работы с временными зонами очень не хватало в стандарте и круто, что ее добавили.

Develop yourself. Stay cool.

#cpp20
2🔥38👍128😁7
​​Уплощаем многомерный массив
#опытным

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

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
22👍13🔥7
Инициализация внутри 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
​​Предотвращаем висячие ссылки
#опытным

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

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
🔥2114👍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👍129🤯3👎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
👍28🔥149😁3🤯1
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👍16🔥9🥱21🤯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
🔥44👍188🤯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
👍159🔥7😁1🤯1