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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Недостатки std::make_shared. Кастомный new и delete
#новичкам

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

И начнем с непопулярного.

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

class A {
public:
void *operator new(size_t) {
std::cout << "allocate\n";
return ::new A();
}

void operator delete(void *a) {
std::cout << "deallocate\n";
::delete static_cast<A *>(a);
}
};

int main() {
const auto a =
std::make_shared<A>(); // ignores overloads
//const auto b =
// std::shared_ptr<A>(new A); // uses overloads
}
// OUTPUT:
// Пусто!


В общем, если нужный кастомный менеджент памяти, то std::make_shared - не ваш бро.

Customize your solutions. Stay cool.

#cppcore #cpp11 #memory
628👍18😁14🔥4❤‍🔥1
Недостатки std::make_shared. Непубличные конструкторы
#новичкам

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

Если конструктор публичный - проблем нет, просто вызываем std::make_shared.

Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.

Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:

struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};


Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.

В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.

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

Hide your secrets. Stay cool.

#cppcore #cpp11
614👍14🔥61
​​Недостатки std::make_shared. Кастомные делитеры
#новичкам

Заходим на cppreference и видим там такие слова:

This function may be used as an alternative to std::shared_ptr<T>(new T(args...)).

Также видим ее сигнатуру:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );


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

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

Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".

Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.

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

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

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

Customize your solutions. Stay cool.

#cppcore #cpp11 #memory
🔥177👍62❤‍🔥1
Одно значимое улучшение С++17
#опытным

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

Возьмем, например, вызов функции:

f( g(expr1, expr2), h(expr3) );


В каком порядке вызываются expr1, expr2, expr3, g, h и f?

Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.

Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.

Что было до С++17?

Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!

То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.

Полный бардак! И это приводило на самом деле к неприятным последствиям.

Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:

void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}

bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));


Какие тут могут быть проблемы?

Итоговый порядок вычислений может быть следующий:

new SomeClass1{} -> new SomeClass2{} -> std::unique_ptr<SomeClass1> -> std::unique_ptr<SomeClass2>


Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.

Эту проблему решали с помощью std::make_* фабрик умных указаателей:

bar(std::make_unique<SomeClass1>(), std::make_unique<SomeClass2>());


Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.

Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.

Что стало с наступлением С++17?

f(e(), g(expr1, expr2), h(expr3));


До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.

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

Теперь такой код не будет проблемой:

void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}

bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));


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

Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.

Fix problems. Stay cool.

#cppcore #memory #cpp17
👍3615🔥10❤‍🔥4
Помогите Доре найти ошибку
#опытным

А у нас новая рубрика #бага, где мы пытаемся найти нетривиальные ошибки в коде. Коллективные усилия и жаркие обсуждения в комментариях приветствуются.

Вот такой код:

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);
}
};


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

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

Сможете найти? Пишите свои варианты в комментариях.

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

Deduce the error. Stay cool.
13👍7🤓6🔥3🤔1
Бага обнаружена!
#опытным

Проблема в текущей реализации

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👍2012🔥5❤‍🔥3
​​Как использовать std::unordered_map с ключом в виде std::pair?
#опытным

При работе над задачами 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
37🔥15👍8😁7
​​Методы, определенные внутри класса
#новичкам

Вы хотите написать 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🔥129
Они про скорость и многозадачность: асинхронные сетевые фреймворки для 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