Исключения в перемещающем конструкторе
Продолжаем серию постов. Как вы могли заметить, во всех примерах с перемещающим конструктором был поставлен спецификатор
И неспроста я это делал! Я бы даже сказал, что где-то это является очень важным требованием.
Возьмем в качестве примера всем нам известный
Когда же есть возможность переместить объект? Оказывается, наличие обычного перемещающего конструктора — это недостаточное условие! Необходимо гарантировать, что перемещение будет выполнено успешно и без исключений.
Про исключения мы пока не успели написать, но в рамках этой статьи можно считать, что это специальный способ сообщить об ошибке, которую можно обработать без падения программы.
Представим ситуацию, что МЫ - ВЕКТОР. Вот мы выделили новую память и начали туда перемещать объекты. И где-то на середине процесса получаем исключение при перемещении одного из объектов. Что нам делать-то с этим? Вообще говоря, надо разрушить все что переместили в новой памяти и сообщить об этом пользователю. Т.е. откатить все назад. НО! Назад дороги нет 😅 Разрушать объекты из новой области памяти нельзя — их ресурсы перемещены из старой памяти. Обратно перемещать тоже нельзя — вдруг опять исключение прилетит? Брать на себя ответственность сделать что-то одно тоже нельзя — мы вектор из стандартной библиотеки. В общем, встаем в аналитический ступор...
Таким образом, мы приходим к выводу, что перемещать можно, если есть явные гарантии от пользовательского класса. И это действительно так, взгляните на живой пример 1.
Конечно, если копирующий конструктор запрещен (например), то будет вызван хоть какой-то, т.е. перемещающий с исключениями: живой пример 2. Тут важно отметить стремление разработчиков STL обезопаситься там, где это возможно.
Если мы тоже хотим по возможности не нести ответственность за касяки стороннего класса, то нам приходит на помощь функция:
Она делает всё то же самое, что и классическая
Пользовательские классы очень и очень часто засовывают в стандартные контейнеры. Порой это происходит не сразу, через долгое время. Следовательно, если производительность в проекте важна, то побеспокоиться о гарантиях работы без исключений при перемещении есть смысл сразу, как только был написан наш класс. Либо же есть другой путь — копировать всё подряд, но это тема другого поста 😊
Надеюсь, что мне удалось вас убедить в важности
#cppcore #memory #algorithm
Продолжаем серию постов. Как вы могли заметить, во всех примерах с перемещающим конструктором был поставлен спецификатор
noexcept:class string
{
public:
string(string &&other) noexcept
{ ... }
};
И неспроста я это делал! Я бы даже сказал, что где-то это является очень важным требованием.
Возьмем в качестве примера всем нам известный
std::vector. Одним из свойств этой структуры данных является перевыделение памяти большего размера, при увеличении количества объектов. При этом старые объекты отправляются в новый участок памяти. Логично задаться вопросом — как? И логично ответить, что в целях повышенной производительности нужно выполнять перемещение каждого объекта, а не копирование, если есть возможность.Когда же есть возможность переместить объект? Оказывается, наличие обычного перемещающего конструктора — это недостаточное условие! Необходимо гарантировать, что перемещение будет выполнено успешно и без исключений.
Про исключения мы пока не успели написать, но в рамках этой статьи можно считать, что это специальный способ сообщить об ошибке, которую можно обработать без падения программы.
Представим ситуацию, что МЫ - ВЕКТОР. Вот мы выделили новую память и начали туда перемещать объекты. И где-то на середине процесса получаем исключение при перемещении одного из объектов. Что нам делать-то с этим? Вообще говоря, надо разрушить все что переместили в новой памяти и сообщить об этом пользователю. Т.е. откатить все назад. НО! Назад дороги нет 😅 Разрушать объекты из новой области памяти нельзя — их ресурсы перемещены из старой памяти. Обратно перемещать тоже нельзя — вдруг опять исключение прилетит? Брать на себя ответственность сделать что-то одно тоже нельзя — мы вектор из стандартной библиотеки. В общем, встаем в аналитический ступор...
Таким образом, мы приходим к выводу, что перемещать можно, если есть явные гарантии от пользовательского класса. И это действительно так, взгляните на живой пример 1.
Конечно, если копирующий конструктор запрещен (например), то будет вызван хоть какой-то, т.е. перемещающий с исключениями: живой пример 2. Тут важно отметить стремление разработчиков STL обезопаситься там, где это возможно.
Если мы тоже хотим по возможности не нести ответственность за касяки стороннего класса, то нам приходит на помощь функция:
std::move_if_noexcept(object);
Она делает всё то же самое, что и классическая
std::move, но только если перемещающий конструктор помечен как noexcept (или кроме перемещающего конструктора нет альтернатив). А вот если внутри метода, помеченного noexcept, исключение всё таки будет брошено, то будет все очень очень плохо... Скажу по опыту, такое отладить достаточно тяжело. Поговорим об этом, когда наступит время серии постов про исключения 😉Пользовательские классы очень и очень часто засовывают в стандартные контейнеры. Порой это происходит не сразу, через долгое время. Следовательно, если производительность в проекте важна, то побеспокоиться о гарантиях работы без исключений при перемещении есть смысл сразу, как только был написан наш класс. Либо же есть другой путь — копировать всё подряд, но это тема другого поста 😊
Надеюсь, что мне удалось вас убедить в важности
noexcept в перемещающем конструкторе. Осталось совсем немного - оптимизации RVO/NRVO.#cppcore #memory #algorithm
🔥18❤6👍6❤🔥1
Оптимизации RVO / NRVO
Всем привет! Настало время завершающего поста этой серии. Сегодня мы поговорим об одной из самых нетривиальных оптимизаций в С++.
Я очень удивлюсь, если встречу человека, который по мере изучения стандартных контейнеров никогда не задумывался, что эти ребята слишком «жирные», чтобы их просто так возвращать в качестве результата функции или метода:
...и приходили к мысли, что нужно заполнять уже существующий объект:
Эта мысль волновала всех с давних времен... Поэтому она нашла поддержку от разработчиков компиляторов.
Существует такие древние оптимизации, как RVO (Return Value Optimization) и NRVO (Named Return Value Optimization). Они призваны избавить нас от потенциально избыточных и лишних вызовов конструктора копирования для объектов на стеке. Например, в таких ситуациях:
Давайте взглянем на живой пример 1, в котором вызов конструктора копирования явно пропускается. Вообще говоря, эта информация немного выбивается в контексте постов, посвященных move семантике C++11, т.к. это работает даже на C++98. Вот поэтому я её называю древней 😉
Немного теории. При вызове функции резервируется место на стеке, куда должно быть записано возвращаемое значение функции. Если компилятор может гарантировать, что функция возвращает единственный локальный объект, тип которого совпадает с
Иными словами, компилятор пытается понять, можно ли "подсунуть" область памяти
В конце поста потом почитайте ассемблерный код в комментариях, а пока продолжим.
RVO отличается NRVO тем, что в первом случае выполняется оптимизация для объекта, который создается при выходе из функции в
А во втором для возвращаемого именованного объекта:
Но при этом замысел и суть остаются такими же! Тут важно отметить, что и вам, и компилятору, по объективным причинам, намного проще доказать корректность RVO, чем NRVO.
Давайте покажу, когда NRVO может не сработать и почему. Рассмотрим кусочек из живого примера 2:
Оптимизация NRVO не выполнится. В данном примере компилятору будет неясно, какой именно из объектов
Продолжение в комментариях!
#cppcore #memory #algorithm #hardcore
Всем привет! Настало время завершающего поста этой серии. Сегодня мы поговорим об одной из самых нетривиальных оптимизаций в С++.
Я очень удивлюсь, если встречу человека, который по мере изучения стандартных контейнеров никогда не задумывался, что эти ребята слишком «жирные», чтобы их просто так возвращать в качестве результата функции или метода:
std::string get_very_long_string();
...и приходили к мысли, что нужно заполнять уже существующий объект:
void fill_very_long_string(std::string &);
Эта мысль волновала всех с давних времен... Поэтому она нашла поддержку от разработчиков компиляторов.
Существует такие древние оптимизации, как RVO (Return Value Optimization) и NRVO (Named Return Value Optimization). Они призваны избавить нас от потенциально избыточных и лишних вызовов конструктора копирования для объектов на стеке. Например, в таких ситуациях:
// RVO example
Foo f()
{
return Foo();
}
// NRVO example
Foo f()
{
Foo named_object;
return named_object;
}
// Foo no coping
Foo obj = f();
Давайте взглянем на живой пример 1, в котором вызов конструктора копирования явно пропускается. Вообще говоря, эта информация немного выбивается в контексте постов, посвященных move семантике C++11, т.к. это работает даже на C++98. Вот поэтому я её называю древней 😉
Немного теории. При вызове функции резервируется место на стеке, куда должно быть записано возвращаемое значение функции. Если компилятор может гарантировать, что функция возвращает единственный локальный объект, тип которого совпадает с
lvalue, тогда он может сразу сконструировать этот объект напрямую в ожидаемом месте вызывающего кода. Допустимо отличаться на константность.Иными словами, компилятор пытается понять, можно ли "подсунуть" область памяти
lvalue при вычислении rvalue и гарантировать, что мы получим тот же результат, что и при обычном копировании. Можно считать, что компилятор преобразует код в следующий:void f(Foo *address)
{
// construct an object Foo
// in memory at address
new (address) Foo();
}
int main()
{
auto *address = reinterpret_cast<Foo *>(
// allocate memory directly on stack!
alloca(sizeof(Foo))
);
f(address);
}
В конце поста потом почитайте ассемблерный код в комментариях, а пока продолжим.
RVO отличается NRVO тем, что в первом случае выполняется оптимизация для объекта, который создается при выходе из функции в
return:// RVO example
Foo f()
{
return Foo();
}
А во втором для возвращаемого именованного объекта:
// NRVO example
Foo f()
{
Foo named_object;
return named_object;
}
Но при этом замысел и суть остаются такими же! Тут важно отметить, что и вам, и компилятору, по объективным причинам, намного проще доказать корректность RVO, чем NRVO.
Давайте покажу, когда NRVO может не сработать и почему. Рассмотрим кусочек из живого примера 2:
// NRVO failed!
Foo f(bool value)
{
Foo a, b;
if (value)
return a;
else
return b;
}
Оптимизация NRVO не выполнится. В данном примере компилятору будет неясно, какой именно из объектов
a или b будет возвращен. Несмотря на то, что объекты БУКВАЛЬНО одинаковые, нельзя гарантировать применимость NRVO. До if (value) можно было по-разному поменять каждый из объектов и их память. Или вдруг у вас в конструкторе Foo зашит генератор случайных чисел? 😉 Следовательно, компилятору может быть непонятно куда надо конструировать объект напрямую из этих двух. Тут будет применено копирование.Продолжение в комментариях!
#cppcore #memory #algorithm #hardcore
🔥17👍8❤5
shared_ptr и массивы
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
⚡️Использовать лямбду:
⚡️Ну или воспользоваться уже готовым вариантом:
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory
Есть одна не самая приятная вещь при работе с std::shared_ptr. С момента его выхода в С++11 и в С++14 он не может быть использован из коробки для того, чтобы хранить динамические массивы. По дефолту во всех случаях при исчерпании ссылок на объект, шареный указатель вызывает оператор delete. Однако, когда мы аллоцируем динамический массив new[], мы хотим вызвать delete[] для его удаления. Но shared_ptr просто вызовет delete. А это неопределенное поведение.
То есть я не могу просто так вот взять и написать
shared_ptr<int[]> sp(new int[10]);
Кстати говоря, у его собрата std::unique_ptr с этим все получше. У него есть отдельная частичная специализация для массивов. Поэтому вот так я могу написать спокойно:
std::unique_ptr<int[]> up(new int[10]); // вызовется корректный delete[]
Что можно сделать, чтобы таки использовать сишные массивы с шареным указателем?
👉🏿 Обернуть указатель на массив в класс и шарить уже объекты этого класса. Типа того(упрощенно):
template <class T>
struct DynamicArrayWrapper {
DynamicArrayWrapper(size_t size) : ptr{new T[size]} {}
~DynamicArrayWrapper() {delete[] ptr;}
T * ptr;
};
std::shared_ptr<DynamicArrayWrapper> sp{10};
У такого метода есть 2 проблемы. Первое - прокси класс. Дополнительные обертки увеличивают объем и сложность кода и затрудняют его понимание. Второе - перформанс. Здесь уже два уровня индирекции, что замедлит обработку.
👉🏿 Передать свой кастомный делитер. Тут тоже несколько вариантов.
⚡️Написать свой:
template< typename T >
struct array_deleter
{
void operator ()( T const * p)
{
delete[] p;
}
};
std::shared_ptr<int> sp(new int[10], array_deleter<int>());
⚡️Использовать лямбду:
std::shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });⚡️Ну или воспользоваться уже готовым вариантом:
std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
std::default_delete имеет частичную специализацию для массивов.
Но! Какой хороший все-таки стандарт С++17, который поправил многие такие маленькие косячки. А как он это сделал - увидим в следующий раз)
Be comfortable to work with. Stay cool.
#cpp11 #memory
👍12🔥10❤4
Исправляем косяк std::shared_ptr с массивами
Ну не мы сами, конечно. Стандарт С++17 исправляет этот момент.
Что мы теперь имеем.
Для создания объекта таким конструктором:
используется делитер delete ptr, если T - не массив, и delete[] ptr если Т -массив.
Также теперь изменился тип хранимого объекта element_type. Раньше был просто шаблонный тип Т, теперь же это
std::remove_extent - это такой type_trait. Все, что нужно о нем знать - если Т - массив, то тип element_type будет совпадать с типом элементов массива.
Теперь мы даже можем использовать operator[] для доступа к элементам массива. Делается это так:
Так что теперь это действительно полноценные шареные массивы из коробки. Весь интерфейс подогнали под это дело.
Но вот вопрос: а нафига это вообще надо? Когда кто-то вообще в последний раз использовал динамический массив?
Мы же вроде на плюсах пишем. Есть плюсовые решения - std::vector, если размер не известен на момент компиляции, и std::array, если известен. У них и интерфейс удобный и унифицированный и все-таки это объектно-ориентированный подход. И сердцу тепло, и глаз радуется. Динамические массивы выглядят, как окаменелые какашки динозавров.
C std::array соглашусь. Думаю, что нет адекватных оправданий использования динамических и статических массивов, длина которых известна в compile-time. std::array - очень простая и тонкая обертка над статическим массивом и ее использование вырождается компилятором до использования массива.
Но вот с векторами немного сложнее. Удобство требует жертв. Именно в плане производительности. Поэтому в узких бутылочных горлышках, где надо выжимать всю скорость из кода, лучше использовать динамические массивы вместо std::vector. Видел запрос от Захара на пример, который подверждает эту мысль. Отвечу на него в другом посте как-нибудь. Но обычному бэкэндеру, думаю, это сильно не пригодится.
Если фича есть, значит она кому-то нужна. Просто иногда интересно узнать о таких минорных изменениях. А кому-то поможет больше не использовать кастомные делитеры и иметь более понятный код.
Fix your flaws. Stay cool.
#cpp17 #memory
Ну не мы сами, конечно. Стандарт С++17 исправляет этот момент.
Что мы теперь имеем.
Для создания объекта таким конструктором:
template< class T >
explicit shared_ptr( T* ptr );
используется делитер delete ptr, если T - не массив, и delete[] ptr если Т -массив.
Также теперь изменился тип хранимого объекта element_type. Раньше был просто шаблонный тип Т, теперь же это
using element_type = remove_extent_t<T>;
std::remove_extent - это такой type_trait. Все, что нужно о нем знать - если Т - массив, то тип element_type будет совпадать с типом элементов массива.
Теперь мы даже можем использовать operator[] для доступа к элементам массива. Делается это так:
std::shared_ptr<int[]> num(new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
for (std::size_t i = 0; i < 10; ++i)
std::cout << num[i] << ' ';Так что теперь это действительно полноценные шареные массивы из коробки. Весь интерфейс подогнали под это дело.
Но вот вопрос: а нафига это вообще надо? Когда кто-то вообще в последний раз использовал динамический массив?
Мы же вроде на плюсах пишем. Есть плюсовые решения - std::vector, если размер не известен на момент компиляции, и std::array, если известен. У них и интерфейс удобный и унифицированный и все-таки это объектно-ориентированный подход. И сердцу тепло, и глаз радуется. Динамические массивы выглядят, как окаменелые какашки динозавров.
C std::array соглашусь. Думаю, что нет адекватных оправданий использования динамических и статических массивов, длина которых известна в compile-time. std::array - очень простая и тонкая обертка над статическим массивом и ее использование вырождается компилятором до использования массива.
Но вот с векторами немного сложнее. Удобство требует жертв. Именно в плане производительности. Поэтому в узких бутылочных горлышках, где надо выжимать всю скорость из кода, лучше использовать динамические массивы вместо std::vector. Видел запрос от Захара на пример, который подверждает эту мысль. Отвечу на него в другом посте как-нибудь. Но обычному бэкэндеру, думаю, это сильно не пригодится.
Если фича есть, значит она кому-то нужна. Просто иногда интересно узнать о таких минорных изменениях. А кому-то поможет больше не использовать кастомные делитеры и иметь более понятный код.
Fix your flaws. Stay cool.
#cpp17 #memory
🔥8❤6👍3😁1
std::make_shared в С++20
Начиная со стандарта С++11 в С++ появилась поддержка создания std::shared_ptr при помощи фабричной функции std::make_shared. У нас даже есть пост про особенности этой функции вот здесь. Но у нее были такие же недостатки, как и у std::shared_ptr до С++17. Нельзя было ее использовать для массивов. Но, как отметил уже в комментах Константин, начиная с С++20 эта фабричная функция синхронизировалась со своим вдохновителем и теперь тоже поддерживает создание массивов из std::shared_ptr. Например:
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024): создает std::shared_ptr c 1024 значениями типа double, проинициализированными по умолчанию;
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024, 1.0): создает std::shared_ptr c 1024 значениями типа double, проинициализированными значениями, равными 1,0.
Как обычно make функции немного тормозят относительно типов, для которых они созданы. Типа std::make_unique появился только в с++14, хотя сам уникальный указатель был представлен в предыдущем релизе. Но главное, что эти особенности все-таки доезжают, что не может не радовать.
Enjoy small things. Stay cool.
#cpp20 #memory
Начиная со стандарта С++11 в С++ появилась поддержка создания std::shared_ptr при помощи фабричной функции std::make_shared. У нас даже есть пост про особенности этой функции вот здесь. Но у нее были такие же недостатки, как и у std::shared_ptr до С++17. Нельзя было ее использовать для массивов. Но, как отметил уже в комментах Константин, начиная с С++20 эта фабричная функция синхронизировалась со своим вдохновителем и теперь тоже поддерживает создание массивов из std::shared_ptr. Например:
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024): создает std::shared_ptr c 1024 значениями типа double, проинициализированными по умолчанию;
⚡️ std::shared_ptr<double[]> shar = std::make_shared<double[]>(1024, 1.0): создает std::shared_ptr c 1024 значениями типа double, проинициализированными значениями, равными 1,0.
Как обычно make функции немного тормозят относительно типов, для которых они созданы. Типа std::make_unique появился только в с++14, хотя сам уникальный указатель был представлен в предыдущем релизе. Но главное, что эти особенности все-таки доезжают, что не может не радовать.
Enjoy small things. Stay cool.
#cpp20 #memory
🔥11👍3❤1
Все грани new
#опытным
Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3!
Сейчас со всем разберемся.
То, что наиболее часто используется, называется new expression. Это выражение делает 2 вещи последовательно: пытается в начале выделить подходящий объем памяти, а потом пытается сконструировать либо один объект, либо массив объектов в уже аллоцированной памяти. Возвращает либо указатель на объект, либо указатель на начало массива.
Выглядит оно так:
Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти?
За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:
Но что, если у меня уже есть выделенная память и я хочу на ней создать объект? Допустим, я не хочу использовать кучу и у меня есть массивчик на стеке, который я хочу переиспользовать для хранения разных объектов, потенциально разных типов.
Тогда мне нужен инструмент, который позволяет только вызывать конструктор на готовой памяти.
Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.
В этом случае кстати вызывается специальная перегрузка operator new:
Однако она не делает ничего полезного и просто возвращает второй аргумент наружу.
Вот так по разному в С++ можно использовать new. Главное - не запутаться!
Have a lot of sides. Stay cool.
#cppcore #memory
#опытным
Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3!
Сейчас со всем разберемся.
То, что наиболее часто используется, называется new expression. Это выражение делает 2 вещи последовательно: пытается в начале выделить подходящий объем памяти, а потом пытается сконструировать либо один объект, либо массив объектов в уже аллоцированной памяти. Возвращает либо указатель на объект, либо указатель на начало массива.
Выглядит оно так:
// creates dynamic object of type int with value equal to 42
int* p_triv = new int(42);
// created an array of 42 dynamic objects of type int with values equal to zero
int* p_triv_arr = new int[42];
struct String {std::string str};
// creates dynamic object of custom type String using aggregate initialization
String* p_obj = new String{"qwerty"};
// created an array of 5 dynamic objects of custom type String with default initialization
String* p_obj = new String[5];
Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти?
За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:
// class-specific allocation functions
struct X
{
static void* operator new(std::size_t count)
{
std::cout << "custom new for size " << count << '\n';
// explicit call to operator new
return ::operator new(count);
}
static void* operator new[](std::size_t count)
{
std::cout << "custom new[] for size " << count << '\n';
return ::operator new;
}
};
int main()
{
X* p1 = new X;
delete p1;
X* p2 = new X[10];
delete[] p2;
}
Но что, если у меня уже есть выделенная память и я хочу на ней создать объект? Допустим, я не хочу использовать кучу и у меня есть массивчик на стеке, который я хочу переиспользовать для хранения разных объектов, потенциально разных типов.
Тогда мне нужен инструмент, который позволяет только вызывать конструктор на готовой памяти.
Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.
alignas(T) unsigned char buf[sizeof(T)];
// after new in parentheses we specified location of future object
T* tptr = new(buf) T;
// You must manually call the object's destructor
tptr->~T();
В этом случае кстати вызывается специальная перегрузка operator new:
void* operator new (std::size_t count, void* ptr);
Однако она не делает ничего полезного и просто возвращает второй аргумент наружу.
Вот так по разному в С++ можно использовать new. Главное - не запутаться!
Have a lot of sides. Stay cool.
#cppcore #memory
30👍34❤18🔥8🏆3⚡1
new vs malloc
Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим.
Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать.
👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память.
👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель.
👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций.
👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно.
👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя).
👉🏿 Для new не нужно вручную высчитывать количество нужных байт. То есть мы не лезем на низкий уровень. Мы заботимся только типе данных, количестве объектов и об аргументах конструктора.
👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete.
Однако они имеют одну неочевидную схожесть. Нужно стараться по максимуму избегать их явного вызова. Давно придумали умные указатели и контейнеры, которые позволяют максимально освободить разработчика от обязанности ручного управления памятью.
Мы все же современные плюсовики. Поэтому в большинстве случаев, вам не нужны будут прямые вызовы этих функций. В более редких случаях(например кастомные аллокаторы) можно явно использовать new. Ну и в совсем редких случаях(нужда в реаллокации памяти или работа с сишным кодом) можно использовать malloc.
Control your memory. Stay cool.
#cppcore #interview #memory
Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим.
Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать.
👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память.
👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель.
👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций.
👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно.
👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя).
👉🏿 Для new не нужно вручную высчитывать количество нужных байт. То есть мы не лезем на низкий уровень. Мы заботимся только типе данных, количестве объектов и об аргументах конструктора.
👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete.
Однако они имеют одну неочевидную схожесть. Нужно стараться по максимуму избегать их явного вызова. Давно придумали умные указатели и контейнеры, которые позволяют максимально освободить разработчика от обязанности ручного управления памятью.
Мы все же современные плюсовики. Поэтому в большинстве случаев, вам не нужны будут прямые вызовы этих функций. В более редких случаях(например кастомные аллокаторы) можно явно использовать new. Ну и в совсем редких случаях(нужда в реаллокации памяти или работа с сишным кодом) можно использовать malloc.
Control your memory. Stay cool.
#cppcore #interview #memory
🔥41👍17❤8⚡2👎1
Безопасный для исключений new
#опытным
Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено.
Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.
В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc.
Так что, если хотите избавиться от исключений - вот вам еще один инструмент.
#cppcore #memory
#опытным
Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено.
Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.
MyClass * p1 = new MyClass; // привычное использование
MyClass * p2 = new (std::nothrow) MyClass; // небросающая версия
В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc.
Так что, если хотите избавиться от исключений - вот вам еще один инструмент.
#cppcore #memory
❤🔥39👍25🔥9❤8⚡1
Еще один способ сделать new небросающим
#опытным
Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler.
Это такой typedef'чик:
И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[].
Чтобы установить такой хэдлер используется функция std::set_new_handler:
Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик.
Предполагаемое назначение хэндлера - одна из трех вещей:
1️⃣ Сделать больше памяти доступной. (За гранью фантастики)
2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок.
3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему.
Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера?
На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер.
С обработчиками есть один нюанс.
Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc.
Примерно так это работает:
Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?"
Спасибо @Nikseas_314 за идею для поста)
Customize your tools. Stay cool.
#memory #cppcore
#опытным
Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler.
Это такой typedef'чик:
typedef void (*new_handler)();
И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[].
Чтобы установить такой хэдлер используется функция std::set_new_handler:
std::new_handler set_new_handler(std::new_handler new_p) noexcept;
Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик.
Предполагаемое назначение хэндлера - одна из трех вещей:
1️⃣ Сделать больше памяти доступной. (За гранью фантастики)
2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок.
3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему.
Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера?
На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер.
С обработчиками есть один нюанс.
Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc.
Примерно так это работает:
void handler()
{
std::cout << "Memory allocation failed, terminating\n";
std::set_new_handler(nullptr);
}
int main()
{
std::set_new_handler(handler);
try
{
while (true)
{
new int [1000'000'000ul] ();
}
}
catch (const std::bad_alloc& e)
{
std::cout << e.what() << '\n';
}
}
Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?"
Спасибо @Nikseas_314 за идею для поста)
Customize your tools. Stay cool.
#memory #cppcore
👍37😁16❤🔥6❤4🔥2
emplace_back vs push_back
#новичкам
Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора).
В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back.
По сигнатуре видно, что они предназначены немного для разного.
Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться.
push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back:
Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов.
То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая.
Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.
Be just better. Stay cool.
#STL #memory
#новичкам
Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора).
В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back.
template< class... Args >
reference emplace_back( Args&&... args ); // returns ref to created element
void push_back( const T& value );
void push_back( T&& value );
По сигнатуре видно, что они предназначены немного для разного.
Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
template <typename... Args>
reference emplace_back(Args&&... args) {
if (size == capacity) grow();
return *new (start + size++) T(std::forward<Args>(args)...);
}
Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться.
push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back:
void push_back(T&& value) {
emplace_back(std::move(value));
}Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов.
То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая.
Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.
Be just better. Stay cool.
#STL #memory
🔥30👍13❤6😁3🤔3⚡2
Когда мы вынуждены явно использовать new
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
Но можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
auto ptr = std::unique_ptr<int, void()(int*)>(new int(42), [](int* p) {
delete p;
std::cout << "Custom deleter called!\n";
});👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
struct Class {
static std::unique_ptr<Class> Create() {
// return std::make_unique<Class>(); // It will fail.
return std::unique_ptr<Class>(new Class);
}
private:
Class() {}
};👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
struct MyStruct {
int a, b, c;
};
auto ptr = std::make_unique<MyStruct>(1, 2, 3); // Will fail C++17
auto ptr = std::unique_ptr<MyStruct>(new MyStruct{1, 2, 3}); // NormНо можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
👍24❤7🔥2😁2⚡1
Перегружаем деструктор
#новичкам
Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных.
Но можно ли перегружать деструктор класса?
Резонный вопрос, деструктор - это такой же метод и такая же функция, почему бы его и не перегрузить.
По поводу дополнительных параметров деструктора.
Деструкторы стековых переменных вызываются неявно при выходе из скоупа. В языке просто нет инструментов, чтобы сообщить компилятору, как надо удалить объект. Способ только один. Удаление объектов, аллоцированных на стеке, ничем не должно идейно отличаться от удаления автоматических переменных. Поэтому и операторы delete и delete[] не принимают никаких аргументов.
Единственный вариант остается - это передавать дополнительные параметры при явном вызове деструктора. Однако кейсы применимости явного вызова деструктора и так сильно ограничены. Добавлять в стандарт перегрузку деструкторов, чтобы на этом строилась какая-то логика - излишне. И если вам уж захотелось построить какую-то логику на удалении, то можно ее вынести в статический метод destroy.
Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом.
Нет никакой опциональной логики при освобождении ресурсов в деструкторе. Вне зависимости от типа объекта и его ссылочности, данные внутри него выглядят одинаково. А значит и деструктор должен делать свою работу единообразно.
Не то, чтобы сильно полезный пост. У новичков иногда возникают такие вопросы. Но в принципе иногда нужно задумываться над такими, казалось бы, привычными вещами, чтобы глубже понимать инструменты, с которыми мы работаем.
Have a deeper understanding. Stay cool.
#memory #cppcore
#новичкам
Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных.
Но можно ли перегружать деструктор класса?
Резонный вопрос, деструктор - это такой же метод и такая же функция, почему бы его и не перегрузить.
По поводу дополнительных параметров деструктора.
Деструкторы стековых переменных вызываются неявно при выходе из скоупа. В языке просто нет инструментов, чтобы сообщить компилятору, как надо удалить объект. Способ только один. Удаление объектов, аллоцированных на стеке, ничем не должно идейно отличаться от удаления автоматических переменных. Поэтому и операторы delete и delete[] не принимают никаких аргументов.
Единственный вариант остается - это передавать дополнительные параметры при явном вызове деструктора. Однако кейсы применимости явного вызова деструктора и так сильно ограничены. Добавлять в стандарт перегрузку деструкторов, чтобы на этом строилась какая-то логика - излишне. И если вам уж захотелось построить какую-то логику на удалении, то можно ее вынести в статический метод destroy.
Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом.
Нет никакой опциональной логики при освобождении ресурсов в деструкторе. Вне зависимости от типа объекта и его ссылочности, данные внутри него выглядят одинаково. А значит и деструктор должен делать свою работу единообразно.
Не то, чтобы сильно полезный пост. У новичков иногда возникают такие вопросы. Но в принципе иногда нужно задумываться над такими, казалось бы, привычными вещами, чтобы глубже понимать инструменты, с которыми мы работаем.
Have a deeper understanding. Stay cool.
#memory #cppcore
❤27👍15🔥7😁4⚡1
Ответ на квиз
#новичкам
В этом коде:
И получается, что и исключение есть, и память освобождена. Таким образом new обеспечивает базовую гарантию исключений, чтобы программа осталась в согласованном состоянии и отсутствовали утечки памяти.
Вообще, это даже интересная техника. Ловить исключение, заметать следы преступления и пробросить исключение дальше. Может помочь избежать утечек памяти в конструкторах ваших классов, которые сами внутри выделяют память.
Спасибо, @PyXiion за предоставленную информацию)
Don't let your memory leak. Stay cool.
#cppcore #memory
#новичкам
В этом коде:
class Bar {
public:
Bar() {
throw std::runtime_error("Error");
}
};
int main() {
Bar* bar = nullptr;
try {
bar = new Bar();
} catch(...) {
std::cout << "Houston, we have a problem" << std::endl;
}
}
Не будет утечки памяти. Стандарт нам это гарантирует при использовании new expression.
Дело вот в чем. В этом посте мы поговорили о том, что есть 3 вида new:
👉🏿 operator new, который выделяет память.
👉🏿 placement new, который вызывает конструктор на заданной памяти.
👉🏿 new expression, который в начале выделяет память через operator new, а потом конструирует объект на этой памяти через placement new. Это именно то, что используется в коде выше.
Так вот, new expression заботится о своих пользователях и оборачивает в try-catch вызов конструктора объекта. В catch оно освобождает память и пробрасывает исключение наружу:
Bar* bar = new Bar();
// инструкция выше эквивалентнас следующему коду:
Bar* bar;
void* tmp = operator new(sizeof(Bar));
try {
new(tmp) Bar(); // Placement new
bar = (Bar*)tmp; // The pointer is assigned only if the ctor succeeds
}
catch (...) {
operator delete(tmp); // Deallocate the memory
throw; // Re-throw the exception
}И получается, что и исключение есть, и память освобождена. Таким образом new обеспечивает базовую гарантию исключений, чтобы программа осталась в согласованном состоянии и отсутствовали утечки памяти.
Вообще, это даже интересная техника. Ловить исключение, заметать следы преступления и пробросить исключение дальше. Может помочь избежать утечек памяти в конструкторах ваших классов, которые сами внутри выделяют память.
Спасибо, @PyXiion за предоставленную информацию)
Don't let your memory leak. Stay cool.
#cppcore #memory
👍41❤11🔥11
Volatile
#опытным
Ключевое слово, которое не embedded С++ разработчик вряд ли когда-нибудь встречал в код. Сегодня мы поговорим, для чего оно используется.
Предположим, что у нас есть переменная keyboard_press, память под которую замаплена на память устройства ввода-вывода. Когда нажимается кнопка клавиатуры, изменяется переменная keyboard_press. Оставим сам маппинг за скобками и попробуем написать какую-то детсадовскую логику с переменной keyboard_press:
Что в ассемблере?
А где цикл? А где инкремент count_test?
На самом деле код собран с -О3 и компилятор просто выкинул цикл. Он не видит, что в данном коде где-то еще изменяется keyboard_press, поэтому разумно полагает, что мы написали бесконечный цикл без сайдэффектов, который вообще-то ub.
Но keyboard_press может изменяться, просто это никак не понятно по коду программы.
Теоретически компилятор мог бы увидеть, что мы замапили устройство ввода-вывода на эту переменную. А может и не увидеть. Если маппинг происходит в другой единице трансляции, то точно не увидит. Компилятор технически не может знать всего, что творится в коде. Он оптимизирует какой-то локальный участок кода на основе своих эвристик, которые просто не могут учитывать весь код программы.
Однако компилятор точно видит тип переменной. И на него мы можем повлиять. Вот чтобы отучить компилятор от таких фокусов, нужно пометить keyboard_press ключевым словом volatile.
Теперь ассемблер выглядит так:
Все, что делает volatile - все операции над переменной становятся видимыми спецэффектами и не могут быть оптимизированы компилятором. Ну и еще операции над volitile переменными не могут переупорядочиваться с другими видимыми спецэффектами в порядке кода программы.
Говорится ли здесь что-нибудь о потоках? Нет! Здесь говорится только об оптимизациях компилятора.
Поэтому использовать volatile можно только для обработки сигналов(хэндлер которых вызывается в том же прерванном потоке), либо в тех местах, где вы работаете с переменной строго в одном потоке.
Доступ к volatile переменным не атомарный + с их помощью нельзя делать синхронизацию неатомарных переменных между потоками, так как volitile не подразумевает барьеров памяти.
Именно из-за этих ограничений volatile используется в очень узком спектре задач работы с I/O. Во всех остальных случаях в С++ используются атомики.
Don't be optimized out. Stay cool.
#cppcore #concurrency #memory
#опытным
Ключевое слово, которое не embedded С++ разработчик вряд ли когда-нибудь встречал в код. Сегодня мы поговорим, для чего оно используется.
Предположим, что у нас есть переменная keyboard_press, память под которую замаплена на память устройства ввода-вывода. Когда нажимается кнопка клавиатуры, изменяется переменная keyboard_press. Оставим сам маппинг за скобками и попробуем написать какую-то детсадовскую логику с переменной keyboard_press:
int keyboard_press = 0;
size_t count_test = 0;
void some_function() {
while(keyboard_press == 0) {
count_test++;
}
// doing stuff
}
Что в ассемблере?
some_function():
mov eax, DWORD PTR keyboard_press[rip]
test eax, eax
jne .L1
.L3: // это кстати пустой бесконечный цикл, куда нельзя попасть и откуда нельзя выбраться
jmp .L3
.L1:
ret
count_test:
.zero 8
keyboard_press:
.zero 4
А где цикл? А где инкремент count_test?
На самом деле код собран с -О3 и компилятор просто выкинул цикл. Он не видит, что в данном коде где-то еще изменяется keyboard_press, поэтому разумно полагает, что мы написали бесконечный цикл без сайдэффектов, который вообще-то ub.
Но keyboard_press может изменяться, просто это никак не понятно по коду программы.
Теоретически компилятор мог бы увидеть, что мы замапили устройство ввода-вывода на эту переменную. А может и не увидеть. Если маппинг происходит в другой единице трансляции, то точно не увидит. Компилятор технически не может знать всего, что творится в коде. Он оптимизирует какой-то локальный участок кода на основе своих эвристик, которые просто не могут учитывать весь код программы.
Однако компилятор точно видит тип переменной. И на него мы можем повлиять. Вот чтобы отучить компилятор от таких фокусов, нужно пометить keyboard_press ключевым словом volatile.
volatile int keyboard_press = 0;
size_t count_test = 0;
// same
Теперь ассемблер выглядит так:
some_function():
mov eax, DWORD PTR keyboard_press[rip]
test eax, eax
jne .L1
mov rax, QWORD PTR count_test[rip]
add rax, 1
.L3:
mov edx, DWORD PTR keyboard_press[rip]
mov rcx, rax
add rax, 1
test edx, edx
je .L3
mov QWORD PTR count_test[rip], rcx
.L1:
ret
Все, что делает volatile - все операции над переменной становятся видимыми спецэффектами и не могут быть оптимизированы компилятором. Ну и еще операции над volitile переменными не могут переупорядочиваться с другими видимыми спецэффектами в порядке кода программы.
Говорится ли здесь что-нибудь о потоках? Нет! Здесь говорится только об оптимизациях компилятора.
Поэтому использовать volatile можно только для обработки сигналов(хэндлер которых вызывается в том же прерванном потоке), либо в тех местах, где вы работаете с переменной строго в одном потоке.
Доступ к volatile переменным не атомарный + с их помощью нельзя делать синхронизацию неатомарных переменных между потоками, так как volitile не подразумевает барьеров памяти.
Именно из-за этих ограничений volatile используется в очень узком спектре задач работы с I/O. Во всех остальных случаях в С++ используются атомики.
Don't be optimized out. Stay cool.
#cppcore #concurrency #memory
2❤🔥20👍19❤9🔥5
Преимущества std::make_shared
#новичкам
Попсовая тема, которая часто спрашивается на собеседованиях. Краем касались ее в других постах, но пусть будет и отдельно, для удобной пересылки.
Коротко о том, что это за функция.
Это по сути фабрика для создания шаред поинтеров из параметров конструктора разделяемого объекта. Внутри себя она производит аллокацию памяти и вызов конструктора с помощью переданных аргументов на этой памяти.
В чем же преимущества этой функции по сравнению с явным вызовом конструктора shared_ptr?
Ну для начала, она не предполагает явного использования сырых указателей. Никакого вызова new!
Сами по себе сырые указатели - это неплохо. Просто на душе спокойнее, когда их как можно меньше в современном С++ коде.
Но если new не вызывает программист, это не значит, что функция его не вызывает. Еще как вызывает. И в том, как она это делает кроется главное преимущество std::make_shared над явным вызовом конструктора.
ОООчень упрощенно внутреннее устройство std::shared_ptr выглядит вот так:
Это два указателя: на сам объект и на контрольный блок, в котором находятся счетчики ссылок и некоторая другая информация.
Память под объекты, на которые указывают эти указатели, обычно выделяется раздельно:
Память под объект Foo выделяется при вызове new, а память под контрольный блок выделяется внутри конструктора shared_ptr.
При явном вызове конструктора невозможно по-другому: будет две аллокации.
Но когда make_shared забирает у пользователя возможность самому вызывать конструктор, у нее появляется уникальная возможность: за один раз выделить один большой кусок памяти, в который влезет и объект, и контрольный блок:
Это очень упрощенная реализация, которая показывает главный принцип: выделяется один кусок памяти под два объекта.
Отсюда повышение производительности за счет уменьшения количества аллокаций и за счет большей локальности данных и кеш-френдли структурой.
Ну и на последок.
В этой записи два раза повторяется имя класса. В коде могут быть довольно длинные названия сущностей, даже при использовании алиасов. Получается в каком-то смысле явный вызов конструктора приводит к дублированию кода.
Это не происходит с std::make_shared, потому что у нас есть волшебное слово auto:
Есть(было) и еще одно преимущество make_shared. Но его разберем уже отдельно, там ситуация непростая.
А на этом у нас все)
Make better tools. Stay cool.
#cppcore #memory
#новичкам
Попсовая тема, которая часто спрашивается на собеседованиях. Краем касались ее в других постах, но пусть будет и отдельно, для удобной пересылки.
Коротко о том, что это за функция.
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
Это по сути фабрика для создания шаред поинтеров из параметров конструктора разделяемого объекта. Внутри себя она производит аллокацию памяти и вызов конструктора с помощью переданных аргументов на этой памяти.
В чем же преимущества этой функции по сравнению с явным вызовом конструктора shared_ptr?
Ну для начала, она не предполагает явного использования сырых указателей. Никакого вызова new!
Сами по себе сырые указатели - это неплохо. Просто на душе спокойнее, когда их как можно меньше в современном С++ коде.
Но если new не вызывает программист, это не значит, что функция его не вызывает. Еще как вызывает. И в том, как она это делает кроется главное преимущество std::make_shared над явным вызовом конструктора.
ОООчень упрощенно внутреннее устройство std::shared_ptr выглядит вот так:
template <typename T>
struct shared_ptr {
T * obj_ptr;
ControlBlock * block_ptr;
}
Это два указателя: на сам объект и на контрольный блок, в котором находятся счетчики ссылок и некоторая другая информация.
Память под объекты, на которые указывают эти указатели, обычно выделяется раздельно:
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
Память под объект Foo выделяется при вызове new, а память под контрольный блок выделяется внутри конструктора shared_ptr.
При явном вызове конструктора невозможно по-другому: будет две аллокации.
Но когда make_shared забирает у пользователя возможность самому вызывать конструктор, у нее появляется уникальная возможность: за один раз выделить один большой кусок памяти, в который влезет и объект, и контрольный блок:
template <typename T, typename... Args>
shared_ptr<T> my_make_shared(Args&&... args) {
// Выделяем память для ControlBlock и объекта T одним блоком
char* memory = new char[sizeof(ControlBlock) + sizeof(T)];
// Инициализируем ControlBlock в начале памяти
ControlBlock* block = new (memory) ControlBlock();
// Инициализируем объект T после ControlBlock
T* object = new (memory + sizeof(ControlBlock)) T(std::forward<Args>(args)...); // Placement new
shared_ptr<T> ptr;
ptr.obj_ptr = object;
ptr.block_ptr = block;
return ptr;
Это очень упрощенная реализация, которая показывает главный принцип: выделяется один кусок памяти под два объекта.
Отсюда повышение производительности за счет уменьшения количества аллокаций и за счет большей локальности данных и кеш-френдли структурой.
Ну и на последок.
std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));
В этой записи два раза повторяется имя класса. В коде могут быть довольно длинные названия сущностей, даже при использовании алиасов. Получается в каком-то смысле явный вызов конструктора приводит к дублированию кода.
Это не происходит с std::make_shared, потому что у нас есть волшебное слово auto:
auto ptr = std::make_shared<Foo>(arg1, arg2);
Есть(было) и еще одно преимущество make_shared. Но его разберем уже отдельно, там ситуация непростая.
А на этом у нас все)
Make better tools. Stay cool.
#cppcore #memory
👍40❤16🔥7💯2❤🔥1
Недостатки std::make_shared. Кастомный new и delete
#новичкам
В этой небольшой серии будем рассказывать уже о различных ограничениях при работе с std::make_shared.
И начнем с непопулярного.
Внутри себя она создает объект с помощью ::new. Это значит, что если вы для своего класса переопределяете операторы работы с памятью, то make_shared не будет учитывать это поведение, а вы будете гадать, почему не видите нужных спецэффектов:
В общем, если нужный кастомный менеджент памяти, то std::make_shared - не ваш бро.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
#новичкам
В этой небольшой серии будем рассказывать уже о различных ограничениях при работе с 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
6❤28👍18😁14🔥4❤🔥1
Недостатки std::make_shared. Кастомные делитеры
#новичкам
Заходим на cppreference и видим там такие слова:
Также видим ее сигнатуру:
И понимаем, что make_shared не предоставляет возможности указывать кастомный делитер. Все аргументы функции просто перенаправляются в конструктор шареного указателя.
Опустим рассуждения об оправданных кейсах применения кастомных делитеров для шареных указателей. Можете рассказать о своих примерах из практики в комментариях.
Мы же попытаемся ответить на вопрос: "А почему нельзя указать делитер?".
Одной из особенностей make_shared является то, что она аллоцирует единый отрезок памяти и под объект, и под контрольный блок. И использует базовый оператор new для этого.
Получается и деаллокация для этих смежных частей одного отрезка памяти должна быть совместная, единая и через базовый оператор delete.
Если бы мы как-то хотели бы встроить делитер в эту схему, получился бы конфликт: делитер хочет удалить только объект, но им придется пользоваться и для освобождения памяти под контрольный блок. Это просто некорректное поведение.
Да и скорее всего, если вас устраивает помещать объект в шареный указатель вызовом дефолтного new, то устроит и использование дефолтного delete. Поэтому эта проблема тесно связана с проблемой из первой части серии, но не добавляет особых проблем сверх этого.
Customize your solutions. Stay cool.
#cppcore #cpp11 #memory
#новичкам
Заходим на 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
🔥17❤7👍6⚡2❤🔥1
Одно значимое улучшение С++17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
В каком порядке вызываются expr1, expr2, expr3, g, h и f?
Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.
Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.
Что было до С++17?
Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!
То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.
Полный бардак! И это приводило на самом деле к неприятным последствиям.
Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.
Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен.
Теперь такой код не будет проблемой:
потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
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
👍36❤15🔥10❤🔥4
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
❤20👍13🔥9🤣1
Недостатки std::make_shared. Деаллокация
#новичкам
Представляете, забыл выложить один важный пост из серии про недостатки std::make_shared. Затерялся он в пучине заметок. Исправляюсь.
В предыдущих сериях:
Кастомный new и delete
Непубличные конструкторы
Кастомные делитеры
А теперь поговорим про деаллокацию.
В этом посте мы рассказали о том, что std::make_shared выделяет один блок памяти под объект и контрольный блок. 1 аллокация вместо 2-х = большая производительность. Однако у монеты всегда две стороны.
Что происходит с объектом и памятью при работе с shared_ptr напрямую через конструктор?
Отдельно выделяется память и конструируется разделяемый объект с помощью явного вызова new, отдельно выделяется память и конструируется контрольный блок.
Деструктор разделяемого объекта и освобождение памяти для него происходит ровно в тот момент, когда счетчик сильных ссылок становится нулем. При этом контрольный блок остается живым до момента уничтожения последнего std::weak_ptr:
Мы переопределили глобальный delete, чтобы увидеть момент освобождения памяти на разных этапах.
Деструктор и delete для разделяемого объекта вызываются ровно в момент выхода объекта
Что же происходит при использовании std::make_shared? В какой момент освобождается вся выделенная память?
Только когда оба счетчика сильных и слабых ссылок будут равны нулю. То есть не осталось ни одного объекта std::shared_ptr и std::weak_ptr, которые указывают на объект. Тем не менее семантически разделяемый объект уничтожается при разрушении последней сильной ссылки:
Отметьте вызов деструктора после выхода из скоупа, но при этом память еще не освобождается. Она делает это только после reset и уничтожении последней слабой ссылки.
Учитывайте эти особенности, когда используете weak_ptr(например в кэше или списках слушателей).
Consider both sides of the coin. Stay cool.
#cpp11 #memory
#новичкам
Представляете, забыл выложить один важный пост из серии про недостатки std::make_shared. Затерялся он в пучине заметок. Исправляюсь.
В предыдущих сериях:
Кастомный new и delete
Непубличные конструкторы
Кастомные делитеры
А теперь поговорим про деаллокацию.
В этом посте мы рассказали о том, что std::make_shared выделяет один блок памяти под объект и контрольный блок. 1 аллокация вместо 2-х = большая производительность. Однако у монеты всегда две стороны.
Что происходит с объектом и памятью при работе с shared_ptr напрямую через конструктор?
Отдельно выделяется память и конструируется разделяемый объект с помощью явного вызова new, отдельно выделяется память и конструируется контрольный блок.
Деструктор разделяемого объекта и освобождение памяти для него происходит ровно в тот момент, когда счетчик сильных ссылок становится нулем. При этом контрольный блок остается живым до момента уничтожения последнего std::weak_ptr:
void operator delete(void *ptr) noexcept {
std::cout << "Global delete " << std::endl;
std::free(ptr);
}
class MyClass {
public:
~MyClass() {
std::cout << "Деструктор MyClass вызван.\n";
}
};
int main() {
std::weak_ptr<MyClass> weak;
{
std::shared_ptr<MyClass> shared(new MyClass());
weak = shared;
std::cout
<< "shared_ptr goes out of scope...\n";
} // Here shared is deleting
std::cout << "weak.expired(): " << weak.expired()
<< '\n';
weak.reset();
std::cout << "All memory has been freed!\n";
}
// OUTPUT:
// shared_ptr goes out of scope...
// Dtor MyClass called.
// Global delete
// weak.expired(): 1
// Global delete
// All memory has been freed!Мы переопределили глобальный delete, чтобы увидеть момент освобождения памяти на разных этапах.
Деструктор и delete для разделяемого объекта вызываются ровно в момент выхода объекта
shared из своего скоупа. Тем не менее weak_ptr жив, он знает, что объекта уже нет, но своим наличием продлевает время жизни контрольного блока. После ресета weak ожидаемо происходит деаллокация блока.Что же происходит при использовании std::make_shared? В какой момент освобождается вся выделенная память?
Только когда оба счетчика сильных и слабых ссылок будут равны нулю. То есть не осталось ни одного объекта std::shared_ptr и std::weak_ptr, которые указывают на объект. Тем не менее семантически разделяемый объект уничтожается при разрушении последней сильной ссылки:
void operator delete(void *ptr) noexcept {
std::cout << "Global delete " << std::endl;
std::free(ptr);
}
class MyClass {
public:
~MyClass() {
std::cout << "Деструктор MyClass вызван.\n";
}
};
int main() {
std::weak_ptr<MyClass> weak;
{
auto shared = std::make_shared<MyClass>();
weak = shared;
std::cout << "shared_ptr goes out of scope...\n";
} // shared уничтожается здесь
std::cout << "weak.expired(): " << weak.expired()
<< '\n'; // true
weak.reset();
std::cout << "All memory has been freed!\n";
}
// shared_ptr goes out of scope...
// Dtor MyClass called.
// weak.expired(): 1
// Global delete
// All memory has been freed!Отметьте вызов деструктора после выхода из скоупа, но при этом память еще не освобождается. Она делает это только после reset и уничтожении последней слабой ссылки.
Учитывайте эти особенности, когда используете weak_ptr(например в кэше или списках слушателей).
Consider both sides of the coin. Stay cool.
#cpp11 #memory
1🔥15👍10❤6❤🔥3😁1