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

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

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

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

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
41👍22🔥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
31👍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
23👍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🔥37👍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
21👍13🔥7