Бага обнаружена!
#опытным
Проблема в текущей реализации
заключается в том, что константность не правильно распространяется через итератор при работе с const-объектами Vector. Давайте по порядку
Когда мы имеем
В текущей реализации, когда вызывается
Шаблонный вывод типов в форме CTAD отбрасывает верхнеуровневую константность: для
В результате мы получаем итератор, который позволяет изменять элементы, даже когда Vector константный.
То есть, такой код становится вполне валиден:
Что не есть хорошо.
Выходом тут будет не использовать CTAD, а явно и гибко задавать тип шаблонного параметра на основе константности self и нескольких трейтов:
По пути еще чуть-чуть причесываем передачу универсальной ссылки через std::forward.
Fix the problem. Stay cool.
#template #cppcore
#опытным
Проблема в текущей реализации
template <typename T>
class Vector {
private:
T *m_data;
T *m_endSize;
T *m_endCapacity;
public:
// Use a different type "U to support const and non-const
template <typename U>
class Iterator {
private:
U *m_ptr;
public:
Iterator(U *ptr) : m_ptr{ptr} {}
U &operator*() const { return m_ptr; }
};
template <typename Self>
auto begin(this Self &&self) {
return Iterator(self.m_data);
}
};
заключается в том, что константность не правильно распространяется через итератор при работе с const-объектами Vector. Давайте по порядку
Когда мы имеем
const Vector<T>, поле m_data становится T* const (константный указатель на T), а не const T* (указатель на константный T). Так называемая синтаксическая или поверхностная константность.В текущей реализации, когда вызывается
begin() на const-объекте:Self выводится как const Vector<T>& self.m_data имеет тип T const Шаблонный вывод типов в форме CTAD отбрасывает верхнеуровневую константность: для
Iterator создает Iterator<T>, а не Iterator<const T> В результате мы получаем итератор, который позволяет изменять элементы, даже когда Vector константный.
То есть, такой код становится вполне валиден:
const Vector<int> vec{1, 2, 3};
auto it = vec.begin();
*it = 2;Что не есть хорошо.
Выходом тут будет не использовать CTAD, а явно и гибко задавать тип шаблонного параметра на основе константности self и нескольких трейтов:
template<typename Self>
auto begin(this Self&& self)
{
using value_type = std::conditional_t<
std::is_const_v<std::remove_reference_t<Self>>,
const T,
T>;
return Iterator<value_type>(std::forward<Self>(self).m_data);
}
По пути еще чуть-чуть причесываем передачу универсальной ссылки через std::forward.
Fix the problem. Stay cool.
#template #cppcore
1👍20❤12🔥5❤🔥3
Как использовать std::unordered_map с ключом в виде std::pair?
#опытным
При работе над задачами C++ часто необходимо использовать сложные ключи в контейнерах на основе хэша - std::unordered_map. Распространенным подходом является использование std::pair<int, int> в качестве типа ключа. Однако попытка объявить unordered_map следующим образом:
приводит к подобной ошибке компиляции:
Происходит это, потому что для std::pair не определена хэш-функция. Она нужна для превращения значение объекта-ключа в число, которое используется для индексации элемента в хэш-таблице.
STL предоставляет нам хэш-функции для тривиальных типов данных и, например, std::string.
Но для сложных шаблонных типов непонятно в общем случае, как реализовать хэш-функцию. Поэтому эту задачу и возложили на самих программистов. Нужно самим определять хэш-функцию для объекта так, как того требует конкретная задача.
Ну хорошо. Определять надо. Но как это сделать? В азбуке не написано, как написать хэш для пары...
Давайте по порядку. Самый тривиальный подход - просто ксорим два хэша типов пары(в предположении, что они уже есть):
Отлично, заработало! Или нет?
Это компилируется, но есть проблема с коллизиями. Если ключом будет std::pair<int, int>, то для двух разных ключей {1, 2} и {2, 1} будут одинаковые хэши. Не очень хорошо.
Сделаем ход конем:
Побитово сдвинем второй хэш на один бит влево. Так мы не сильно ухудшим распределение(всего один бит заменим на нолик), но уберем коллизии.
Но это конечно все на коленке сделаный велосипед и можно найти антипримеры. В бусте есть функция hash_combine, которая делает ровно то, что мы хотим:
Если хочется узнать, что там у этой штуки под капотом, что в сущности код выше будет эквивалентен следующему коду:
Магические числа во всей красе. Но это нормально, когда мы имеем дело с математикой: генераторы случайных чисел, шифрование, хэш-функции.
Кстати, естественно, что такой подход можно использовать и для кастомных структур, и для туплов. В общем, можно пользоваться. Хотите тяните буст, хотите сами пишите, там все равно не так сложно.
Use ready-made solutions. Stay cool.
#cppcore #STL #template
#опытным
При работе над задачами C++ часто необходимо использовать сложные ключи в контейнерах на основе хэша - std::unordered_map. Распространенным подходом является использование std::pair<int, int> в качестве типа ключа. Однако попытка объявить unordered_map следующим образом:
std::unordered_map<std::pair<int, int>, int> map;
приводит к подобной ошибке компиляции:
error: call to implicitly-deleted default constructor of
'unordered_map<std::pair<int, int>, int>'
Происходит это, потому что для std::pair не определена хэш-функция. Она нужна для превращения значение объекта-ключа в число, которое используется для индексации элемента в хэш-таблице.
STL предоставляет нам хэш-функции для тривиальных типов данных и, например, std::string.
Но для сложных шаблонных типов непонятно в общем случае, как реализовать хэш-функцию. Поэтому эту задачу и возложили на самих программистов. Нужно самим определять хэш-функцию для объекта так, как того требует конкретная задача.
Ну хорошо. Определять надо. Но как это сделать? В азбуке не написано, как написать хэш для пары...
Давайте по порядку. Самый тривиальный подход - просто ксорим два хэша типов пары(в предположении, что они уже есть):
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ h2;
}
};
}
std::unordered_map<std::pair<int, std::string>, double> map;
map[{42, "foo"}] = 3.14;Отлично, заработало! Или нет?
Это компилируется, но есть проблема с коллизиями. Если ключом будет std::pair<int, int>, то для двух разных ключей {1, 2} и {2, 1} будут одинаковые хэши. Не очень хорошо.
Сделаем ход конем:
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ (h2 << 1);
}
};
}
std::unordered_map<std::pair<int, int, double> map;Побитово сдвинем второй хэш на один бит влево. Так мы не сильно ухудшим распределение(всего один бит заменим на нолик), но уберем коллизии.
Но это конечно все на коленке сделаный велосипед и можно найти антипримеры. В бусте есть функция hash_combine, которая делает ровно то, что мы хотим:
namespace std {
template <typename T1, typename T2>
struct hash<std::pair<T1, T2>> {
size_t operator()(const std::pair<T1, T2>& p) const {
size_t seed = 0;
boost::hash_combine(seed, p.first);
boost::hash_combine(seed, p.second);
return seed;
}
};
}Если хочется узнать, что там у этой штуки под капотом, что в сущности код выше будет эквивалентен следующему коду:
namespace std {
template <typename T1, typename T2>
struct hash<pair<T1, T2>> {
size_t operator()(const pair<T1, T2>& p) const {
size_t h1 = hash<T1>{}(p.first);
size_t h2 = hash<T2>{}(p.second);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
}Магические числа во всей красе. Но это нормально, когда мы имеем дело с математикой: генераторы случайных чисел, шифрование, хэш-функции.
Кстати, естественно, что такой подход можно использовать и для кастомных структур, и для туплов. В общем, можно пользоваться. Хотите тяните буст, хотите сами пишите, там все равно не так сложно.
Use ready-made solutions. Stay cool.
#cppcore #STL #template
❤38🔥15👍8😁7
Методы, определенные внутри класса
#новичкам
Вы хотите написать header-only библиотеку логирования и собственно пишите:
Ваши пользователи вызывают из одной единицы трансляции метод Log:
И из второй:
А потом это все успешно линкуется в один бинарник. Как так? Должно же было сработать One Definition Rule, которое запрещает иметь более одного определения функции на всю программу? А у нас как раз все единицы трансляции видят определение метода Log.
Дело в том, что все методы, определенные внутри тела класса, неявно помечены как inline. Это не значит, что компилятор встроит код этих методов в вызывающий код. Это значит, что для таких методов разрешается иметь сколько угодно одинаковых определений внутри программы. На этапе линковки выберется одна любая реализация и везде, где будет нужен адрес метода для вызова будет подставляться адрес именно этой реализации.
Так что явно использовать ключевое слово inline в этом случае бессмысленно.
Но и в обычном, не херед-онли коде, можно определять методы внутри класса. Когда это стоит делать?
Каждая единица трансляции должна сгенерировать свой код для inline метода. Это значит, что обильное использование inline методов может привести к увеличенному времени компиляции.
Однако наличие определения метода внутри класса может быть использовано компилятором для встраивания его кода в caller. Это снижает издержки на вызов метода.
Противоречивые последствия. Либо быстрый рантайм и медленный компайл-тайм, либо наоборот. Как быть?
Обычно inline делают простые и короткие методы, типа сеттеров и геттеров, а длинные методы, которые менее вероятно будут встраиваться, выносят в цпп. Короткие функции сильнее всего страдают от оверхеда на вызов, который может быть сравним с временем выполнения самой функции. Но они не засоряют собой интерфейс класса, хэдэр также легко и быстро читается. Вот такой компромисс.
Look for a compromise. Stay cool.
#cppcore #goodpractice
#новичкам
Вы хотите написать header-only библиотеку логирования и собственно пишите:
// logger.hpp
namespace SimpleLogger {
enum class Level { Debug, Info, Warning, Error };
class Logger {
public:
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
static Logger &GetInstance() {
static Logger instance;
return instance;
}
void SetMinLevel(Level level) {
m_minLevel = level;
}
void Log(Level level, const std::string &message) {
if (level < m_minLevel)
return;
auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::lock_guard lock{m_mutex};
std::cout << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] " << "["
<< levelToString(level) << "] " << message << std::endl;
}
private:
Logger() : m_minLevel(Level::Info) { Log(Level::Info, "Logger initialized"); }
Level m_minLevel;
std::mutex m_mutex;
std::string levelToString(Level level) {
switch (level) {
case Level::Debug: return "DEBUG";
case Level::Info: return "INFO";
case Level::Warning: return "WARN";
case Level::Error: return "ERROR";
default: return "UNKNOWN";
}
}
};
} // namespace SimpleLogger
Ваши пользователи вызывают из одной единицы трансляции метод Log:
...
#include <logger.hpp>
...
using namespace SimpleLogger;
Logger::GetInstance().Log(Level::Info, "Select recent items");
db->Execute("Select bla bla");
...
И из второй:
...
#include <logger.hpp>
...
using namespace SimpleLogger;
if (!result) {
Logger::GetInstance().Log(Level::ERROR, "Result is empty");
throw std::runtime_error("Result is empty");
}
...
А потом это все успешно линкуется в один бинарник. Как так? Должно же было сработать One Definition Rule, которое запрещает иметь более одного определения функции на всю программу? А у нас как раз все единицы трансляции видят определение метода Log.
Дело в том, что все методы, определенные внутри тела класса, неявно помечены как inline. Это не значит, что компилятор встроит код этих методов в вызывающий код. Это значит, что для таких методов разрешается иметь сколько угодно одинаковых определений внутри программы. На этапе линковки выберется одна любая реализация и везде, где будет нужен адрес метода для вызова будет подставляться адрес именно этой реализации.
Так что явно использовать ключевое слово inline в этом случае бессмысленно.
Но и в обычном, не херед-онли коде, можно определять методы внутри класса. Когда это стоит делать?
Каждая единица трансляции должна сгенерировать свой код для inline метода. Это значит, что обильное использование inline методов может привести к увеличенному времени компиляции.
Однако наличие определения метода внутри класса может быть использовано компилятором для встраивания его кода в caller. Это снижает издержки на вызов метода.
Противоречивые последствия. Либо быстрый рантайм и медленный компайл-тайм, либо наоборот. Как быть?
Обычно inline делают простые и короткие методы, типа сеттеров и геттеров, а длинные методы, которые менее вероятно будут встраиваться, выносят в цпп. Короткие функции сильнее всего страдают от оверхеда на вызов, который может быть сравним с временем выполнения самой функции. Но они не засоряют собой интерфейс класса, хэдэр также легко и быстро читается. Вот такой компромисс.
Look for a compromise. Stay cool.
#cppcore #goodpractice
👍25🔥12❤9
Forwarded from Карьера в Bell Integrator
Они про скорость и многозадачность: асинхронные сетевые фреймворки для C++ 🚀
Когда дело доходит до обработки тысяч запросов, в бой вступает асинхронное программирование. Здесь потоки не блокируются при сетевых запросах или работе с файлами — они продолжают выполнять другие задачи.
Сравнительный анализ популярных С++ фреймворков, работающих как раз по такой парадигме, уже в карточках. Сделали, кстати, совместно с каналом Грокаем C++: там много полезного для разработчиков си-плюс-плюс, так что заходите!
#BellintegratorTeam #советыBell
Когда дело доходит до обработки тысяч запросов, в бой вступает асинхронное программирование. Здесь потоки не блокируются при сетевых запросах или работе с файлами — они продолжают выполнять другие задачи.
Сравнительный анализ популярных С++ фреймворков, работающих как раз по такой парадигме, уже в карточках. Сделали, кстати, совместно с каналом Грокаем C++: там много полезного для разработчиков си-плюс-плюс, так что заходите!
#BellintegratorTeam #советыBell
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25❤9🔥9👎1
Inline виртуальные методы
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы inline, то можно и виртуальные сделать. Что это изменит?
Ну то есть такая иерархия:
Получим ли мы какой-то профит от того, что виртуальные методы теперь inline?
Посмотрим на такой код:
Конечно же метод будет встраиваться в вызывающий код. Компилятор на этапе компиляции четко знает тип, с которым он работает, и может проводить оптимизации.
Но вообще говоря, это далеко не основной кейс использования виртуальных методов. Давайте ближе к реальной задаче:
Здесь все понятно. Компилятор реально не знает, какие типы находятся внутри my_vec в функции vec_base. Поэтому все, что он может сделать - это использовать указатель на таблицу виртуальных функций и пусть рантайме уже решается, какой конкретно метод вызвать.
Теперь посмотрим такой код:
И здесь тоже не инлайнится код! Хотя компилятор же видит, что Derived - это единственный наследник.
Но на деле это может быть не так. Единица трансляции с vec_derived может не знать о наследниках уже Derived класса, которые определены отдельно. Компилятор не может достоверно доказать, что у Derived нет наследников, поэтому и не может инлайнить код. Опять vtable и оверхэд.
И здесь в игру вступает ключевое слово final и девиртуализация вызовов, о которой мы говорили тут и тут. Пометив класс как final, мы можем ожидать, что компилятор поймет, что никаких наследников у него нет. Поэтому он может встраивать вызов метода foo для Derived класса в последнем примере.
Вот код на годболте с последними примерами и final девиртуализацией. Немного упростил его, чтобы легче было ассемблер читать.
Правды ради стоит сказать, что для девиртуализации нужны очень особенные условия, которых сложно достичь, используя стандартные практики программирования и паттерны ООП.
Учитывая, что
1️⃣ виртуальные функции обычно довольно громоздкие(их код будет генерится во всех единицах трансляции, что раздувает бинарь)
2️⃣ операции над непосредственно типами наследниками не распространены
3️⃣ а для девиртуализации нужно танцевать с бубнами
от определения чуть сложных виртуальных методов внутри класса могут быть только проблемы. Поэтому например в Chromium гайдлайнах четко прописано, что запрещены инлайн определения виртуальных методов. Все уносим в цпп.
Be useful. Stay cool.
#cppcore #OOP #design
#опытным
В догонку предыдущего поста. Если можно сделать обычные методы 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
1❤13👍6🔥6❤🔥2
Zero-Cost Abstractions
#новичкам
Нельзя в промышленных масштабах писать код без абстракций. Вряд ли вы за обозримое время напишите даже эхо-сервер на ассемблере. Даже сам язык программирования - это абстракция. Он позволяет писать программы более-менее на английском языке(или на патриотическом Русском на 1С).
Абстракции упрощают программирование. А что с перфомансом?
Гипотеза такова, что чем выше уровень абстракции, тем выше косты производительности в рантайме.
Это например четко видно на примере ООП. ООП и ООДизайн позволили нам создать почти весь софт, которым мы пользуемся. Но на вызов виртуальных функций накладывается большой дебафф: компилятор во время компиляции не знает конкретных типов и приходится использовать индиректные вызовы с помощью таблицы виртуальных функций. Никакого инлайнинга да и хотя бы конкретных фиксированных адресов.
Однако есть и такие абстракции, которые не накладывают оверхэд на рантайм! Они называются Zero-Cost абстракциями.
И это чуть ли не основаная философия программирования на С++. Мы можем играться с уровнями абстракции, писать самый отдаленный от железа код и за все это мы ничего не платим! Компилятор своим мегамозгом анализирует на код, выполняет кучу оптимизаций и на выходе получаем быструю, высокопроизводительную конфетку.
За примерами долго ходить не надо. Можно сложить элементы вектора руками, а можно использовать std::accumulate:
При этом для обоих вариантов генерируется почти идентичный код. И это с использованием итераторов и прочих прелестей стандартных контейнеров.
На каких китах стоит возможность использовать "бесплатные" абстракции в С++?
🐳 Полиморфизм времени компиляции, compile-time вычисления и метапрограммирование. Тут все просто: переносим вычисления с рантайма в компайл тайм с помощью шаблонов, constexpr и меты и радуемся жизни. Параллельно можно еще и кофеек себе заваривать, пока проект билдится.
🐳 Инлайнинг. Одна из основных оптимизаций компилятора. Позволяет встраивать код функции в вызывающий код. С помощью инлайнинга 4 вложенных вызова функций могут превратиться в одну сплошную портянку низкоуровневого кода без дорогостоящих инструкций call.
🐳 Другие оптимизации. Компилятор дополнительно выполняет кучу оптимизаций: переставляет инструкции, подставляет известные значения сразу в код, обрезает ненужные инструкции, векторизует циклы и тд. Все они нужны для одной цели - ускорение кода.
Объединяя эти механизмы, C++ стремится обеспечить абстракции высокого уровня, которые можно использовать для написания выразительного и читаемого кода, сохраняя при этом производительность сравнимую с более низкоуровневым кодом, написанным например на С.
Don't pay for abstraction. Stay cool
#cppcore #compiler
#новичкам
Нельзя в промышленных масштабах писать код без абстракций. Вряд ли вы за обозримое время напишите даже эхо-сервер на ассемблере. Даже сам язык программирования - это абстракция. Он позволяет писать программы более-менее на английском языке(или на патриотическом Русском на 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
👍33❤15🔥6😁3
Zero-Cost Abstraction. Или нет?
#опытным
Концепция бесплатных абстракций-то есть. Но концепции они обычно в банках-консервах хранятся. В реальности все немного сложнее и бесплатный только труд стажеров в современном IT.
Вот как много из вас думает, что уникальный указатель - это бесплатная абстракция? Думаю, что очень многие. Но давайте посмотрим на реальность.
Для чего используется уникальный указатель? Чтобы избавиться от сырых указателей, снять головную боль по освобождению памяти и четко показать в коде передачу владения. То есть такой код:
хорошо бы переписать с использованием std::unique_ptr:
Будет ли здесь оверхэд рантайма? Ох-ох-ох, еще как!
Вот асм для первого сниппета:
А вот для второго:
Он в 4 раза больше! Если внимательно присмотреться, то можно увидеть, что значительная часть кода здесь тратится на подчищение памяти и обработку исключений. Да, исключения - это вещь, где RAII очень сильно помогает. Вот вам ссылочки на годболт: тык и тык
Но предположим, что изначальный код был корректным и там не вылетало никаких исключений. Пометим функции как noexcept:
Вот асм:
Стало чуть меньше, но это явно не zero-cost.
Любую проблему можно решить с помощью введения дополнительного уровня индирекции, но эти индирекции накладывают свои косты.
Этот пост не для того, чтобы не пользоваться абстракциями. Просто у всего есть цена и надо это осознавать и в жизни, и при написании программ.
Be aware about costs. Stay cool.
#опытным
Концепция бесплатных абстракций-то есть. Но концепции они обычно в банках-консервах хранятся. В реальности все немного сложнее и бесплатный только труд стажеров в современном 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.
👍21❤10🔥9🤯3😁2
std::allocate_shared
#опытным
В одном из прошлых постов мы упоминали, что одним из недостатков std::make_shared является то, что с ней нельзя использовать кастомный менеджмент памяти.
Причиной является то, что для выделения памяти она использует глобальный new. Поэтому и для освобождения памяти должна использовать глобальный delete. Здесь нет места кастомщине.
Но что, если нам очень нужно по-особенному работать с памятью для объекта? Даже хотя бы просто отслеживать выделение и разрушение без влияния на глобальный delete?
Тут на помощью приходит функция std::allocate_shared. Первым аргументом она принимает аллокатор, который и будет ответственен за выделение и освобождение памяти.
Вот вам простой примерчик с простым STL-совместимым аллокатором, логирующим операции выделения и освобождения памяти:
Стандартом на аллокаторы накладываются определенные требования: нужно определить нужные алиасы, методы allocate и deallocate, структуру rebind и соответствующий конструктор копирования и операторы сравнения. Полный список требований можно прочитать тут.
По консольному выводу видно, что аллокатор выделяет немного больше памяти, чем должен занимать объект. Это сделано в том числе для того, чтобы хранить в выделенном куске еще и контрольный блок std::shared_ptr. Так что касаемо особенностей аллокации и деаллокации тут похожая ситуация с std::make_shared.
Аллокатор кстати хранится в том же контрольном блоке, так что информация о способе деаллокации берется оттуда.
Customize your solutions. Stay cool.
#cppcore #STL #memory
#опытным
В одном из прошлых постов мы упоминали, что одним из недостатков 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
Оператор, бороздящий просторы вселенной
#новичкам
В этом посте мы рассказали об одной фишке, которая может помочь при сравнении кастомных структур:
Однако иногда структуры требуется сравнивать и с помощью других операторов: >, ==, !=, >=, <=. В итоге полноценный набор операторов сравнения для Time выглядит так:
Попахивает зловонным бойлерплейтом.
Недавно увидел мем, где девочка 8-ми лет, которая изучает питон, спрашивает отца: "папа, а если компьютер знает, что здесь пропущено двоеточие, почему он сам не может его поставить?". И батя такой: "Я не знаю, дочка, я не знаю ...".
Здесь вот похожая ситуация. Компилятор же умеет сравнивать набор чисел в лексикографическом порядке. Какого хрена он не может сделать это за нас?
Начиная с С++20 может!
Теперь вы можете сказать компилятору, что вам достаточно простого лексикографического сравнения поле класса и пусть он сам его генерирует:
В отличие от специальных методов класса, компилятор не сгенерирует за нас эти операторы, если мы явно не попросим. Получается, что мы решили только полпроблемы и нам все равно нужно писать 6 скучных засоряющих код строчек. Хотелось бы один раз сказать, что нам нужны сразу все операторы.
Тут же нам на помощью приходит еще одна фича С++20 - трехсторонний оператор сравнения или spaceship operator. Теперь код выглядит так:
Spaceship потому что похож на космический корабль, имхо прям имперский истребитель из далекой-далекой.
Один раз определив этот оператор можно сравнивать объекты какими угодно операторами и это будет работать. Подробнее про применение будет в следующем посте.
Conquer your space. Stay cool.
#cppcore #cpp20
#новичкам
В этом посте мы рассказали об одной фишке, которая может помочь при сравнении кастомных структур:
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 операций сравнения:
Это уже прекрасно, но это еще не все!
Обратите внимание на сигнатуру spaceship operator. Зачем там нужен auto?
Вот теперь объясненяем, почему это называется оператор трехстороннего сравнения.
Он возвращает объект, который содержит информацию о результате сравнения:
Если результат сравнения >0, то первый операнд больше второго. И так далее по аналогии.
Тип возвращаемого значения у оператора один из этих трех:
- std::strong_ordering
- std::weak_ordering
- std::partial_ordering
Что они значат - тема отдельного разговора, но каждый из них может находится в одном из 3-х состояний: less, greater, equal. Это можно использовать, например, для проверки возвращаемых значений системных вызовов:
Кейсы применения непосредственно spaceship'а в коде не так обширны, потому что не очень привычно, есть вопросы к перфу(об этом в следующем посте) да и поди разберись с этими ордерингами еще. Но его точно стоит использовать для автоматической генерации 6 базовых операторов.
Be universal. Stay cool.
#cppcore #cpp20
#новичкам
В прошлом посте мы рассказали, как трехсторонний оператор сравнения может помочь сократить код определения операций сравнения, но это не единственное его предназначение. Сегодня подробнее рассмотрим, какую функциональность он предоставляет.
Ну для начала: наличие определенного 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🔥10⚡1
Spaceship оператор. Детали 2
#опытным
Продолжаем углубляться в космический оператор.
В прошлом посте мы рассказали о том, что определение одного единственного spaceship оператора возможность использовать все 6 операторов сравнения.
Это правда, но с оговорочками. На самом деле компилятор подменяет привычные операторы на использование spaceship оператора вот так:
Ну оно и понятно. Самих операторов не завезли, приходится компилятору как ужу извиваться.
Но не всегда трехсторонний оператор предоставляет все 6 операторов.
Вгляните на пример:
Мы всего лишь взяли стандартную строку и совсем чуть-чуть переопределили для нее трехсторонний оператор, чтобы удостоверится, что действительно он и вызывается для всех случаев.
Однако казус: операторы проверки на равенство не определены, тогда как операторы определения порядка элементов нормально работают.
Почему так?
Дело в производительности.
Для всех базовых типов проблем никаких нет, для них все за нас определено. Но давайте возьмем класс вектора и попробуем для него написать оператор трехстороннего сравнения:
Этот код работает и проходит все тесты. Однако для определения равенства этот код очень плохо перфомит. Вектора могут иметь очень длинный общий префикс, но по итогу иметь разные размеры. Логично сразу же проверить размеры векторов и, если они не равны, сразу принять решение. В рамках данного spaceship оператора это сделать невозможно(только по размерам нельзя определить отношение порядка), но стандарт нам не мешает явно определять нужные операторы:
И это будет работать для
Получается, что если мы сами определили оператор <=>, то нас не удовлетворило базовое лексикографическое сравнение. Поэтому скорее всего нас не удовлетворит и использование такого spaceship'а для проверки на равенство. Поэтому нужно писать его отдельно. И это решает проблему с производительностью.
Хорошие новости в том, что при оборачивании такого класса в другой класс, сравнения обертки будут использовать оптимизированное сравнение, а не spaceship реализацию:
#опытным
Продолжаем углубляться в космический оператор.
В прошлом посте мы рассказали о том, что определение одного единственного 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 реализацию:
👍10❤8🔥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
❤23👍7🔥5
std::less vs std::ranges::less
#опытным
В догонку к предыдущим постам. Вот у вас есть структурка TIme из С++17:
Вы как-то заполняете ее и сортируете:
И все работает прекрасно.
И тут решили вы перейти на С++20 и заменить все алгоритмы классической stl на алгоритмы ренджей. Синтаксис у них понятнее, да и почему бы и нет. Изменения тут будут тривиальные:
Запускаете сборку, а она падает с какими-то нечитаемыми ошибки, типа таких:
Нам говорили, что с концептами ошибки станут понятнее, но это только в теории. А на практике вот это.
В чем в итоге проблема?
По умолчанию в сортировке рэнджей используется компаратор std::ranges::less, а в обычной сортировке - std::less. И в их разнице и зарыта собака: std::ranges::less требует определения всех шести операторов сравнения или одного spaceship'а и все заработает:
Ну или можете пометить оператор< дефотным и передавать в сортировку std::less:
Пару раз натыкался на эту проблему, поэтому решил поделиться болью.
Give clear feedback. Stay cool.
#опытным
В догонку к предыдущим постам. Вот у вас есть структурка 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.
❤45👍25🔥18🤯1
Unity build
#опытным
Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.
Конечно это бесит всех в коммьюнити и все пытаются сократить время ожидания сборки. Для этого есть несколько подходов, один из которых мы обсудим сегодня.
Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали
Но помимо компиляции вообще-то есть линковка. И чем больше единиц трансляции, библиотек и все прочего, тем больше времени нужно линковщику на соединение все этого добра в одно целое.
Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)
Условно. Есть у вас 2 цппшника и один хэдэр:
Вы все цппшники подключаете в один файл unity_build.cpp:
И компилируете его. За счет гардов хэдэров у вас будет по одной версии каждого из них в едином файле, меньше кода анализируется и компилируется в принципе. Каждая инстанциация шаблона компилируется ровно однажды, а затраты на линковку отсутствуют. Красота!
Или нет?
У этой техники есть ряд недостатков:
Потеря преимуществ инкрементной сборки. При изменении даже одного маленького файла приходится перекомпилировать всю объединенную единицу трансляции, что значительно увеличивает время и именно пересборки. Сборка быстрее, но пересборка потенциально медленнее.
Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.
Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.
У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.
Solve the problem. Stay cool.
#cppcore #compiler #tools
#опытным
Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.
Конечно это бесит всех в коммьюнити и все пытаются сократить время ожидания сборки. Для этого есть несколько подходов, один из которых мы обсудим сегодня.
Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или 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
1❤23🔥7👍6😁2🗿1
Короткий совет по отладке кода от мэтра
#новичкам
Отладка занимает около 30% всего времени разработки. К тому же сам язык у нас способствует появлению самых неожиданных проблем. Сидишь целый день и не вдупляешь, почему тесты падают.
Невозможно дать конкретный алгоритм по пунктам, что нужно делать. Однако общие советы могут помочь направить внимание в нужную точку. Вот, что об отладке говорит сам Страуструп:
Почему это работает?
✅ Отбрасывая все ненужное, вы сужаете пространство для анализа возможного проблемного места. Меньше кода для анализа - больше вероятность понять место проблемы.
✅ Вместо того, чтобы ныть, вы встаете в авторскую позицию. Это очень актуально для новичков: энтропия задачи ощутимо выше текущих навыков и просто опускаются руки.
Но вашей целью может стать не пофиксить неизвестную причину проблему, на самом минимальном примере попытаться воспроизвести проблему во всей красе.
✅ Если вы так и не поняли, в чем проблема, с маленьким примером вам намного охотнее и эффективнее смогут помочь коллеги.
Как локализовать проблему?
🔍 Мокайте зависимости от других компонентов и модулей.
🔍 Фиксируйте значения переменных.
🔍 Упрощайте входные данные.
Это практика полезна и с точки зрения архитектуры. Если вам много всего нужно менять для составления маленького примера, возможно у вас высокая связность и стоит порефакторить код после успешной отладки.
А какие вы дадите полезные советы по отладке кода?
Fix problems playfully. Stay cool.
#goodpractice
#новичкам
Отладка занимает около 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🔥19❤7👍6❤🔥3🤣3