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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Они про скорость и многозадачность: асинхронные сетевые фреймворки для C++ 🚀

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

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

#BellintegratorTeam #советыBell
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍259🔥9👎1
​​Inline виртуальные методы
#опытным

В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?

Ну то есть такая иерархия:

class Base {
public:
virtual int foo() = 0;

protected:
int x = 2;
};

class Derived : public Base {
public:
int foo() override {
x *= 2;
return x;
}
};


Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?

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

void simple() {
Derived d;
std::cout << d.foo();
}


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

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

void vec_base(std::vector<std::unique_ptr<Base>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Base>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);


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

Теперь посмотрим такой код:

void vec_derived(std::vector<std::unique_ptr<Derived>>& my_vec) {
for (auto &p : my_vec) {
std::cout << p->foo();
}
}
// другая TU
std::vector<std::unique_ptr<Derived>> my_vec;
my_vec.push_back(new Derived());
vec_base(my_vec);


И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.

Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.

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

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

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

Учитывая, что

1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)

2️⃣ операции над непосредственно типами наследниками не распространены

3️⃣ а для девиртуализации нужно танцевать с бубнами

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

Be useful. Stay cool.

#cppcore #OOP #design
113👍6🔥6❤‍🔥2
​​Zero-Cost Abstractions
#новичкам

Нельзя в промышленных масштабах писать код без абстракций. Вряд ли вы за обозримое время напишите даже эхо-сервер на ассемблере. Даже сам язык программирования - это абстракция. Он позволяет писать программы более-менее на английском языке(или на патриотическом Русском на 1С).

Абстракции упрощают программирование. А что с перфомансом?

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

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

Однако есть и такие абстракции, которые не накладывают оверхэд на рантайм! Они называются Zero-Cost абстракциями.

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

За примерами долго ходить не надо. Можно сложить элементы вектора руками, а можно использовать std::accumulate:

void foo(std::vector<int>& vec) {
int sum = 0;
for(int i = 0; i < vec.size(); i++) {
sum += vec[i];
}
std::cout << sum;
}

void bar(std::vector<int>& vec) {
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << sum;
}


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

На каких китах стоит возможность использовать "бесплатные" абстракции в С++?

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

🐳 Инлайнинг. Одна из основных оптимизаций компилятора. Позволяет встраивать код функции в вызывающий код. С помощью инлайнинга 4 вложенных вызова функций могут превратиться в одну сплошную портянку низкоуровневого кода без дорогостоящих инструкций call.

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

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

Don't pay for abstraction. Stay cool
#cppcore #compiler
👍3315🔥6😁3
Zero-Cost Abstraction. Или нет?
#опытным

Концепция бесплатных абстракций-то есть. Но концепции они обычно в банках-консервах хранятся. В реальности все немного сложнее и бесплатный только труд стажеров в современном IT.

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

Для чего используется уникальный указатель? Чтобы избавиться от сырых указателей, снять головную боль по освобождению памяти и четко показать в коде передачу владения. То есть такой код:

void bar(int *ptr);

// Takes ownership
void baz(int *ptr);

void foo(int *ptr) {
if (*ptr > 42) {
bar(ptr);
*ptr = 42;
}
baz(ptr);
}


хорошо бы переписать с использованием std::unique_ptr:

void bar(int *ptr);

// Takes ownership.
void baz(std::unique_ptr<int> ptr);

void foo(std::unique_ptr<int> ptr) {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}


Будет ли здесь оверхэд рантайма? Ох-ох-ох, еще как!

Вот асм для первого сниппета:

foo(int):
cmpl $43, (%rdi)
jl baz(int)@PLT
pushq %rbx
movq %rdi, %rbx
callq bar(int)@PLT
movq %rbx, %rdi
movl $42, (%rbx)
popq %rbx
jmp baz(int)@PLT


А вот для второго:

foo(std::unique_ptr<int, std::default_delete<int>>):
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq (%rdi), %rdi
cmpl $43, (%rdi)
jl .LBB0_2
callq bar(int)@PLT
movq (%rbx), %rdi
movl $42, (%rdi)
.LBB0_2:
movq %rdi, 8(%rsp)
movq $0, (%rbx)
leaq 8(%rsp), %rdi
callq baz(std::unique_ptr<int, std::default_delete<int>>)@PLT
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_5
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_5:
addq $16, %rsp
popq %rbx
retq
movq %rax, %rbx
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_8
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_8:
movq %rbx, %rdi
callq _Unwind_Resume@PLT


Он в 4 раза больше! Если внимательно присмотреться, то можно увидеть, что значительная часть кода здесь тратится на подчищение памяти и обработку исключений. Да, исключения - это вещь, где RAII очень сильно помогает. Вот вам ссылочки на годболт: тык и тык


Но предположим, что изначальный код был корректным и там не вылетало никаких исключений. Пометим функции как noexcept:

void bar(int *ptr) noexcept;

void baz(std::unique_ptr<int> ptr) noexcept;

void foo(std::unique_ptr<int> ptr) {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}


Вот асм:

foo(std::unique_ptr<int, std::default_delete<int>>):
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq (%rdi), %rdi
cmpl $43, (%rdi)
jl .LBB0_2
callq bar(int)@PLT
movq (%rbx), %rdi
movl $42, (%rdi)
.LBB0_2:
movq %rdi, 8(%rsp)
movq $0, (%rbx)
leaq 8(%rsp), %rdi
callq baz(std::unique_ptr<int, std::default_delete<int>>)@PLT
movq 8(%rsp), %rdi
testq %rdi, %rdi
je .LBB0_4
movl $4, %esi
callq operator delete(void, unsigned long)@PLT
.LBB0_4:
addq $16, %rsp
popq %rbx
retq


Стало чуть меньше, но это явно не zero-cost.

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

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

Be aware about costs. Stay cool.
👍2110🔥9🤯3😁2
std::allocate_shared
#опытным

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

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

Но что, если нам очень нужно по-особенному работать с памятью для объекта? Даже хотя бы просто отслеживать выделение и разрушение без влияния на глобальный delete?

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

Вот вам простой примерчик с простым STL-совместимым аллокатором, логирующим операции выделения и освобождения памяти:

template <typename T>
class LoggingAllocator {
public:
using value_type = T;
using pointer = T *;
using const_pointer = const T *;
using reference = T &;
using const_reference = const T &;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;

template <typename U>
struct rebind {
using other = LoggingAllocator<U>;
};

LoggingAllocator() noexcept = default;

template <typename U>
LoggingAllocator(const LoggingAllocator<U> &) noexcept {}

pointer allocate(size_type n) {
size_type bytes = n * sizeof(T);
std::cout << "Allocating " << n << " elements ("
<< bytes << " bytes)\n";
return static_cast<pointer>(::operator new(bytes));
}

void deallocate(pointer p, size_type n) noexcept {
size_type bytes = n * sizeof(T);
std::cout << "Deallocating " << n << " elements ("
<< bytes << " bytes)\n";
::operator delete(p);
}
};

template <typename T, typename U>
bool operator==(const LoggingAllocator<T> &,
const LoggingAllocator<U> &) noexcept { return true; }

template <typename T, typename U>
bool operator!=(const LoggingAllocator<T> &,
const LoggingAllocator<U> &) noexcept { return false; }

class MyClass {
public:
MyClass(int value) : value(value) {
std::cout << "Constructed with " << value << "\n";
}
~MyClass() {
std::cout << "Destroyed with " << value << "\n";
}
void print() const {
std::cout << "Value: " << value << "\n";
}

private:
int value;
};

int main() {
LoggingAllocator<MyClass> alloc;
auto ptr = std::allocate_shared<MyClass>(alloc, 42);
ptr->print();
return 0;
}
// OUTPUT:
// Allocating 1 elements (24 bytes)
// Constructed with 42
// Value: 42
// Destroyed with 42
// Deallocating 1 elements (24 bytes)


Стандартом на аллокаторы накладываются определенные требования: нужно определить нужные алиасы, методы allocate и deallocate, структуру rebind и соответствующий конструктор копирования и операторы сравнения. Полный список требований можно прочитать тут.

По консольному выводу видно, что аллокатор выделяет немного больше памяти, чем должен занимать объект. Это сделано в том числе для того, чтобы хранить в выделенном куске еще и контрольный блок std::shared_ptr. Так что касаемо особенностей аллокации и деаллокации тут похожая ситуация с std::make_shared.

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

Customize your solutions. Stay cool.

#cppcore #STL #memory
21👍13🔥9🤣1
Как вам такие фокусы?)
1😁677🤯6👍3
​​Оператор, бороздящий просторы вселенной
#новичкам

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

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
40👍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
30👍15🔥101
Spaceship оператор. Детали 2
#опытным

Продолжаем углубляться в космический оператор.

В прошлом посте мы рассказали о том, что определение одного единственного spaceship оператора возможность использовать все 6 операторов сравнения.

Это правда, но с оговорочками. На самом деле компилятор подменяет привычные операторы на использование spaceship оператора вот так:

SomeType a;
SomeType b;
a == b; // компилятор распознает как (a <=> b) == 0
a != b; // компилятор распознает как (a <=> b) != 0
a < b; // компилятор распознает как (a <=> b) < 0
// и тд, суть вы поняли


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

Но не всегда трехсторонний оператор предоставляет все 6 операторов.

Вгляните на пример:

struct String {
std::string str;
auto operator<=>(const String &other) const {
std::cout << "Using <=>\n";
return str <=> other.str;
}
};

String a{"foobar"};
String b{"foo"};
a == b; // Ошибка компиляции
a != b; // Ошибка компиляции
a < b; // OK
a > b; // OK
a <= b; // OK
a >= b; // OK


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

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

Почему так?

Дело в производительности.

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

template<typename T>
strong_ordering operator<=>(vector<T> const& lhs,
vector<T> const& rhs)
{
size_t min_size = min(lhs.size(), rhs.size());
for (size_t i = 0; i != min_size; ++i) {
if (auto const cmp = lhs[i] <=> rhs[i]; cmp != 0) {
return cmp;
}
}
return lhs.size() <=> rhs.size();
}


Этот код работает и проходит все тесты. Однако для определения равенства этот код очень плохо перфомит. Вектора могут иметь очень длинный общий префикс, но по итогу иметь разные размеры. Логично сразу же проверить размеры векторов и, если они не равны, сразу принять решение. В рамках данного spaceship оператора это сделать невозможно(только по размерам нельзя определить отношение порядка), но стандарт нам не мешает явно определять нужные операторы:

template<typename T>
bool operator==(vector<T> const& lhs, vector<T> const& rhs)
{
// short-circuit on size early
const size_t size = lhs.size();
if (size != rhs.size()) {
return false;
}

for (size_t i = 0; i != size; ++i) {
// use ==, not <=>, in all nested comparisons
if (!(lhs[i] == rhs[i])) {
return false;
}
}

return true;
}


И это будет работать для ==. Но не будет работать для !=. Вы же помните, во что он раскрывается? В (a <=> b) != 0. Поэтому надо явно предоставлять и этот оператор. Но компилятор за нас здесь поработает и сам сгенерирует operator!=, как отрицание ==.

Получается, что если мы сами определили оператор <=>, то нас не удовлетворило базовое лексикографическое сравнение. Поэтому скорее всего нас не удовлетворит и использование такого spaceship'а для проверки на равенство. Поэтому нужно писать его отдельно. И это решает проблему с производительностью.

Хорошие новости в том, что при оборачивании такого класса в другой класс, сравнения обертки будут использовать оптимизированное сравнение, а не spaceship реализацию:
👍108🔥3
struct String {
std::string str;
bool operator==(const String &other) const {
std::cout << "Using optimized ==\n";
if (str.size() != other.str.size())
return false;
return str == other.str;
}
auto operator<=>(const String &other) const {
std::cout << "Using <=>\n";
return str <=> other.str; // Сравнивает символы до первого различия
}

};

struct SString {
String str;
auto operator<=>(const SString &other) const = default;
};

a == b; // Вызовет operator==
a != b; // Вызовет operator!=, который определен через ==
a < b; // Вызовет spaceship
// OUTPUT:
// Using optimized ==
// Using optimized ==
// Using <=>


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

Don't go to a rabbit hole. Stay cool.

#cppcore
22👍7🔥5
std::less vs std::ranges::less
#опытным

В догонку к предыдущим постам. Вот у вас есть структурка TIme из С++17:

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

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


Вы как-то заполняете ее и сортируете:

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::sort(times.begin(), times.end());


И все работает прекрасно.

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

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times);


Запускаете сборку, а она падает с какими-то нечитаемыми ошибки, типа таких:

/usr/include/c++/15/concepts:362:13:   required for the satisfaction of 'invocable<_Fn, _Args ...>' [with _Fn = std::ranges::less&; _Args = {Time&, Time&}]
/usr/include/c++/15/concepts:366:13: required for the satisfaction of 'regular_invocable<_Fn, _Args ...>' [with _Fn = std::ranges::less&; _Args = {Time&, Time&}]
/usr/include/c++/15/concepts:370:13: required for the satisfaction of 'predicate<_Rel, _Tp, _Tp>' [with _Rel = std::ranges::less&; _Tp = Time&]
/usr/include/c++/15/concepts:375:13: required for the satisfaction of 'relation<_Rel, _Tp, _Up>' [with _Rel = std::ranges::less&; _Tp = Time&; _Up = Time&]
/usr/include/c++/15/concepts:385:13: required for the satisfaction of 'strict_weak_order<_Fn&, typename std::__detail::__indirect_value<_Iter>::type, typename std::__detail::__indirect_value<_I2>::type>' [with _Fn = std::ranges::less; _I1 = __gnu_cxx::__normal_iterator<Time*, std::vector<Time, std::allocator<Time> > >; _I2 = __gnu_cxx::__normal_iterator<Time*, std::vector<Time, std::allocator<Time> > >]


Нам говорили, что с концептами ошибки станут понятнее, но это только в теории. А на практике вот это.

В чем в итоге проблема?


По умолчанию в сортировке рэнджей используется компаратор std::ranges::less, а в обычной сортировке - std::less. И в их разнице и зарыта собака: std::ranges::less требует определения всех шести операторов сравнения или одного spaceship'а и все заработает:

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

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

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times);


Ну или можете пометить оператор< дефотным и передавать в сортировку std::less:
struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) const = default; // Here
};

std::vector<Time> times = {
{14, 30, 15}, {9, 45, 0}, {14, 30, 0}, {23, 59, 59}, {0, 0, 0}};

std::ranges::sort(times, std::less{}); // And here


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

Give clear feedback. Stay cool.
44👍25🔥18🤯1
Unity build
#опытным

Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.

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

Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали std::vector<int> - компилируем эту инстанциацию. В другом написали std::vector<int> - заново скомпилировали эту инстанциацию. То есть большая проблема в компиляции одного и того же кучу раз.

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

Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)

Условно. Есть у вас 2 цппшника и один хэдэр:

// header.hpp
#pragma once
void foo();

// source1.cpp
#include "header.hpp"
void foo() {
std::cout << "You are the best!" << std::endl;
}

// source2.cpp
#include "header.hpp"
int main() {
foo();
}


Вы все цппшники подключаете в один файл unity_build.cpp:

#include "source1.cpp"
#include "source2.cpp"


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

Или нет?

У этой техники есть ряд недостатков:

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

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

Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.

У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.

Solve the problem. Stay cool.

#cppcore #compiler #tools
122🔥7👍6😁2🗿1
​​Короткий совет по отладке кода от мэтра
#новичкам

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

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

"I get maybe two dozen requests for help with some sort of programming or design problem every day. Most have more sense than to send me hundreds of lines of code. If they do, I ask them to find the smallest example that exhibits the problem and send me that. Mostly, they then find the error themselves. 'Finding the smallest program that demonstrates the error' is a powerful debugging tool."

Каждый день я получаю около пары дюжин запросов с помощью решить какую-то программную или архитектурную проблему. Большинство из них достаточно благоразумны, чтобы не присылать мне сотни строк кода. Если они всё же присылают, я прошу их найти самый маленький пример, демонстрирующий проблему, и прислать его мне. В большинстве случаев они сами находят ошибку. «Найти самую маленькую программу, демонстрирующую ошибку» — мощный инструмент отладки.

Почему это работает?

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

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

Если вы так и не поняли, в чем проблема, с маленьким примером вам намного охотнее и эффективнее смогут помочь коллеги.

Как локализовать проблему?

🔍 Мокайте зависимости от других компонентов и модулей.

🔍 Фиксируйте значения переменных.

🔍 Упрощайте входные данные.


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

А какие вы дадите полезные советы по отладке кода?

Fix problems playfully. Stay cool.

#goodpractice
1🔥197👍6❤‍🔥3🤣3
​​Какой день будет через месяц?
#новичкам

Работа со временем в стандартных плюсах - боль. Долгое время ее вообще не было. 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
22👍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🔥36👍128😁7
Внутрянка бэкэнда

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

И 4 октября в Москве эксперты нашего бигтеха будут раскрывать про внутрянку своих продуктов на конференции «Я про бэкенд»! В докладах будет,много про бэкенд челледжи, связанных с МЛ, рекомендательными и генеративными технологиями.

Держите список докладов:

-Антон Полднев (Яндекс Реклама): как рекомендательный движок ежегодно экономит 200 тыс. CPU в инфраструктуре Рекламы

-Дмитрий Погорелов (VK): эволюция рекомендательного движка и перезапуск рекомендаций ВКонтакте

-Михаил Чебаков (T-Банк): как прятать сложность LLM-инференса за понятными числами

-Андрей Шукшов (Яндекс R&D): внутри LLM: как выжать максимум из decoder attention на GPU

-Алёна Васильева (Шедеврум): про архитектуру для ML-моделей и длинный инференс

-Никита Сикалов (Яндекс Поиск): про эволюцию технологий реалтайм-индексации

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

Регистрируйтесь и прокачивайте свои бэкэндерские мускулы
5👍5🔥3🤨1🗿1
​​Распределенные компиляторы
#опытным

Как только ваш проект достигает определенного размера, время компиляции начинает становиться проблемой. В моей скромной практике были проекты, которые полностью собирались с 1-2 часа. Но это далеко не предел. Пишите кстати в комментах ваши рекордные тайминги сборки проектов.

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

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

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

Так как обычно девелоперские задачи в среднем потребляют мало ресурсов(не так много нужно, чтобы писать буквы в редакторе), этими удаленными CPU могут быть даже машины ваших коллег!

Наиболее известный представитель систем распределенной компиляции с открытым исходным кодом — это distcc. Он состоит из демона-сервера, принимающего задания на сборку по сети, и обёртки (wrapper) для компилятора, которая распределяет задания по доступным узлам сборки в сети.

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

┌─────────────────┐    ┌─────────────────────────────────┐
│ Клиентская │ │ Ферма компиляции │
│ машина │ │ │
│ │ │ ┌───────┐ ┌───────┐ ┌───────┐│
│ ┌─────────────┐│ │ │Worker │ │Worker │ │Worker ││
│ │ Координатор │◄──────►│ 1 │ │ 2 │ │ N ││
│ └─────────────┘│ │ └───────┘ └───────┘ └───────┘│
│ │ │ │
│ ┌─────────────┐│ │ ┌─────────────────────────────┐│
│ │ Кэш .o ││ │ │ Distributed Cache ││
│ │ файлов │◄──────►│ ││
│ └─────────────┘│ │ └─────────────────────────────┘│
└─────────────────┘ └─────────────────────────────────┘

- Координатор анализирует зависимости между файлами и распределяет задачи компиляции

- Компиляционные ноды выполняют фактическую компиляцию, их набор конфигурируется на клиенте

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

Этапы работы примерно такие:

1️⃣ Все цппшники проекта проходят этап препроцессинга на локальной машине и уже в виде единиц трансляции перенаправляются на ноды компиляции.

2️⃣ Ноды компиляции преобразуют единицы трансляции в объектные файлы и пересылают их на клиентскую машину.

3️⃣ Последним этапом идет бутылочное горлышко всей системы - линковка. Для нее необходим доступ ко многим объектым файлам одновременно и эта задача слабо параллелится, поэтому и выполняется на клиентской машине.

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

Speed up processes. Stay cool.

#compiler #tools
1👍249🔥8
​​ccache
#опытным

Еще один полезный и простой во внедрении инструмент для ускорения компиляции - ccache.

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

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

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

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

👉🏿 идентификатор компилятора
👉🏿 использованные флаги компилятора
👉🏿 содержимое входного исходного файла,
👉🏿 содержимое подключаемых заголовочных файлов (и их транзитивное замыкание).

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

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

Если распределенный компилятор distcc каждый раз выполняет препроцессинг, то для ccache это не обязательно. В одном из режимов работы ccache вычисляет хэши MD4 для каждого включаемого заголовочного файла отдельно и сохраняет результаты в так называемом манифесте. Поиск в кеше выполняется путём сравнения хэшей исходного файла и всех его включений с содержимым манифеста; если все хэши попарно совпадают, мы имеем попадание. В текущих версиях ccache прямой режим включён по умолчанию.

Для того, чтобы начать пользоваться ccache, достаточно его установить, добавить в PATH и в cmake'е прописать CMAKE_CXX_COMPILER_LAUNCHER=ccache. Это можно сделать и через команду запуска, и через установку переменной окружения. Вот вам ссыль.

Но это было введение в ccache для незнающих. Опытный же подписчик спросит: а зачем нужен этот кэш, если cmake и собирает только то, что мы недавно изменили? Об этом ключевом вопросе мы и поговорим в следующем посте.

Compile fast. Stay cool.

#compiler #tools
1👍2112🔥9🤯1
​​ccache vs cmake
#опытным

И давайте раскроем очевидный вопрос: чем кэширование ccache отличается от кэширования самого cmake'а? Ведь при искрементальной сборке cmake пересобирает только те файлы, которые поменялись.

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

Проще понять разницу на примерах:

🔍 Вы скомпилировали проект и случайно или специально(бывает нужно, если cmake троит) удалили папку с билдом. Без ccache нужно перекомпилировать все, а с ним - ничего, только линковку сделать.

🔍 Вы плотно работаете с несколькими ощутимо отличающимися бранчами, собираете, коммитите и переключаетесь. Без ccache нужно будет перекомпилировать все измененные при переключении бранчей файлы. С ccache - только те, которые вы сами изменили после последней сборки.

🔍 Если вы правите только комменты в файле, то голый cmake пойдет перекомпилировать его. ccache - нет, потому что работает с препроцесснутым файлом.

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

Not much, но каждый из нас частенько сталкивается с одним из этих пунктов. Поэтому ставьте ccache. Это сделать просто, но импакт дает ощутимый в определенных кейсах.

Compile fast. Stay cool.

#compiler #tools
133👍14🔥8