Меня зовут Михаил Матросов, я технический менеджер в компании Align Technology. Сегодня я поработаю капитаном и немного расскажу об основах современного С++.
Работая над большим проектом, мне часто приходится смотреть чужой код и порой я вижу странное. А именно, многие даже вполне матёрые программисты на С++ могут не знать некоторых фундаментальных для языка вещей. Ну, это даже не слишком удивительно — язык такой.
Мне бы хотелось поговорить об этих основах и начну я со своей любимой темы. Будем говорить об операторах
new
и delete
. А точнее, об их отсутствии. Я расскажу, как писать надёжный и современный код на С++ без использования операторов new
и delete
. Казалось бы, тема стара как мир, Саттер и Майерс в своё время всё разложили по полочкам. Именно поэтому я не буду вдаваться в ненужные подробности, отправляя читателей к первоисточникам. Моя цель собрать информацию по вопросу в одном месте, дать соответствующие ссылки и сформулировать ёмкие рекомендации.
Статья будет интересна в первую очередь начинающим разработчикам и регулярам, но я уверен, что и опытные программисты узнают для себя что-то новое.
Изображение взято с сайта behappy.me
Основная мысль: старайтесь убрать вызовы
new
и delete
из клиентского кода. Они нужны только в исключительных случаях, и эти случаи требуют исключительного внимания. Например, это создание собственного контейнера или менеджера памяти.Мы поступим следующим образом:
- Сделаем небольшое теоретическое введение.
- Рассмотрим несколько сценариев и покажем, почему
- cледует избегать оператора
delete
; - следует избегать оператора
new
на примереmake
-функций; - следует избегать оператора
new
на других примерах.
- cледует избегать оператора
- Подытожим выводы.
Таким образом мы постепенно придём к пониманию, почему нужно отказаться от использования
new
и delete
в клиентском коде и похоронить их в недрах STL и boost.Зачем вообще нужны new
и delete
?
Операции
new
и delete
в С++ нужны для создания и удаления динамических объектов.Основная особенность динамических объектов в том, что временем их жизни нужно управлять вручную. Противоположность им с этой точки зрения составляют автоматические объекты, временем жизни которых управляет компилятор. Существуют ещё статические и thread-local объекты, но они нам в рамках данной статьи не интересны. См. storage duration.
Автоматические объекты удаляются неявно в соответствии с чёткими правилами, которые реализованы в компиляторе. Локальные переменные функции удаляются, когда поток управления покидает область видимости, в которой они объявлены. Члены класса удаляются после выполнения деструктора этого класса.
А вот для динамических объектов таких правил нет. Их нужно всегда удалять явно (явное удаление может быть скрыто в недрах утилитарных классов и функций). Вот небольшая иллюстрация для лучшего понимания:
struct A
{
std::string str; // Автоматический объект, неявно удаляется в деструкторе A (который сгенерирован
// автоматически). Сам строковый буфер - динамический объект (*), будет явно
// удалён в деструкторе std::string, который будет неявно вызван в деструкторе A.
// (*) Если только строка не слишком короткая, тогда сработает Small String Optimization и динамический
// буфер вообще не будет выделен.
};
void foo()
{
std::vector<int> v; // Автоматический объект, неявно удаляется при выходе из функции.
v.push_back(10); // Содержимое вектора - динамический объект (массив), будет явно удалён в деструкторе
// вектора, который будет неявно вызван при выходе из функции.
A a; // Автоматический объект класса А, неявно удаляется при выходе из функции.
A* pa = new A; // Указатель pa - автоматический объект, неявно удаляется при выходе из функции,
// но он указывает на динамический объект класса А, который нужно удалить в явном виде.
delete pa; // Явное удаление динамического объекта.
auto upa = // Умный указатель upa - автоматический объект, неявно удаляется при выходе из функции,
std::make_unique<A>(); // но он указывает на динамический объект класса А, который будет явно удалён
// в деструкторе умного указателя.
}
Обычно динамические объекты находятся в куче, хотя в общем случае это не так. Автоматические объекты могут находиться как на стеке, так и в куче. В примере выше автоматический объект
upa->str
находится в куче, т.к. он — часть динамического объекта *upa
. Т.е. свойства динамический/автоматический определяют время жизни, но не место жизни объекта. Свойство динамический/автоматический принадлежит именно объекту, а не типу, т.к. объекты одного и того же типа могут быть как динамическими, так и автоматическими*). В примере выше объекты
a
и *pa
оба имеют тип А, но первый является автоматическим, а второй — динамическим.Динамические объекты в С++ создаются с помощью
new
, а удаляются с помощью delete
. Вот отсюда и все проблемы: никто не говорил, что эти конструкции следует использовать напрямую! Это низкоуровневые вызовы, они как бы под капотом. И не нужно лезть под капот без необходимости.О том, зачем вообще могут понадобиться динамические объекты, мы поговорим чуть позже.
* Существуют техники, чтобы ограничить свойство динамический/автоматический на уровне типа. Например, закрытые конструкторы.
В чём проблема с new
и delete
?
С самого момента своего изобретения операторы
new
и delete
используются неоправданно часто. Самые большие проблемы относятся к оператору delete
:- Можно вообще забыть вызвать
delete
(утечка памяти, memory leak). - Можно забыть вызвать
delete
в случае исключения или досрочного возврата из функции (тоже утечка памяти). - Можно вызвать
delete
дважды (двойное удаление, double delete). - Можно вызвать не ту форму оператора:
delete
вместоdelete[]
или наоборот (неопределённое поведение, undefined behavior). - Можно использовать объект после вызова
delete
(dangling pointer).
Все эти ситуации приводят в лучшем случае к падениям программы, а в худшем к утечкам памяти и назальным демонам.
Поэтому люди давно сообразили прятать оператор
delete
в недрах контейнеров и умных указателей, убрав тем самым его из клиентского кода. Однако с оператором new
тоже связаны проблемы, но для них решения появились не сразу, и, по факту, многие разработчики до сих пор стесняются этими решениями пользоваться. Об этом мы подробнее поговорим, когда дойдём до make
-функций.Теперь перейдём к сценариям использования
new
и delete
. Напомню, что мы рассмотрим несколько сценариев и планомерно покажем, что в большинстве из них код станет лучше, если отказаться от использования new
и delete
. Начнём с простого — с динамических массивов.
Динамические массивы
Динамический массив — это массив с элементами, выделенными в динамической памяти. Он необходим в случае, если размер неизвестен на этапе компиляции, или если размер достаточно большой, и мы не хотим выделять массив на стеке, размер которого обычно сильно ограничен.
Для выделения динамических массивов С++ на низком уровне предоставляет векторную форму операторов
new
и delete
: new[]
и delete[]
. В качестве примера рассмотрим некоторую функцию, которая работает с внешним буфером:void DoWork(int* buffer, size_t bufSize);
Подобные функции часто встречаются в библиотеках с API на чистом С*. Ниже приведён пример, как может выглядеть использующий её код. Это плохой код, т.к. он в явном виде использует
delete
, а связанные с ним проблемы мы уже описали выше.void Call(size_t n)
{
int* p = new int[n];
DoWork(p, n);
delete[] p; // Плохо!
}
Тут всё просто и большинству известно, что для подобных целей в С++ следует использовать стандартный контейнер
std::vector
**. Он сам выделит память в конструкторе и освободит её в деструкторе. К тому же, он ещё может менять свой размер во время жизни, но для нас это сейчас значения не имеет. С использованием вектора код будет выглядеть так:void Call(size_t n)
{
std::vector<int> v(n); // Лучше.
DoWork(v.data(), v.size());
}
Тем самым мы решаем все проблемы, связанные с вызовом
delete
, и к тому же вместо безликой пары указатель+число, имеем явный контейнер с удобным интерфейсом.При этом никаких
new
и delete
. Не буду более подробно останавливаться на этом сценарии. По моему опыту большинство разработчиков и так знает, что следует делать в данном случае и почему.* На С++ подобный интерфейс следовало бы реализовать с использованием типа
span<int>
. Он предоставляет унифицированный STL-совместимый интерфейс для доступа к непрерывным последовательностям элементов, при этом никак не влияя на их время жизни (невладеющая семантика).** Поскольку эту статью читают программисты на С++, я почти уверен, что кто-то подумает: «Ха!
std::vector
хранит в себе целых три (!) указателя, когда старый добрый int*
— это по определению всего один указатель. Налицо перерасход памяти и нескольких машинных инструкций на их инициализацию! Это неприемлемо!». Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank. Если для вас это действительно проблема, то могу порекомендовать std::unique_ptr<int[]>
, а в будущем стандарт может подарить нам dynarray
.Динамические объекты
Динамические объекты обычно используются, когда невозможно привязать время жизни объекта к какой-то конкретной области видимости. Если это можно сделать, наверняка следует использовать автоматическую память*, (см. почему не стоит злоупотреблять динамическими объектами). Но это предмет отдельной статьи.
Когда динамический объект создан, кто-то должен его удалить, и условно типы объектов можно разделить на две группы: те, которые никак не осведомлены о процессе своего удаления, и те, которые что-то подозревают. Будем говорить, что первые имеют стандартную модель управления памятью, а вторые — нестандартную.
К типам со стандартной моделью управления памятью относятся все стандартные типы**, включая контейнеры. В самом деле, контейнер управляет памятью, которую он выделил сам. Ему нет никакого дела до того, кто его создал и как он будет удалён.
К типам с нестандартной моделью управления памятью можно отнести, например, объекты Qt. Здесь у каждого объекта есть родитель, который ответственен за его удаление. И объект об этом знает, т.к. он наследуется от класса
QObject
. Сюда же относятся типы со счётчиком ссылок, например, рассчитанные на работу с boost::intrusive_ptr
.Иными словами, тип со стандартной моделью управления памятью не предоставляет никаких дополнительных механизмов для управления своим временем жизни. Этим целиком и полностью должна заниматься пользовательская сторона. А вот тип с нестандартной моделью такие механизмы предоставляет. Например,
QObject
имеет методы setParent()
и children()
и содержит в себе список детей, а тип boost::intrusive_ptr
опирается на функции intrusive_ptr_add_ref
и intrusive_ptr_release
и содержит в себе счётчик ссылок.Если тип объекта имеет стандартную модель управления памятью, то будем для краткости говорить, что это объект со стандартным управлением памятью. Аналогично, если тип объекта имеет нестандартную модель управления памятью, то будем говорить, что это объект с нестандартным управлением памятью.
Далее рассмотрим объекты обеих моделей. Забегая вперёд, стоит сказать, что для объектов со стандартным управлением памятью однозначно не стоит использовать
new
и delete
в клиентском коде, а для объектов с нестандартным — зависит от конкретной модели.* Некоторые исключения: идиома
pimpl;
очень большой объект (например, буфер памяти).** Исключение составляет
std::locale::facet
(см. дальше).Динамические объекты со стандартным управлением памятью
Таковые чаще всего встречаются на практике. И именно их следует стараться использовать в современном С++, потому как с ними работают стандартные подходы, используемые в частности в умных указателях.
Собственно, умные указатели, да, это ответ. Именно им следует отдать управление временем жизни динамических объектов. Их в С++ целых два:
std::shared_ptr
и std::unique_ptr
. Не будем здесь выделять std::weak_ptr
, т.к. это просто помощник для std::shared_ptr
в определённых сценариях использования.Что касается
std::auto_ptr
, он был официально исключён из С++ начиная с С++17. Покойся с миром!Не буду здесь останавливаться на устройстве и использовании умных указателей, т.к. это выходит за рамки статьи. Сразу напомню, что они идут в комплекте с замечательными функциями
std::make_shared
и std::make_unique
, и именно их следует использовать для создания умных указателей.Т.е. вместо вот такого:
std::unique_ptr<Cookie> cookie(new Cookie(dough, sugar, cinnamon));
следует писать вот так:
auto cookie = std::make_unique<Cookie>(dough, sugar, cinnamon);
Преимущества
make
-функций над явным созданием умных указателей прекрасно описаны Гербом Саттером в его GotW #89 и Скоттом Майерсом в его Effective Modern C++, Item 21. Не буду повторяться, лишь приведу здесь краткий список тезисов:- Для обеих
make
-функций:
- Безопасность с точки зрения исключений.
- Нет дублирования имени типа.
- Для
std::make_shared
:
- Выигрыш в производительности, т.к. контрольный блок выделяется рядом с самим объектом, что уменьшает количество обращений к менеджеру памяти и увеличивает локальность данных. Оптимизация We Know Where You Live.
У make-функций имеется и ряд ограничений, подробно описанных в тех же источниках:
- Для обеих
make
-функций:
- Нельзя передать свой
deleter
. Это вполне логично, т.к. внутри себяmake
-функции по определению используют стандартныйnew
. - Нельзя использовать
braced initializer
, а также все прочие тонкости, связанные с perfect forwarding (см. Effective Modern C++, Item 30).
- Нельзя передать свой
- Для
std::make_shared
:
- Потенциальный перерасход памяти для больших объектов при долгоживущих слабых ссылках (
std::weak_pointer
). - Проблемы с операторами
new
иdelete
переопределёнными на уровне класса. - Потенциальное ложное разделение (false sharing) между объектом и контрольным блоком (см. вопрос на StackOverflow).
- Потенциальный перерасход памяти для больших объектов при долгоживущих слабых ссылках (
На практике указанные ограничения встречаются редко и не умаляют преимуществ. Получается, что умные указатели скрыли от нас вызов
delete
, а make
-функции скрыли от нас вызов new
. В итоге мы получили более надёжный код, в котором нет ни new
, ни delete
.Кстати, устройство
make
-функций серьёзно раскрывает в своих докладах Стефан Лававей (a.k.a. STL). Приведу здесь красноречивый слайд из его доклада Don’t Help the Compiler:Динамические объекты с нестандартным управлением памятью
Помимо стандартного подхода управления памятью через умные указатели встречаются и другие модели. Например, подсчёт количества ссылок (reference counting) и отношения родитель-ребёнок (parent to child relationship).
Далее рассмотрим несколько примеров с разной моделью памяти и попробуем сделать нашу жизнь легче за счёт избавления от
new
и delete
. Где-то у нас это получится, а где-то нет.Динамические объекты с подсчётом ссылок
Очень часто встречающийся приём, используемый во многих библиотеках. Рассмотрим в качестве примера библиотеку OpenSceneGraph. Это открытый кроссплатформенный 3D-движок, написанный на С++ и OpenGL.
Большая часть классов в нём наследуется от класса
osg::Referenced
, который осуществляет внутри себя подсчёт ссылок. Метод ref()
увеличивает счётчик, метод unref()
уменьшает счётчик и удаляет объект, когда счётчик опускается до нуля.В комплекте также идёт умный указатель
osg::ref_ptr<T>
, который вызывает метод T::ref()
для хранимого объекта в своём конструкторе и метод T::unref()
в деструкторе. Такой же подход используется в boost::intrusive_ptr
, только там вместо методов ref()
и unref()
выступают внешние функции.Рассмотрим фрагмент кода, который приведён в официальном руководстве OpenSceneGraph 3.0: Beginner's guide:
osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
// ...
osg::ref_ptr<osg::Vec3Array> normals = new osg::Vec3Array;
// ...
osg::ref_ptr<osg::Geometry> geom = new osg::Geometry;
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ...
Очень знакомые конструкции вида
osg::ref_ptr<T> p = new T
. Абсолютно аналогично тому, как функции std::make_unique
и std::make_shared
служат для создания классов std::unique_ptr
и std::shared_ptr
, мы можем написать функцию osg::make_ref
для создания класса osg::ref_ptr
. Делается это очень просто, по аналогии с функцией std::make_unique
:namespace osg
{
template<typename T, typename... Args>
osg::ref_ptr<T> make_ref(Args&&... args)
{
return new T(std::forward<Args>(args)...);
}
}
Перепишем этот фрагмент кода вооружившись нашей новой функцией:
auto vertices = osg::make_ref<osg::Vec3Array>();
// ...
auto normals = osg::make_ref<osg::Vec3Array>();
// ...
auto geom = osg::make_ref<osg::Geometry>();
geom->setVertexArray(vertices.get());
geom->setNormalArray(normals.get());
// ...
Изменения тривиальны и легко могут быть выполнены автоматически. Таким нехитрым способом мы получаем безопасность с точки зрения исключений*, отсутствие дублирования имени типа и прекрасное соответствие стандартному стилю.
Вызов
delete
уже был спрятан в методе osg::Referenced::unref()
, а теперь мы спрятали и вызов new
в функции osg::make_ref
. Так что никаких new
и delete
.* Технически, в данном фрагменте нет ситуаций небезопасных с точки зрения исключений, но в более сложных конфигурациях они могли бы быть.
Динамические объекты для немодальных диалогов в MFC
Рассмотрим пример, специфичный для библиотеки MFC. Это обёртка из классов С++ над Windows API. Она используется для упрощения разработки GUI под Windows.
Интересен приём, которым Microsoft официально рекомендует пользоваться для создания немодальных диалогов. Т.к. диалог немодальный, не совсем ясно, кто ответственен за его удаление. Предлагается ему удалять себя самому в переопределённом методе
CDialog::PostNcDestroy()
. Этот метод вызывается после обработки сообщения WM_NCDESTROY
— последнего сообщения, получаемого окном в его жизненном цикле.В примере ниже диалог создаётся по нажатию на кнопку в методе
CMainFrame::OnBnClickedCreate()
и удаляется в переопределённом методе CMyDialog::PostNcDestroy()
.void CMainFrame::OnBnClickedCreate()
{
auto* pDialog = new CMyDialog(this);
pDialog->ShowWindow(SW_SHOW);
}
class CMyDialog : public CDialog
{
public:
CMyDialog(CWnd* pParent)
{
Create(IDD_MY_DIALOG, pParent);
}
protected:
void PostNcDestroy() override
{
CDialog::PostNcDestroy();
delete this;
}
};
Здесь у нас не спрятан ни вызов
new
, ни вызов delete
. Способов выстрелить себе в ногу — масса. Помимо обычных проблем с указателями, можно забыть переопределить в своём диалоге метод PostNcDestroy()
, получим утечку памяти. При виде вызова new
, может возникнуть желание самостоятельно вызвать в определённый момент delete
, получим двойное удаление. Можно случайно создать объект диалога в автоматической памяти, снова получим двойное удаление.Попробуем спрятать вызовы к
new
и delete
внутри промежуточного класса CModelessDialog
и фабрики CreateModelessDialog
, которые будут отвечать в нашем приложении за немодальные диалоги:class CModelessDialog : public CDialog
{
public:
CModelessDialog(UINT nIDTemplate, CWnd* pParent)
{
Create(nIDTemplate, pParent);
}
protected:
void PostNcDestroy() override
{
CDialog::PostNcDestroy();
delete this;
}
};
// Фабрика для создания модальных диалогов
template<class Derived, typename... Args>
Derived* CreateModelessDialog(Args&&... args)
{
// Вместо static_assert в теле функции, можно использовать std::enable_if в её заголовке, что позволит нам использовать SFINAE.
// Но т.к. вряд ли ожидаются другие перегрузки этой функции, разумным выглядит использовать более простое и наглядное решение.
static_assert(std::is_base_of<CModelessDialog, Derived>::value,
"CreateModelessDialog should be called for descendants of CModelessDialog");
auto* pDialog = new Derived(std::forward<Args>(args)...);
pDialog->ShowWindow(SW_SHOW);
return pDialog;
}
Класс сам переопределяет метод
PostNcDestroy()
, в котором мы спрятали delete
, а для создания классов наследников используется фабрика, в которой мы спрятали new
. Создание и определение класса наследника теперь выглядит так:void CMainFrame::OnBnClickedCreate()
{
CreateModelessDialog<CMyDialog>(this);
}
class CMyDialog : public CModelessDialog
{
public:
CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) {}
};
Конечно, подобным образом мы не решили всех проблем. Например, объект всё равно можно выделить на стеке и получить двойное удаление. Запретить выделение объекта на стеке можно только путём модификации самого класса объекта, например добавлением закрытого конструктора. Но мы никак не можем этого сделать из базового класса
CModelessDialog
. Можно, конечно, вообще сокрыть класс CMyDialog
и сделать фабрику не шаблонной, а более классической, принимающей некоторый идентификатор класса. Но это всё уже выходит за рамки статьи.Так или иначе, мы упростили создание диалога из клиентского кода и написание нового класса диалога. И при этом мы убрали из клиентского кода вызовы
new
и delete
.Динамические объекты с отношением родитель-ребёнок
Встречаются достаточно часто, особенно в библиотеках для разработки GUI. В качестве примера рассмотрим Qt — хорошо известную библиотеку для разработки приложений и UI.
Большая часть классов наследуется от
QObject
. Он хранит в себе список детей и удаляет их, когда удаляется сам. Хранит указатель на родителя (может быть нулевой) и может менять родителя в процессе жизни.Отличный пример ситуации, когда избавиться от
new
и delete
так просто не получится. Библиотека проектировалась таким образом, что эти операторы можно и нужно применять во многих случаях. Я предлагал обёртку для создания объектов с ненулевым родителем, но идея не пошла (см. обсуждение в Qt mailing list).Таким образом, мне неизвестен хороший способ избавиться от new и delete в Qt.
Динамические объекты std::locale::facet
Для управления выводом данных в потоки в С++ используются объекты
std::locale
. Локаль является набором фасетов (facet), которые определяют способ вывода тех или иных данных. Фасеты имеют свой счётчик ссылок и при копировании локалей не происходит копирования фасетов, копируется лишь указатель и увеличивается счётчик ссылок.Локаль сама ответственна за удаление фасетов, когда счётчик ссылок падает до нуля, но вот создавать фасеты должен пользователь, используя оператор new (см. секцию Notes в описании конструктора
std::locale)
:std::locale default;
std::locale myLocale(default, new std::codecvt_utf8<wchar_t>);
Этот механизм был реализован ещё до внедрения стандартных умных указателей и выбивается из общих правил применения классов стандартной библиотеки.
Можно сделать простую обёртку, создающую локаль, чтобы убрать
new
из клиентского кода. Однако это достаточно известное исключение из общих правил, и может быть, нет смысла городить ради него огород.Заключение
Итак, сначала мы рассмотрели такие сценарии, как создание динамических массивов и динамических объектов со стандартным управлением памятью. Вместо
new
и delete
мы использовали стандартные контейнеры и make
-функции и получили более простой и надёжный код.Затем мы рассмотрели ряд примеров нестандартного управления памятью и увидели, как можно сделать код лучше, убрав
new
и delete
в подходящие обёртки. Мы также обнаружили пример, когда подобный подход не работает.Тем не менее, в большинстве случаев эта рекомендация даёт отличные результаты, и можно использовать её в качестве принципа по умолчанию. Теперь мы можем считать, что, если код использует
new
или delete
, это особый случай, который требует особого внимания. Если вы видите эти вызовы в клиентском коде, задумайтесь, действительно ли они оправданы.Список рекомендаций:
- Избегайте использования
new
иdelete
в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью. - Используйте стандартные контейнеры для динамических структур данных.
- Используйте
make
-функции для создания динамических объектов, когда это возможно. - Создавайте обёртки для объектов с нестандартной моделью памяти.
От автора
Лично мне приходилось сталкиваться с множеством случаев утечек памяти и падений из-за чрезмерного использования
new
и delete
. Да, большая часть такого кода была написана много лет назад, но потом с ним начинают работать молодые программисты и думают, что вот так и надо писать. Я надеюсь, данная статья подойдёт в качестве практического руководства, к которому можно отправить молодого разработчика, дабы он не сбился с пути истинного.
Чуть больше года назад я выступал с докладом на эту тему на конференции C++ Russia. После моего выступления аудитория разделилась на две группы: те, для кого всё было очевидным, и те, кто сделал для себя замечательное открытие. Полагаю, что на конференции чаще ходят уже достаточно опытные разработчики, так что, если даже среди них было множество людей, для кого эта информация была в новинку, я надеюсь, что эта статья будет полезна для сообщества.
PS В процессе обсуждения статьи, у нас с коллегами разгорелся целый спор, как правильно: «Майерс» или «Мейерс». С одной стороны, для русского слуха более привычно звучит «Мейерс», и мы сами вроде бы всегда говорили именно так. С другой стороны, на вики используется именно «Майерс». Если посмотреть локализованные книги, то там вообще кто во что горазд: к этим двум вариантам прибавляется ещё и «Мэйерс». На конференциях разные люди представляют его по-разному. В конечном итоге нам удалось выяснить, что сам себя он называет именно «Майерс», на чём и порешили.
Ссылки
- Herb Sutter, GotW #89 Solution: Smart Pointers.
- Scott Meyers, Effective Modern C++, Item 21, p. 139.
- Stephan T. Lavavej, Don’t Help the Compiler.
- Bjarne Stroustrup, The C++ Programming Language, 11.2.1, p. 281.
- Five Popular Myths about C++., Part 2
- Mikhail Matrosov, C++ without new and delete.
Комментарии (134)
RPG18
31.05.2016 13:10«Динамические объекты с отношением родитель-ребёнок» может проще назвать это паттерн компоновщик?
vershov
31.05.2016 15:03+6std::make_unique появился только в c++14,
std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.
Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.mmatrosov
01.06.2016 03:28+4std::make_unique появился только в c++14
В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.
std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()
Если быть более точным, они созданы для работы с владеющими указателями. Оператор new — один из способов такой указатель создать. Так или иначе, если в С++ есть инструмент Х, это не значит, что им нужно обязательно пользоваться. В С++ вообще такая философия, что давайте сначала накидаем инструментов, а потом посмотрим, как оно полетит. Наберёмся опыта и выработаем гайдлайны, как этим на самом деле нужно пользоваться. Вот можно ловить исключения по значению, но так делать не надо, потому как слайсинг, исключение при копировании исключения и всё такое. Можно вернуть из функции константный объект по значению, но это не имеет смысла и может скорее только навредить. Можно сделать виртуальную функцию шаблонной или добавить её аргументам значения по умолчанию, но проблем не оберёшься потом. Можно объявить неконстистентные операторы сравнения, но тогда их уже нельзя будет назвать операторами сравнения. Примеров можно много набрать. Поэтому сейчас и развиваются C++ Core Guidelines. Убрать какой-то инструментарий из языка мы не можем — обратная совместимость дело святое. Остаётся оградить разработчиков от неразумного использования существующего инструментария. Именно от неразумного. Т.к. под каждый пример при желании можно подобрать специфическую ситуацию, где следует отойти от гайдлайнов. Но это другой разговор. Это особенные случаи. И в статье я указал на факт, что в особенных ситуациях без использования new и delete нам не обойтись.
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.
Простите, это вы о чём конкретно?
Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.
Не вижу, как это связано. По-моему, это ортогональные вещи.vershov
01.06.2016 12:14В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.
Простите, но для меня, например, современный язык — это тот, который использует отрасть. Нельзя, в общем случае, просто так взять и перейти на c++14. Если компания разрабатывает продукт целиком и полностью — это одно дело. Если, как например в automotive, для создания одной машинки задействованы сотни OEM поставщиков, то данная статья даже вредна для неокрепших умов юниоров.
Простите, это вы о чём конкретно?
я про это:
Саттер и Майерс в своё время всё разложили по полочкам
Избегайте использования new и delete в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью.
std::shared_ptr<A> a; .... a = std::make_shared<A>();
Не пугайтесь и не пугайте юниоров использовать new() в коде:
std::shared_ptr<A> a; .... a.reset(new A());
mmatrosov
01.06.2016 12:57+3но для меня, например, современный язык — это тот, который использует отрасть
Вопрос терминологии. Мне казалось логичным, что «современным» называют язык, который стандартизован и поддерживается рядом мажорных компиляторов.
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области
Я не хотел называть это догмами. Без фанатизма. Да, С++ отличается широким применением и, как следствие, сильно варьируется для разных контекстов и предметных областей. Однако, можно выделить некоторые общие рекомендации, которые работают в большинстве случаев. Например, с мажорными компиляторами. Такие рекомендации вполне можно считать умолчаниями. Если в конкретном контексте или области они неприменимы — без проблем. Просто нужно понимать, почему они не применимы. Возможно, в связи со спецификой конкретного компилятора или платформы (например, не реализованы определённые оптимизации, не поддерживаются определённые инструменты). Возможно, в связи со спецификой предметной области (например, жёсткое ограничение на время отклика системы, которое требует абсолютной прозрачности потока выполнения). Но умолчания это не меняет.
Не пугайтесь и не пугайте юниоров использовать new() в коде: a.reset(new A());
В качестве исключения — до, конечно, можно. По умолчанию — нет. И в статье описано, почему.
Antervis
01.06.2016 11:44+2вместо ptr.reset(new MyClass()) можно сразу делать ptr = make_shared(), например.
SBKarr
31.05.2016 15:09+1Есть определённая беда в C++11, которая периодически заставляет делать так:
data::Value *data = new data::Value(std::move(val));
thread.perform([this, data] () -> bool {
updateFromData(*data);
delete data;
return true;
});
В С++14 беда исправлена (можно перемещать внутрь захвата лямбды), но он ещё не везде есть.mmatrosov
31.05.2016 15:53+1Да, верно, такая проблема есть. Если вы вынуждены использовать С++11, следует хотя бы избавиться от
delete
:
auto* data = new data::Value(std::move(val)); thread.perform([this, data] () -> bool { std::unique_ptr<data::Value> dataPtr(data); updateFromData(*data); return true; });
Другой вариант — использоватьshared_ptr
. Во многих случаях вполне себе вариант. Время работы нити обычно несравнимо больше времени копированияshared_ptr
.
В С++14, действительно, можно использовать init capture. Оставлю здесь пример для полноты:
auto data = std::make_unique<data::Value>(std::move(val)); thread.perform([this, data = std::move(data)]() -> bool { updateFromData(*data); return true; });
Antervis
01.06.2016 12:22+1вот только из-за того, что в лямбде захвачен некопируемый объект, сама лямбда тоже становится некопируемой. И в std::function, например, её уже не засунешь.
mmatrosov
01.06.2016 12:30+1Хорошее замечание, спасибо. Такая проблема есть, универсального решения нет. Больше информации по ссылкам:
http://stackoverflow.com/q/25330716/261217
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4159.pdf
zim32
31.05.2016 15:32+1А есть под с++ какой-нибудь DI Container, с подсчетом ссылок, скоупами и т.д?, который мог бы:
1) давать нам объекты без оператора new
2) следить за зависимостями у выполнять роль некоего GC
3) содержать ральзичные стратегии выделения памятиmbait
31.05.2016 17:39+3Зачем? Вы поличите в итоге Java, на котором ещё и писать неприятно. С хорошим дизайном первые два пункта просто не нужны. Последний пункт частично реализован в STL (см. параметр
allocator
у контейнеров).Miraage
01.06.2016 09:42Не могли бы Вы объяснить человекому, далекому от C++, какой такой хороший дизайн позволяет отказаться от первых двух пунктов — читай, DI?
mbait
01.06.2016 09:58+4- Шаблоны и статический полиморфизм (policy-based design).
- RAII.
Всё это отлично описано в книге Modern C++ Design. Да, реализации GC тоже есть, но нужны они в 0.01% случаев.
mayorovp
01.06.2016 13:54+1Ни одна из этих трех задач не является чем-то, чем должен заниматься DI Container
bitver
31.05.2016 15:33+2После появления стандартных умных указателей с большой опаской юзаю new и delete и трудно понимаю зачем в той или иной документации описано обязательное их использование. Исключение, конечно, Qt, но это относится к их QClass объектам, не к моему коду.
Так о чём это я, спасибо за статью, надеюсь вас услышат многие динозавры и перестанут портить нам нервы.semenyakinVS
31.05.2016 17:59+1Тоже люблю умные указатели, особенно с учётом того, что помимо контроля памяти к ним можно всякие полезные примочки привязывать (например, контроль потокобезопасности, либо управление памятью через нестандартные аллокаторы).
Но меня всегда смущали лишние операции при создании/удалении объектов (особенно при удалении, где есть проверка на ref_count, то есть ветвление). Умом понимаю: всё от проклятого желания переоптимизировать код. Но из-за этого часто так и тянет ручное управление использовать где стоит и где не стоит.burjui
31.05.2016 18:55+3Достаточно усвоить одно правило: заниматься оптимизацией нужно только по данным профайлера. Если что-то тормозит — запускаешь профайлер и смотришь, что именно. Для того, чтобы не тормозило, нужно разумно использовать алгоритмы. А низкоуровневые оптимизации нужно использовать только тогда, когда всё упирается в алгоритмы (впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правило).
Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода. Да и я, когда начал разрабатывать под Android, поначалу параноил по поводу тормозной Java. Потом успокоился, когда заметил, что в подавляющем большинстве случаев тормоза были вызваны небрежно написанным кодом уровня студента третьего курса.
khim
31.05.2016 21:48+5впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правило
Не знаю кто придумал это правило и чем он его мотивировал. Из моей практики: код написанный с присмотром «вполглаза» в сгенерированный ассемблер и код, написанный с подходом «если что-то тормозит — запускаешь профайлер и смотришь, что именно» до запуска профайлера отличаются по скорости раз в 10-20, после запуска и «сшибания верхов» — раза в 2-3.
Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода.
Что, как бы, логично. С++ — сложный, хитрый, опасный инструмент. Если вы не добиваетесь при его использовании максимума производительности и готовы терпеть замедление раза в 2-3, то, может быть, стоит взять что-то другое? Java, C#, Go, в конце-концов?
arteast
31.05.2016 15:33-1К сожалению, make_unique появился только в C++14, так что людям, которые еще не перешли на него, приходится использовать std::unique_ptr ptr = new T(...);
mmatrosov
31.05.2016 15:40+8Не совсем понимаю, почему вы разделяете С++14 и С++11? По моему опыту, люди либо используют современные компиляторы, где уже есть (почти) полная поддержка С++14, либо застряли в прошлом веке, где и С++11 не пахнет.
Так или иначе, вполне можно без палева подпихнуть в std свою реализацию make_unique (см. http://stackoverflow.com/a/12580468/261217):
namespace std { template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } }
Когда компилятор будет наконец обновлён, её нужно будет удалить, а весь код будет работать точно так же. Когда мы в прошлой компании некоторое время застряли на Visual Studio 2010, мы ровно так и поступили.arteast
31.05.2016 16:13+2Ну это какие-то розовые очки. К примеру, вот у нас самый распоследний CentOS 7, с ним идет gcc 4.8. Поддержкивается почти весь C++11, поддержки C++14 нет вообще. Вот у нас есть Visual Studio 2013 — снова то же самое: большая часть C++11 есть (по крайней мере те части, что и хотелось бы иметь), С++14 нет совсем.
Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедер. Чем это городить, проще уж new воткнутьmmatrosov
31.05.2016 16:32Visual Studio 2013 поддерживает make_unqiue. Если говорить о Visual Studio, то Visual Studio 2015 Update 2 уже имеет вполне неплохую поддержку С++14. Думаю, имеет смысл смотреть именно на последнию версию в рамках конкретного компилятора.
> надо во все файлы включать один общий хедер
Достаточно часто такие хедеры имеются уже готовые. Какие-нибудь самые базовые, утилитарные. Если есть pch — ещё проще. Но, конечно, всё индивидуально. Только вы сами можете решить, какое решение лучше подходит в вашем случае. Со своей стороны я лишь надеюсь, что смог собрать в одном месте достаточно информации, чтобы можно было взвесить за и против.
khim
31.05.2016 17:18+2К примеру, вот у нас самый распоследний CentOS 7, с ним идет gcc 4.8.
Уже как минимум есть GCC 4.9, думаю скоро и GCC 5 будет доступен как в RHEL 7.
Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедер
Зачем? include_next никто не отменял.
В общем кто хочет работать — ищет способы, кто не хочет — ищет причину.arteast
31.05.2016 18:40Про devtoolset слышал, но не очень в курсе, как обстоят дела с установкой собранных таким образом программ на другие машины. Тащить devtoolset как пререквизит не комильфо.
Если еще учесть, что у нас помимо gcc/linux/x86 есть еще кросскомпиляция на gcc/linux/arm и компиляция msvs/windows/x86 (и куча third-party библиотек, не дающих обновиться на visual studio 2015), то апгрейд компилятора становится задачей, требующей очень серьезного подхода. А ограничения имеющихся компиляторов дают о себе знать — то в одном regex криво работает, то в другом инициализация статических локальных переменных не атомарна, и т.д.
Костыль с include_next и /FI — в принципе, решение; если понадобится, то будем использовать.khim
31.05.2016 21:54Про devtoolset слышал, но не очень в курсе, как обстоят дела с установкой собранных таким образом программ на другие машины.
Ставите, запускаете. Никаких дополнительных пакетов ставить не нужно: рантайм он использует старый (разница между, скажем, GCC 4.4 из RHEL 6 и GCC 5.3 там оформлена как статическая библиотека, вкомпилируемая в вашу программу). Тем devtoolset и отличается от обычного GCC, собранного «руками», собственно.
khim
31.05.2016 22:14+1Вот, собственно, скромный patch'ик на ~8000 строк. Выкидывает из libstdc++.so всё лишнее и превращает её в libstdc++_nonshared.a содержащую только «добавку» к системной libstdc++.so.6
А вместо symlink'а libstdc++.so в devtoolset — это linker script, автоматически вытаскиващий что возможно из libstdc++.so, а оставшееся — из libstdc++_nonshared.a… Таким образом вы можете получить все фичи C++ 14 (ну… почти: там кой-какие баги есть, так что Boost.Hana не поиспользуешь) с рантаймом от GCC 4.8 (или даже, при необходимости, GCC 4.4 — для совместимости с RHEL 6).
Думаю что и с кросскомпиляторами можно сделать то же самое — было бы желание.
vershov
31.05.2016 16:22У вас немного однобокий опыт.
В реальном мире иногда требуется иметь возможность для копиляции под разные платформы и с разными тулчейнами. Эти требования приходят со стороны заказчика: «вот вам железо, вот вам компилятор, запустите свое рефренс приложение и мы посмотрим, будем ли мы с вами заключать контракт» (утрированно).
Поэтому и сидят в CI инструментах различные задачи под несколько компиляторов и несколько платформ и никто в здравом уме не делит мир на черное (застряли в прошлом веке) и белое (современные компиляторы) с юношеским задором.
Alesh
31.05.2016 23:59std::unique_ptr ptr = new T(...);
Надеюсь тут опечатка)
make_unique можно использовать уже сейчас https://isocpp.org/files/papers/N3656.txt
grechnik
31.05.2016 16:05+3Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank.
Кстати, на Хабре была статья с основными тезисами этого доклада: https://habrahabr.ru/company/infopulse/blog/227529/.
Shamov
31.05.2016 17:17+15При программировании на С++ с использованием безопасных указателей ощущения не те…
ANtlord
31.05.2016 17:19+1Вопрос, наверное глупый, но почему вместо int[] использовать std::vector, а не std:array<int, N>?
Shamov
31.05.2016 17:45+1Основной поинт в том, что не надо использовать int[]. В этом суть. А то, что вместо него надо использовать vector, — это просто альтернатива, которая первой приходит в голову. Естественно, для массивов фиксированного размера можно использовать std::array. Можно даже отдельную статью написать о том, что для фиксированных массивов вместо vector нужно использовать std::array.
mmatrosov
31.05.2016 17:46+1Это совершенно разные контейнеры. std::vector — буфер выделяется в куче, размер неизвестен на этапе компиляции. std::array — буфер выделяется на стеке, размер должен быть известен на этапе компиляции.
Zlobober
02.06.2016 11:30Ну, справедливости ради, данные в std::array могут жить как на стеке, так и на куче (например, если std::array используется как поле в классе, экземпляр которого размещения в куче).
mmatrosov
02.06.2016 11:34Да, вы, разумеется, правы, этот момент я отметил в статье. Здесь я рассуждал так: человек задаёт вопрос, почему нельзя использовать std::array вместо std::vector => он упустил некоторые основы => делать подобные уточнения смысла нет, надо кратко отметить, в чём разница, чтобы было понятно.
mbait
31.05.2016 20:19У вас список сокращённый, в полном списке вы так же не пишете:
- Код деструктора. Если что-то нужно освобождать в деструторе, значит это что-то ранее было инициализировано, что по сути снова new/delete.
- Код копирующего конструктора. Правило трём(пяти), и опять же — если что-то нужно нетривиально копировать, значит стоит задуматься о дизайне.
SBKarr
31.05.2016 21:14+2Конструкторы и деструкторы в первую очередь нужны для работы с внешними ресурсами. Некоторые внешние ресурсы (например, файловые дескрипторы) копируются нетривиальным образом. Большинство не копируются никак. Хороший подход: заворачивать такие вещи в RAII. Вне RAII стоит избегать нетривиальных конструкторов и деструкторов. А с введением значений по умолчанию для членов класса и поддержки для них агрегатных инициализаторов (С++14) и конструкторы по умолчанию лучше оставить компилятору.
Ещё вариант, где от реализации конструкторов с деструкторами не отказаться: классы, реализующие union. Но этот инструмент очень злой и коварный, можно легко отстрелить ногу в нескольких местах по самую шею. Как правило, union это оптимизация на уровне укладки данных в памяти, что суть высшие материи, и относится скорее к чистому C.mbait
31.05.2016 21:42Если поле класса не копируется, что скорее всего и класс копироваться не должен. Исключения есть везде, это естественно.
thesame
31.05.2016 23:44-9Пусть меня заминусуют, но статья должна называться «Почему НЕ НАДО программировать на C++». ;-)
У меня есть живой проект, который начали, кажется, еще в 2002 (или 2001?) году на Qt 1.45. Я его перетаскиваю потихоньку от версии к версии, постепенно созревая, чтобы переписать его с нуля. Если бы я лез к нему в потроха с появлением каждого нового стандарта языка — то уже переписал бы минимум дважды.
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.
И с жабой (Java) та же история, и много еще с чем.
Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.Alesh
01.06.2016 00:05+4Программа на С++/Qt согласен. Но неужели программа на С++ без использования сомнительных библиотек не соберется современными компиляторами?
Chulup
01.06.2016 06:24Есть большой шанс, что нет. При портировании программы, написанной в 2003-2007 годах, мне пришлось, кроме всего прочего (Qt), править шаблоны, ибо их специализации перестали видеться при линковке.
Точно не соберётся, если поставить -Wall -Werror, как любят делать гордые опенсорсы :)
oYASo
01.06.2016 13:44+8Я тоже работал с похожими проектами, и тут у меня есть несколько замечаний к вашему комментарию.
Для начала, зачем вообще перетаскивать в него новые фишечки плюсов? Это legacy code, либо он работает, и вы его не трогаете, либо пишете новый по всем современным канонам. Делать микс — это самое большое зло.
Далее, 2002 год. Код на плюсах в 2002 году не так уж сильно отличался от кода на C. Во всяком случае, прогали в основном в стиле «С с классами». Поэтому когда я слышу «проект 2002» года — это скорее всего намешанные malloc и new, процедурный стиль, венгерская нотация, конструкции вида ** и *& и все в таком же духе.
Насчет LTS. Это не так. Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.
Самое сложное искусство на С++, к которому, как кажется мне, все и придираются — как писать средний код. Средний — в том плане, что в нем и новичок сможет довольно быстро разобраться, и гуру не будет плеваться. Современный C++ как раз движется в этом направлении, и это прекрасно. Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код, без всяких трюков и низкоуровневых хаков. При том, что в частный случаях все трюки и хаки еще по старой совместимости доступны. По-моему, это отлично.thesame
02.06.2016 03:27-1> Для начала, зачем вообще перетаскивать в него новые фишечки плюсов?
Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.
> Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код
При условии правильного проектирования даже не базовых классов, а базовых шаблонов.
Простой пример — автомобиль. 4 колеса, кузов, двигатель внутреннего сгорания… опаньки! тесла-мобиль… что делать будем?
Если мы не заложились в самом начале на существование других двигателей, кроме ДВС, то мы попали — у нас рвется шаблон. :)
Если заложились — то мы влипаем в класс engine по полной программе.
> Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.oYASo
02.06.2016 03:56+1Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.
Ну вы говорили, что вот вышел стандарт — можно лезть и переписывать. А я говорю — нафига?
Простой пример — автомобиль.
Плохой пример, из которого вообще никаких проблем не видно. Решений может быть море, начиная от шаблонной магии (мы же знаем, какие характеристики нужны нам для расчетов) до нескольких реализации ДВС, наследуемых от базового класса движка.
Конечно, нужно думать и проектировать. И это на любом языке. На С, если прогать по принципу «фигак-фигак и в продакш», происходит тоже самое — через некоторое время вообще непонятно, как это поддерживать.
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
clang?
encyclopedist
02.06.2016 04:00+1Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
Микрософт, кстати, не так давно переписал свою CRT на С++. Так что уже свершилось. (Ну и да, даже GCC уже некоторое время написан частично на C++)
khim
02.06.2016 17:04Две самые популярные операционки (iOS/MacOS и Android) так и устроены. Там даже базовая библиотека (начиная со всяких загрузчиков и прочего) на C++.
Lauren
01.06.2016 22:47+3А причём здесь С++ — это ведь Qt штампует новые версии. Был бы Qt написан на C проблемы были бы те же.
До С++11 даже проблем с дунгрейтом практически не было, если знаешь C++ ошибки быстро исправишь.
К примеру, один из моих проектов должен работать и в одной старой ОСи, в которой только только gcc 2.9*. Неудобно конечно, но код( со всеми плюшками) компилируется как с новым gcc, так и со старым.thesame
02.06.2016 03:49-2Речь идет о том, что код, написанный 15 лет назад на c++ почти наверняка не скомпилируется современными компиляторами.
В то время как чистый Си — скорее всего выдаст несколько варнингов.
зы. На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью. Этакий VendorLock. А C++ идет по этому пути очень радостно (или у меня паранойя разыгралась? :)encyclopedist
02.06.2016 04:05+3Я пользуюсь C++ проектом который начали писать в 1989. Код местами выглядит странно по сегодняшним меркам, но все компилируется. Как вам уже много раз говорили, в вашем случае проблема в Qt а не в C++. В С++ несовместимых изменений практически не было.
Antervis
02.06.2016 06:20вот как раз несовместимые изменения в основном связаны с тем, что если раньше класса thread, допустим, не было, то переменная с именем thread была полностью легальна.
mayorovp
02.06.2016 06:49+4А сейчас что, вдруг стала нелегальной?
Antervis
02.06.2016 10:05-2а сейчас в некоторых ситуациях (связанных, как раз, с нововведениями) имя переменной может восприняться компилятором как имя класса.
encyclopedist
02.06.2016 15:21+5Все стандартные классы лежат в
std::
, поэтому не могут вызвать проблем, если не делатьusing namespace std;
, что всегда считалось плохой практикой.khim
02.06.2016 17:08+2Вот это, кстати, одна из больших проблем: в
std::
они переехали в 98м году, с выходом стандарта C++98. С более ранними версиями совместимость действительно… не очень — там, в частности, все эти вещи лежали в глобальномnamespace
(потому что других не было).
Ну так и программы, написанные на C до ~85го года (до выхода проектов ANSI C стандарта) вы современными компиляторами, зачастую, собрать не сможете. В чём разница?
Antervis
02.06.2016 17:11Ну, лично я готов злоупотребить плохой практикой если увижу, что блок кода наполовину состоит из std:: (в хедер, однако, ни-ни). И рассчитывать на то, что 15 лет назад рандомный коллега не зафигачил в проект using namespace std я бы точно не стал
khim
02.06.2016 19:54+1И рассчитывать на то, что 15 лет назад рандомный коллега не зафигачил в проект using namespace std я бы точно не стал.
Ага.
А если он вот такой код зафигачит:
Пример из реального проекта, если что… теперь и C тоже не использовать? В машинных годах писать?char *safe_malloc(size) unsigned size; { char *p; extern char *malloc(); if ((p = malloc(size)) == (char *) 0) { cleanupHandler(heap_no_mem,"safe_malloc"); } return p; }
Antervis
02.06.2016 20:05-1моя изначальная мысль заключалась в том, что с годами в коде на плюсах не должно ничего ломаться…
khim
02.06.2016 21:06+1Давайте я вам процитирую вашу же «начальную мысль»:
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.
Так вот это — бред. 100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан.
Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.
Код, который написан на С или C++ аккуратно — работает отлично и через 10 лет и через 20. Код, который, я извиняюсь, написан через жопу — не работает ни там, ни там.
Так что совершенно неясно — чего вы добиваетесь переходом с С на C++.mmatrosov
02.06.2016 23:11100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан
Здесь вы расширение языка не называете изменением?
khim
03.06.2016 03:19Называю, конечно. Но вы когда-нибудь на
autoconf
-скрипты смотрели? Они бывают как позитивными (если что-то компилируется/работает, то делаем так), так и негативными (если что-то не компилируется, то делаем эдак). Любое изменение может поломать негативный тест (просто по определению), но даже позитивные могут быть сломаны если они сделаны через «одно место» (например залазят в «зарезервированные» индентификаторы типа_Bool
).
khim
02.06.2016 19:47+2Речь идет о том, что код, написанный 15 лет назад на c++ почти наверняка не скомпилируется современными компиляторами.
Вы это проверяли? Или так — вещаете о том, о чём понятия не имеете?
В то время как чистый Си — скорее всего выдаст несколько варнингов.
Я, среди прочего, гоняю на работе SPEC2000 и SPEC2006. Которые, как несложно догадаться, появились примерно как раз ~15 лет назад. Количество костылей, которые нужно пристраивать со всех сторон к тестам на C и на C++ — вполне сравнимо.
И во всех случаях это — результат использования плохого стили. Чем программа на C объявляющая внутри функцииmalloc
вместо того, чтобы заголовочный файл включить (300.twolf) или не использующая include guard'ов (253.perlbmk), в сущности, отличается от программы на C++ сusing namespace std
(252.eon)? Да ничем: во всех случаях кому-то нужно было очень сильно надо «дёшево» «заткнуть проблему» — а через 15 лет это аукается… Вряд ли можно это отнести к недостаткам языка: «свинья грязь везде найдёт»…
На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью.
А чем это плохо, собственно? Посмотрите на PHP: то, что там куча всего-всего-всего прямо сразу из коробки — всем, скорее, нравится, не нравится то, что это всё-всё-всё свалено в кучу без всякой системы… К тому же Python'у (где библиотек лишь немногим меньше, но они более-менее разумно организованы) претензий нет вообще.
FreeMind2000
01.06.2016 00:14-7То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
Но я поспорю с автором.
New/delete — это не враги, а наши очень влиятельные друзья, к помощи которых мы можем прибегать только в серьезных ситуациях. C++ без new/delete, malloc/free — это уже не C++, а хрень на постном масле. Я надеюсь, что очередной 0xXX не забудет, что C++ задумывался как язык для упрощения работы с реальным железом, так сказать высокоуровневый ассемблер… а не солянка из кучи синтаксических наворотов. Думаю, что в скором будущем 0xXX неизбежно выделится в новый язык, как в свое время C превратился в C++, ибо тем, кто глаголит на старославянском, не с руки писать один код с теми, кто не чтит мудрость предтечей :)GamePad64
01.06.2016 02:18+8То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.
0xXX неизбежно выделится в новый язык
Да выделился уже: D. Просто он почти никому не нужен.
FreeMind2000
01.06.2016 21:40Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete
— Вы это лично проверяли?
Вот c++ / ассемблерный код, который выдает C++Builder XE3 для auto_ptr.
struct MyStruct
{
int x;
int y;
};
MyStruct* p = new MyStruct;
delete p;
std::auto_ptr ap(new MyStruct);
ap.reset();
832: MyStruct* p = new MyStruct;
00423BE6 6A08 push $08
00423BE8 E80F7C0400 call $0046b7fc
00423BED 59 pop ecx
00423BEE 8945CC mov [ebp-$34],eax
833: delete p;
00423BF1 FF75CC push dword ptr [ebp-$34]
00423BF4 E8F77B0400 call $0046b7f0
00423BF9 59 pop ecx
835: std::auto_ptr ap(new MyStruct);
00423BFA 66C745E80C00 mov word ptr [ebp-$18],$000c
00423C00 6A08 push $08
00423C02 E8F57B0400 call $0046b7fc
00423C07 59 pop ecx
00423C08 50 push eax
00423C09 8D55FC lea edx,[ebp-$04]
00423C0C 52 push edx
00423C0D E83A000000 call std::auto_ptr::auto_ptr(MyStruct *)
00423C12 83C408 add esp,$08
00423C15 FF45F4 inc dword ptr [ebp-$0c]
00423C18 66C745E81800 mov word ptr [ebp-$18],$0018
836: ap.reset();
00423C1E 6A00 push $00
00423C20 8D4DFC lea ecx,[ebp-$04]
00423C23 51 push ecx
00423C24 E883000000 call std::auto_ptr::reset(MyStruct *)
00423C29 83C408 add esp,$08
Думаю разница теперь Вам очевидна?
К сожалению, unique_ptr данная версия компилятора не поддерживает, поэтому если Вы замените auto_ptr на unique_ptr и привидете ассемблерный код своего компилятора, будет интересно посмотреть.FreeMind2000
01.06.2016 21:48Хабр удалил из примера кода все знаки и содержимое > <, надеюсь сможете расставить сами.
lemelisk
01.06.2016 23:08Вы, скорей всего, при компиляции оптимизацию не включили, вот у вас ничего и не заинлайнилось.
0serg
01.06.2016 23:16Нет, я проверял, не в этом дело. MSVS 2015 с включенной оптимизацией дает более краткий код, но все равно длиннее чем new/delete
По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку вида
if (data != nullptr) delete data;
тогда как прямой вызов delete очевидно этой проверки не проводит. Это связано с желанием авторов unique_ptr правильно хэндлить всякие ситуации типа такой
mystruct *x = new mystruct; std::unique_ptr<mystruct> p(x); p.reset(x); // здесь выполняется проверка что в reset не передали тот же самый указатель
khim
02.06.2016 00:16+6По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку вида
Зачем оно там? Читаем спецификацию на operator delete: указатель на область памяти для освобождения или нулевой указатель. Если вы находите подобный код «по шаблонам», то это значит только одно: разработчики вашего компилятора малость налажали и/или немного слишком сильно перестраховались.
if (data != nullptr) delete data;
Я дополнил ваши несколько строк до полной программы:
здесь#include <memory> struct MyStruct { int x; int y; }; extern void foo(MyStruct *); void bar() { MyStruct* p = new MyStruct; delete p; } void baz() { std::unique_ptr<MyStruct> ap(new MyStruct); ap.reset(); }
0serg
02.06.2016 00:56+1Зачем оно там?
Я же написал зачем, даже пример привел. Если в reset() тупо сделать delete без проверки на указатель, то второй приведенный пример кода сломается. Менее понятно зачем схожая проверка есть в деструкторе, но похоже это дань универсальности — в unique_ptr можно, к примеру, запихать дескриптор потока с кастомным деструктором который этот поток закрывает, или свой собственный умный указатель. Есть даже целая интересная тема на тему того что гарантированное стандартом поведение (вызывать deleter для нулевого указателя или нет) отличается для unique_ptr и shared_ptr:
http://stackoverflow.com/questions/11164354/does-the-standard-behavior-for-deleters-differ-between-shared-ptr-and-unique-ptr
А так да, в реализации STL от GCC похоже аккуратно написали специализацию для «простых» случаев и это еще раз показывает всю крутизну C++. Но к сожалению не все реализации STL столь же вылизаны.
Было бы любопытно глянуть что ICC сгенерит, но лень сейчас на рабочую машину лезть и проверять )encyclopedist
02.06.2016 04:08+2но лень сейчас на рабочую машину лезть и проверять )
Используйте http://gcc.godbolt.org/ для таких случаев.
Antervis
02.06.2016 06:25+1так в итоге внутри unique_ptr есть проверка на nullptr и внутри delete есть вторая такая же проверка. Компилятор по идее должен просто выкинуть одну из них из-за её тождественности.
lemelisk
02.06.2016 15:09Да, так и будет, потому что проверка внутри
delete
не скрыта в недрах какой-нибудь вызываемой функции, а вставляется компилятором прямо по месту самогоdelete
(что логично, т.к. иначе мы не смогли бы получать преимущества от знания, что в этой точке указатель всегда ненулевой). В комментарии ниже есть искусственный пример, показывающий, что в обоих случаях код будет идентичным.
lemelisk
02.06.2016 01:20+1Зачем оно там?
В реальности, там в шаблоне конструкция вида:
для поддержки нестандартных deleter'ов (стандарт обязывает делать такую проверку и вызывать их только для ненулевых указателей). И да, в ситуациях, когда компилятор не может доказать (в примерах выше то у него всего на виду), что указатель ненулевой, проверка останется. Но и для простогоif (get() != nullptr) get_deleter()(get());
delete
компилятор прямо по месту такую проверку вставит, т.к., как вы верно указали, по стандартуdelete
от нулевого указателя абсолютно легален и должен просто не делать ничего, а вызывать, к примеру, какой-нибудь нетривиальный деструктор, передав ему нулевой указатель, не самая лучшая идея. Вот синтетический пример, в обоих случаях идет проверка.
Вообще, по логике, если мы уверены, что в этой точке указатель не нулевой, то мы можем «подсказать» это компилятору, написав какое-нибудь не делающее ничего действие с использованием разыменования этого указателя (скажем,delete &*p;
). Тогда компилятор имеет право считать, что указатель всегда ненулевой, т.к. иначе происходит UB. На практике, я чуток поигрался, и компиляторы (пока?) такие подсказки не воспринимают, нужно что-то очень явное с объектом сделать (например, вывести его на экран), чтоб такая оптимизация заработала. Возможно, у кого-нибудь получится найти рабочий способ?
lemelisk
02.06.2016 01:41// здесь выполняется проверка что в reset не передали тот же самый указатель
Я не уверен, кстати, что подобная проверка (если я верно понял то, что в ситуации равенства обоих указателей при ней ничего не происходит) не нарушает стандарт. Стандарт (C++11 20.7.1.2.5p4) говорит нам о поведенииreset()
следующее:Effects: assigns
Как видим, вышеупомянутой проверки стандарт не предусматривает, а значит в точкеp
to the stored pointer, and then if the old value of the stored pointer,old_p
, was not equal to nullptr, callsget_deleter()(old_p)
.p.reset(x);
должен быть вызван деструктор дляx
и освобождена память. Да, можно сказать, что дальше в момент вызова деструктораp
у нас возникает UB из-за повторного удаления, и поэтому мы можем делать, что захотим. Ну а вдруг такого не будет (например, ниже мы вызовемrelease()
)?0serg
02.06.2016 10:45+1Да, верно. Я невнимательно посмотрел MS-реализацию, там все реализовано по Стандарту и мой пример будет крэшиться.
mmatrosov
02.06.2016 10:23+1здесь выполняется проверка что в reset не передали тот же самый указатель
Каким образом, если сравнение идёт с нулевым указателем? Собственно, на cppreference указано:
A test for self-reset, i.e. whether ptr points to an object already managed by *this, is not performed, except where provided as a compiler extension or as a debugging assert.
0serg
02.06.2016 10:44Хм, а ведь и правда. Невнимательно посмотрел, такой вариант работать действительно в MS STL не будет
khim
02.06.2016 00:32+4Оптимизацию он включил. Он просто использует компиляторы, которые известны своей плохой оптицизацией. У нас были случаи, когда MSVC отказывался инйланить функции буквально в две машинных инструкции — что теперь, отказываться от C++ и возвращаться на C, используя макросы вместо типобезопасных обёрток? По мне — так уж лучше на
clang
перейти. Некоторые вещи в проектах на Windows можно собрать только MSVC, так как они завязаны на непереносимые расширения MSVC — ну такclang
и MSVC совместимы на уровне объектников, можно делать как Chromium: собирать всё сначалаclang
'ом, а то, что не собралось (обычно буквально пара-тройка файлов) — уже MSVC.
0serg
01.06.2016 23:20+1Вы правы что код с обертками будет будет чуть медленнее.
Но это экономия на спичках, на практике разница будет ничтожно мала
Где-то возможно востребована даже такая экономия, но ИМХО даже если Вам нужна максимальная производительность, то в 99% случаев Вы выиграете намного больше соптимизировав что-то другое.FreeMind2000
03.06.2016 00:30-2Спички бывают разные если вы сжигаете их миллионами, то стоит и на них поэкономить.
Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…
Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизации, дабы показать какая реальная разница между кодом реализации new/delete и auto_ptr. Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.
Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)
struct MyStruct
{
int x;
int y;
};
void test2_work(MyStruct* in_p, int& r)
{
if(in_p->x == in_p->y)
r = in_p->x + in_p->y;
}
void test2_new(MyStruct** in_p, int i)
{
*in_p = new MyStruct;
(*in_p)->x = i;
(*in_p)->y = i;
}
void test2_delete(MyStruct** in_p)
{
delete *in_p;
}
void test2_auto(std::auto_ptr& in_ap, int i)
{
in_ap.reset(new MyStruct);
in_ap->x = i;
in_ap->y = i;
}
void test2_awork(std::auto_ptr& in_ap, int& r)
{
if(in_ap->x == in_ap->y)
r = in_ap->x + in_ap->y;
}
void test2_reset(std::auto_ptr& in_ap)
{
in_ap.reset();
}
int _tmain(int argc, _TCHAR* argv[])
{
MyStruct* p_main;
std::auto_ptr ap_main;
int r=0;
int t1, t2;
int k = 20000000;
printf(«Start test auto!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_auto(ap_main, i);
test2_awork(ap_main, r);
test2_reset(ap_main);
}
t2 = GetTickCount();
printf(«Result auto t=%d, r=%d\r\n», t2-t1, r);
printf(«Start test new/delete!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_new(&p_main, i);
test2_work(p_main, r);
test2_delete(&p_main);
}
t2 = GetTickCount();
printf(«Result new/delete t=%d, r=%d\r\n», t2-t1, r);
printf("-------- Surprize -------\r\n");
printf(«Start test auto (2)!\r\n»);
t1 = GetTickCount();
{
std::auto_ptr as(new MyStruct);
for (int i = 0; i < k; i++)
{
as->x = i;
as->y = i;
test2_awork(as, r);
}
}
t2 = GetTickCount();
printf(«Result auto (2) t=%d, r=%d\r\n», t2-t1, r);
printf(«Start test new/delete (2)!\r\n»);
t1 = GetTickCount();
{
MyStruct* s = new MyStruct;
for (int i = 0; i < k; i++)
{
s->x = i;
s->y = i;
test2_work(s, r);
}
delete s;
}
t2 = GetTickCount();
printf(«Result new/delete (2) t=%d, r=%d Fatality!!!!!!\r\n», t2-t1, r);
getch();
return 0;
}
x-----------------------------x
x------- Результат -------x
x-----------------------------x
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!
Т.е. разница больше чем в 2 раза…
Если оставить в цикле только присвоение, то компилятору и auto_ptr становится стыдно! Да, в тесте (2) можно просто объявить локальную переменную без всяких new/delete и auto_ptr, но наша цель сравнить именно их. Будет интересно увидеть результат компиляторов «конкурентов» и unique_ptr :)
А тем кто минусует мой прошлый коммент еще раз процетирую, что там написано:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода. Тот, кто сомневается в своей способности их безопасно использовать и не видит пользы, может от них полностью отказаться. А тот кто не сомневается — настоящий Чак Норис :)
И да прибудет с нами сила…khim
03.06.2016 05:10+6Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…
Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код. Потому что на это у них «денех нет». Разные всякие рюшечки — продать можно легко. А просто генерация более качественного кода — это не «buzzword-compliant approach». Тем более что обычно удаётся за год «отвоевать» на реальных программах 5-7%. Но 5-7% один год, второй, третий… и вдруг, лет через 10 выясняется что ваш компилятор уже неконкурентоспособен — а за эти 10 компиляторы кокурентов научились, в частности, «сворачивать» C++-обёртки и… реальные программы — уже на это закладываются!
Что делать? Ну с Embarcadero всё просто: если это — не расписка в собственном бессили, тогда что это? Обратите, кстати, внимание, на то, что BCC64 основан на clang 3.1. 3.1, Карл! Компилятор, которому недавно 4 года стукнуло! А вы не… в общем не хочу материться…
Microsoft же несколько лет назад (когда clang, наконец-то, допилили) — тоже «вдруг» прозрел и ринулся в догонку. Но пока — он ещё очень и очень отстаёт от современных качественных компиляторов.
Приличные компиляторы для x86 — это GCC и Intel сегодня. Clang хорош, но пока всё-таки отстаёт.
Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.
А сколько «аперкотов и хуков» пропустит программист из-за того, что ему придётся писать более сложный код? И не захочет ли он использовать алгоритмы попроще для компенсации?
А вашу программку я запустил, да. Вначале пришлось заменить GetTickCount на gettimeofday и «вдарить компилятору по почкам» использованием__attribute__((noinline))
(а то он весь ваш «сурпрайз» умудрился выкинить из программу к чёртовой матери)
Результат:
Обратите внимание на ваш «suprise», кстати — но это так, лирика: современные процессоры очень плохо относятся к слишком коротким функциям — они им пайплайн сбивают и адрес возврата не успевает доползти до предсказателя ветвлений. На более старых процессорах может быть примерно аналогичный по размерам проигрыш в этом месте (а когда вы принудительно «вставляете компилятору палки в колёса и не даёте ему заняться оптимизациями, то да, можно и ~25-30% проигрыша получить).Start test auto! Result auto t=8178878, r=399999998 Start test new/delete! Result new/delete t=6458617, r=399999998 -------- Surprize ------- Start test auto (2)! Result auto (2) t=470250, r=399999998 Result new/delete (2) t=488211, r=399999998 Fatality!!!!!!
Главная проблема: с какого перепугу вы заменили „указатель“ на „указатель на указатель“? Конечно у вас замедление будет (если функция не слишком мала)!
Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)
«Крутизна» компилятора никак не может заменить мозги. Но если не загонять компилятор «в угол» и использоватьunique_ptr
, то результат будет уже таким:
Про отсутствие „сурпрайза“ мы уже говорили, а скорость двух версий — совпадает (в пределах погрешности измерений). Да и с чего им отличаться? В обоих случаях в цикле — 9 инструкций (правда разных):Start test auto! Result auto t=5899570, r=399999998 Start test new/delete! Result new/delete t=5768141, r=399999998 -------- Surprize ------- Start test auto (2)! Result auto (2) t=0, r=399999998 Result new/delete (2) t=0, r=399999998 Fatality!!!!!!
... Код для unique_ptr ... .L19: movl $8, %edi call operator new(unsigned long) movq %rax, %rdi movl %ebx, (%rdi) movl %ebx, 4(%rdi) movl $8, %esi addl $1, %ebx call operator delete(void*, unsigned long) cmpl $200000000, %ebx jne .L19 ... ... Код для new/delete ... .L20: movl $8, %edi call operator new(unsigned long) movl $8, %esi movl %ebx, (%rax) movl %ebx, 4(%rax) movq %rax, %rdi addl $1, %ebx call operator delete(void*, unsigned long) cmpl $200000000, %ebx jne .L20
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
Я тоже когда-то так думал. Но факты — упрямая вещь. В 2016м году — это дааааалеко не „очевидно“. Современные компиляторы очень, очень хорошо умеют управляться с обёртками. Если ваш код „с обёртками“ и код „без обёрток“ рализуют один и тот же алгоритм — то результат будет, как правило, идентичен. Если вы введёте лишние операции — ну так кто ж вам судья? В коде сunique_ptr
передавать самunique_ptr
куда-то не принято: функции, которым не нужно „хранить“ указатель должны получать простой, не „умный“ указатель, а если вы „передаёте“ им владение, то нужно передавать его (используя rvalue-ссылки).
new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода.
Кто сказал, что это „враги“? Вовсе нет. Но в современном мире их место — там же, где и место для ассемблерных вставок: иногда, очень редко и с помощью new/delete и с помощью ассемблерных вставок можно существенно ускорить код. Но… редко, очень редко.
Лет 10 назад (когда MSVC 6 был ещё вполне „в ходу“) я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…grechnik
03.06.2016 15:16Вот только не во всякой программе оптимизации кодогенерации могут дать «5-7% каждый год лет 10», а заменять пузырьковую сортировку быстрой компиляторы не умеют. Поэтому GCC и Clang… ломают код. Не, ну правда, "NULL как аргумент memcpy/memmove — это UB, поэтому если указатель засветился в вызове memcpy/memmove, мы выкинем все проверки с ним и весь код обработки NULL, а если вы делали memcpy(NULL, NULL, 0) — мы сломали весь ваш код, но вы сами виноваты" — больше похоже на троллинг, чем на приличную оптимизацию. (Разумеется, в чистом виде memcpy(NULL, NULL, 0) не встречается, но memcpy(p, q, len) без дополнительных проверок, где при нулевом len p или q могут быть нулями, — очень даже.)
khim
04.06.2016 02:30+3NULL как аргумент memcpy/memmove — это UB, поэтому если указатель засветился в вызове memcpy/memmove, мы выкинем все проверки с ним и весь код обработки NULL, а если вы делали memcpy(NULL, NULL, 0) — мы сломали весь ваш код, но вы сами виноваты
Ну всё-таки выкидываются не «все проверки», а «все проверки после вызова memmove». Также выкидываются проверкиthis
и т.д. и т.п. А чего вы хотите? C/C++ — это не Java и не C#…
Кто вообще сказал, что «будет легко»? Я не зря чуть выше писал «С++ — сложный, хитрый, опасный инструмент» — это один из довольно неприятных моментов в нём, да. Программа в которой такое встрчается была сломана с самого начала, так как никто не гарантирует того, что у васmemcpy
не читает память до проверки длины. Такие платформы реально существуют, код там примерно такой:
Про то, что UB — это инструкция для програмиста, а не для компилятора многие присали, в том числе и я, если ваша программа вызывает UB — приготовьтесь к подобным сюрпризам, да. Логика здесь та же самая, которая проверкуvoid *memcpy(void *dest, const void *src, size_t n) { size_t pre_count = n & 7U; n &= ~7U; switch (pre_count) { do { case 0: *dest++ = *src++; case 1: *dest++ = *src++; case 2: *dest++ = *src++; case 3: *dest++ = *src++; case 4: *dest++ = *src++; case 5: *dest++ = *src++; case 6: *dest++ = *src++; case 7: *dest++ = *src++; } while (n-=8); } }
a + 1 > b + 1
превращает вa > b
— но почему-то подобная оптимизация у вас нареканий не вызывает… или вызывает?
Но вообще, если вас этот так волнует (например если вы работает в окружении где NULL — валидный адрес… так встройка иногда сконфигурирована) или где memmove гарантированно не ломается (хотя кто и когда это обещал — сегодня не ломается, завтра сломается), то вы всегда можете использовать опцию -fno-delete-null-pointer-checks.grechnik
04.06.2016 15:37-2n=0 — корректное значение длины, а ваша версия memcpy на нём сломается даже при корректных значениях указателей. Попробуйте ещё раз.
Более того, указатель на конец массива — это валидный указатель, поэтому memcpy вообще не имеет права разыменовывать переданные ей указатели при n=0.
Код типа
довольно типичен. Он работает на всех платформах, он не был сломан до того, как в gcc закоммитили изменение, его не может сломать корректная реализация memcpy, он не сломан при использовании компилятора, думающего о программистах, типа VC2015.class MyContainer { // если контейнер пуст, то указатель на данные держим нулевым size_t Length = 0; int* Data = nullptr; ... void Append(const MyContainer& other) { Realloc(Length + other.Length); memcpy(Data + Length, other.Data, other.Length * sizeof(int)); Length += other.Length; } ... }
UB — это не какие-то законы природы, данные нам свыше. UB означает всего лишь «конструкции, которые стандарт пометил словами undefined behaviour». Часть из них действительно отражают то, как устроен компьютерный мир — в неинициализированной переменной может оказаться всё, что угодно, а если она размещена в регистре, то и Itanium-ный Not-a-Thing с исключением при чтении; аналогично при выходе за границы массива. Часть из них отражают законы физики Марса, и джинна выпустили из бутылки, когда фразу «это UB», после которой забыли написать «потому что на Марсе вот так» (signed overflow на системах со странными представлениями отрицательных чисел), начали интерпретировать «поэтому на Земле мы вам делать так тоже запретим» («ну если ваш код вдруг попадёт на Марс, он же уже сломан!»). А часть — просто неудачные формулировки, и интерпретировать фразу со смыслом «memcpy не обязана делать явных проверок своих аргументов на NULL» как «memcpy нельзя передавать NULL даже при копировании нуля байт» — расписка в бессилии сделать что-то приличными средствами.encyclopedist
04.06.2016 15:54+2С99 7.1.4 Use of library functions
If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, or a pointer to non-modifiable storage when the corresponding parameter is not const-qualified) or a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined.
Я не знаю, как это можно по-другому интерпретировать.
grechnik
04.06.2016 16:17-1Я же писал: это значит, что memcpy (как и прочие функции) не обязана делать явных проверок своих аргументов. Если попросить memcpy скопировать один байт из одного мусорного указателя в другой, она может упасть (в случае NULL или совсем невалидных указателей) или действительно скопировать один байт, последствия чего в случае некорректного dst-указателя непредсказуемы.
khim
04.06.2016 23:21+4У вас станная интерперетация. Очень странная. Обратите внимание на то, что эта фраза живёт не в разделе «Implementation of functions», а в разделе «Use of library functions» — то есть направлена она на программиста, который эти функции будет вызывать, а не писать.
Но чтобы уж совсем никаких разночтений не осталось в C11 описания фукнкция из<string.h>
было дополнено: Where an argument declared assize_t n
specifies the length of the array for a function,n
can have the value zero on a call to that function. Unless explicitly stated otherwise in the description of a particular function in this subclause, pointer arguments on such a call shall still have valid values, as described in 7.1.4..
Изивините — но разработчики GCC сделали то, что стандарт им явным образом разрешил — и не более того.
P.S. И, кстати, стало действительно видно, что моя реализация — неправильна. Посыпаю голову пеплом. Но то, что моя реализация некорректна не делает вашу программу правильной, увы.
FreeMind2000
04.06.2016 04:27-5x-----x
Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
x-----x
— Эхх… Зачем же на личности-то переходить? ;) Если у Вас годы практики, то у вашего покорного слуги — десятки. И лет 20 назад, когда я студентом в колледже после лаб по ассемблеру, под досом запускал этот волшебный watcom c++ мы про STL даже и не слышали c интернетом туговато было. Но за-то у нас считалась модной фишка — создать текстовый файл, забить там по памяти строчку символов и после переименовав его в xxx.com выполнить «теплую» перезагрузку дос :)
Мое " Какая наивность…", относится к попытке объявлять некоторыми представителями человечества свое субъективное мнение — общепринятым. Неужели кто-то на полном серьезе протестировал все версии всех компиляторов и сравнил их все между собой на всех возможных программах? У всех компиляторов есть свои плюсы и минусы, но безапелляционно утверждать, что у кого-то отвратительный код, а у кого-то хороший… слишком большая вольность.
Возьмем наш пример с сюрпризом.
Для объективности сравнения, я все-таки скачал последний MinGW и скомпилил в нем код, вообще без каких либо изменений. Извиняюсь, прошлый раз не выложил инклуды (это про GetTickCount)
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <conio.h>
#include Вот результат для auto_ptr vs new/delete с несколькими вариантами оптимизации gcc
g++ test1.cpp
Start test auto!
Result auto t=6266, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=938, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=359, r=39999998 Fatality!!!
g++ test1.cpp -O1
Start test auto!
Result auto t=5000, r=39999998
Start test new/delete!
Result new/delete t=4531, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=234, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=157, r=39999998 Fatality!!!
g++ test1.cpp -O2
Start test auto!
Result auto t=4563, r=39999998
Start test new/delete!
Result new/delete t=3968, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!
Напомню результат с сюрпризом C++Builder XE3 (оптимизация была включена на fasters код):
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!
Это корректное сравнение компиляторов, без ударов по почкам :)
Причем, на одном и том же компе, поэтому можно сравнить и реальные скорости итоговых бинарников.
x-------x
Выводы:
x-------x
auto_ptr Тест с созданием и удалением в цикле:
gсс 01=5000, O2=4563
cb = 5203
Если сравнивать по O1, то примерно одинаково, по O2 — gcc на 12% быстрее
x-------x
new/delete Тест с созданием и удалением в цикле:
gсс 01=4531, O2=3968
cb = 2282
Если сравнивать по O1, то bc в 2раза быстрее, по O2 — bc на 42% быстрее
Ну КАК тут можно утверждать, что в bc — код отвратительный???
И самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr
Смотрим, что с сюрпризом:
x-------x
auto_ptr Тест только с присваиванием в цикле:
gсс 01=234, O2=0
cb = 2937
Если сравнивать по O1 — то gcc в 12.5 раз быстрее, по O2 — gcc удалил(оптимизировал) весь код
x-------x
new/delete Тест только с присваиванием в цикле:
gсс 01=157, O2=0
cb = 172
Если сравнивать по O1 — то примерно одинаково, по O2 — gcc удалил(оптимизировал) весь код
Вот тут можно сказать — в bc присваивание auto_ptr в цикле выполняется чертовски долго.
Но, опять же, самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr!!! Хотя в gcc vs gcc сюрприз не такой крутой (67%) как в bc vs bc (17раз)
Общий вывод: new/delete и в bc и в gcc работают быстре чем auto_ptr.
x-------x
Идем дальше, и пробуем поменять auto_ptr на unique_ptr:
x-------x
g++ test2.cpp -std=c++14
Start test unique_ptr!
Result unique_ptr t=9875, r=39999998
Start test new/delete!
Result new/delete t=4782, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=3000, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=375, r=39999998 Fatality!!!
g++ test2.cpp -std=c++14 -O1
Start test unique_ptr!
Result unique_ptr t=4813, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=250, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=187, r=39999998 Fatality!!!
g++ test2.cpp -std=c++14 -O2
Start test unique_ptr!
Result unique_ptr t=4219, r=39999998
Start test new/delete!
Result new/delete t=4141, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!
x-------x
Выводы:
x-------x
Тест повторил несколько раз unique_ptr vs new/dellete = хоть и не намного, но new/dellete стабильно быстрее unique_ptr (в O1 разница более заметна чем в O2)
Ну и последнее, попробуем все-таки сделать компилятору gcc подсечку даже для режима O2 и изменим код так, чтоб он не смог додуматься до оптимизации цикла с сюрпризом в 0тиков и посмотрим, что в реальности кроется за этими нулями:
struct MyStruct
{
volatile int x;
volatile int y;
};
int k = 2000000000; //+00 иначе слишком шустро
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7672, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7641, r=-294967298
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7703, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7641, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298
Итого: в O2 сюрприз (т.е. только присваивание в цикле) для new/delete и unique_ptr работает одинаково. Для создания/удаления в цикле new/delete работает немного быстрее, но разница незначительна. А вот разница new/delete vs auto_ptr заметна и в gcc и в bc auto_ptr — медленнее.
Очевидно ли, что код с оберткой медленнее, чем без нее?
Можно ли утверждать, что компилятор создаст для обертки код по скорости не уступающий ее ручному развороту?
Пожалуй, ответить можно так:
— Если без оптимизации, да очевидно, код с оберткой будет медленнее.
— Если с оптимизацией, то всё зависит от компилятора и самой программы, насколько хитро организован код и сможет ли его «понять» компилятор, лучший способ узнать — это самостоятельно проверить, что-то заранее утверждать тут действительно не стоит… Доверие к компилятору конечно растет по мере успешности таких проверок, но не стоит свое доверие принимать за «общепринятый» стандарт/качество, слишком сложные критерии для объективmmatrosov
04.06.2016 22:21+3Вы меня прямо заинтриговали, я немного поигрался с вашими тестами. Во-первых, оставил только вариант с сюрпризом. Во-вторых, сделал запуски функции идентичными для голого указателя и для
unique_ptr
. Потому как вы передавалиunique_ptr
по ссылке, что в данном сценарии не подходит: функция ведь не участвует в определении времени жизни объекта, следовательно, и передавать объект нужно именно по голому указателю, в соответствии с C++ Core Guidelines. Сразу же оговорюсь, что если передавать объект именно как ссылку на умный указатель, результаты не меняются, я проверил.
В-третьих, заменил
auto_ptr
наunique_ptr
. Ну потому что ну в конце-то концов. Наконец, я добавил ещё один вариант с более сложной функцией, далее станет ясно, почему. Протестировал это дело на gcc, clang и VS. Результаты:
Simple test with unique_ptr done in 0 Simple test with new/delete done in 0 Complex test with unique_ptr done in 851 Complex test with new/delete done in 850
Simple test with unique_ptr done in 0 Simple test with new/delete done in 0 Complex test with unique_ptr done in 1057 Complex test with new/delete done in 1046
VS 2015 (количество итераций уменьшено в 4 раза):
Simple test with unique_ptr done in 4021 Simple test with new/delete done in 575 Complex test with unique_ptr done in 4347 Complex test with new/delete done in 1233
Мы видим следующее:
- gcc и clang соптимизировали простую версию "с сюрпризом" в ноль. Просто догадались, что результат зависит только от последней итерации. Весьма впечатляюще.
- gcc и clang работают для
unique_ptr
иnew/delete
на более сложной функции одинаково. - Оптимизатор VS показал себя не очень. Чтобы уложиться в лимит времени онлайн-компиляторов пришлось даже уменьшить ему число итераций в 4 раза.
Что характерно, у меня на машине локально VS показала себя более достойно (возможно, на сервере стоит не та оптимизация, не знаю, не нашёл, где посмотреть ключи). В частности, время выполнения более сложной функции для
unique_ptr
иnew/delete
отличается не сильно. Но всё жеnew/delete
выигрывает.
Теперь, мне вообще кажется, что ваш пример не слишком показателен. В цикле из всего функционала умного указателя вы по сути использовали только
operator->()
. Я в своём примере использовал толькоget()
(но, повторюсь, ваш я тоже проверил). Так вот, и то и другое — однострочные функции, объявленные в заголовочном файле. Такие вещи компиляторы научились инлайнить давным-давно (Visual C++, видимо, это исключение :) ). Я не уверен, что вы сможете придумать такой пример, на котором gcc или clang смогут сделать какую-то оптимизацию с голым указателемT*
, но при этом не смогут применить её над методами умного указателяunique_ptr<T>::get()
илиunique_ptr<T>::operator->()
.FreeMind2000
05.06.2016 18:06-2Пример был сделан с одной целью: показать что код (со включенной оптимизацией) без оберток работает быстрее чем с обертками.
По результатам для компиляторов bc и vc это оказалось именно так.
Для gcc и clang — практически разницы нет, хотя в gcc все же есть небольшой перевес в сторону более быстрого new/delete.
Спасибо за ссылку на интересный инструмент для тестов, теперь можно подвести окончательные итоги:
Было два варианта тестирования:
A) Тест создания/удаления объектов в цикле в раздельных ф-циях. Т.е. в одной ф-ции объект создается, в другой используется, в третей удаляется и все это делается через один общий обычный или умный указатель.
Почему для void test2_new(MyStruct** in_p, int i) — используется указатель на указатель? — чтобы можно было выделить внутри ф-ции память для нового объекта и ассоциировать его с общим указателем. Точно также через указатель на указатель в test2_delete освобождается память под объект на который указывает общий указатель.
Почему для void test2_auto(std::unique_ptr& in_ap, int i) — используется ссылка, дабы проверить в цикле именно скорость работы reset() и ->(), а не ф-ции get(), использование которой для передачи указателя на объект в ф-цию, делает внутреннюю работу ф-ции абсолютно не связанную с «умными» указателями.
Изменения в онлайн тесте:
Оставил int — дабы компилятор использовал туже разрядность что и на сервере
Добавил volatile — дабы компилятор имел в виду, что значения могут меняться без его ведома через аппаратные прерывания или др.потоки.
Убрал из блока измерения времени ф-цию вывода cout (оставленную вами там видимо по ошибке)
http://rextester.com/NSQ67595
VC k = 20000000
Test2 unique_ptr done in 6629
Test2 with new/delete done in 2061
http://rextester.com/GGNQL2697
CLANG k=VC*4
Test2 unique_ptr done in 2291
Test2 with new/delete done in 2290
http://rextester.com/QEABX66753
GCC k=VC*4
Test2 unique_ptr done in 2478
Test2 with new/delete done in 2340
Для теста A:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо (но CLANG быстрее GCC)
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC (в 4раза уменьшено кол-во итераций)
Б) Тест в котором указатель обычный/умный, только используется в цикле.
1. Вы повторили, но с лишними изменениями. Как я уже писал выше — использование get() полностью лишает смысла действия производимые с умным указателем внутри ф-ции, Поэтому цитирую «Теперь, мне вообще кажется, что ваш пример не слишком показателен.» становится не показательным именно из-за вашего изменения, т.к. вы вообще убрали умный указатель из ф-ции, оставив лишь для тестирования только get()
2. Ф-ция test_complex() — так же лишена смысла, т.к. мы мерим не скорость выполнения операции %, а именно доступ к членам объекта через умный/обычный указатель. Для того чтобы компилятор не оптимизировал код и вы не получали 0 и было предложено объявить члены MyStruct через volatile (тогда компилятор будет считать, что члены MyStruct могут быть изменены без его ведома и не сможет их оптимизировать).
Результаты локальных тестов я выкладывал в сообщении выше, а вот результаты online:
http://rextester.com/LRD24665
VC k = 80000000
Test3 unique_ptr done in 14452
Test3 with new/delete done in 595
http://rextester.com/JWH56302
CLANG k = VC*4
Test3 unique_ptr done in 267
Test3 with new/delete done in 269
http://rextester.com/MICE52083
GCC k = VC*4
Test3 unique_ptr done in 319
Test3 with new/delete done in 314
Для теста Б:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC ( в 4раза уменьшено кол-во итераций)
Ну и вспомним локальное сравнение auto_ptr — new/delete для BC vc GCC
Тест А:
BC
Result auto t=5203, r=39999998
Result new/delete t=2282, r=39999998
GCC -O2
Result auto t=4563, r=39999998
Result new/delete t=3968, r=39999998
BC — плохо оптимизирует auto_ptr
GCC — средне
По абсолютной скорости — BC в 2раза быстре GCC (если использовать new/delete)
Тест Б:
BC
Result auto (2) t=2937, r=39999998
Result new/delete (2) t=172, r=39999998
GCC -O2
Result auto (2) t=63, r=39999998
Result new/delete (2) t=78, r=39999998
BC — плохо оптимизирует auto_ptr
GCC — хорошо
По абсолютной скорости — а вот тут уже GCC в 2раза быстре BC (если использовать new/delete)
x-----x
В общем и целом, конечно удивляет такой провал VC. Но вывод все-таки остается тем же, доверяй, но проверяй. В BC и VC явно оптимизация не настолько крута, и поэтому для ускорения работы замена оберток на new/delete вполне может быть оправдана. Для clang и gcc, это более спорный вопрос. По вашим измененным тестам видно, что get() оптимизируется так же хорошо как reset() и ->() в моих, поэтому наверно сразу с ходу такой пример, который вы предложили привести будет сложно. Да и нужно ли? Самый лучший пример — это реальная программа для реального компилятора используемого в релизах, в которой либо можно существенно увеличить быстродействие за счет new/delete либо нельзя т.к. компилятор это сделал за вас.mmatrosov
06.06.2016 00:23В общем и целом, конечно удивляет такой провал VC.
Да, я тоже был немало удивлён и решил повторить ваши тесты у себя локально. Получил совершенно другие результаты. Тест А: и
unique_ptr
иnew/delete
дали одинаковое время работы. Тест Б:unique_ptr
проигралnew/delete
в полтора раза. Похоже, я предложил не самый объективный инструмент для замера скорости, прошу прощения. Я уже воспользовался формой обратной связи на сайте и сообщил о проблеме.
Но основной ваш вывод, остаётся в силе. Тест Б всё равно работает в полтора раза медленнее для умных указателей. Я зарепортил баг на Microsoft.Connect (но его пока не видно в системе; возможно, он на премодерации, а может у них просто всё лагает).
khim
06.06.2016 12:27+6Честно говоря меня удивляет это удивление (не знаю уж до какого уровня рефлексии можно тут дойти).
Да, если считать верхом совершенства, прости господи, watcom c++, то это может оказаться удивительным, так как в прошлом веке Borland C++ действительно ещё был «на коне», да и MSVC был неплох, да. Но с тех пор — много воды утекло. Как я уже писал: в прошлом веке я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…
Я с большим уважением отношусь к «старому» Borland'у. Borland C++ 3.1 — это вообще чудо было. Напомню что хотя Borland С++ был «базовым компилятором» для разработчиков STL (потому что остальные компиляторы тогда просто не могли все эти шаблоны разобрать вообще) — но ни о какой скорости тогда речь не шла, была задача «тупо это всё собрать хоть как-то и не упасть».
Но к 1996м году Microsoft тупо «задавил» Borland (и все остальные независимые компании: Watcom/Sybase, Zortech/Symantec, JPI/TopSpeed и прочие всякие Comeau) — мало того, что денег на разработку стало не хватать, так ещё и, в случае с Borland'ом (последний «живой» конкурент) существенную часть команды разработчиков в 1996м году сманили — но они занялись не C++, а совсем другим: считалось, что C++ «скоро уйдёт в историю» и все-все-все будут использовать managed код, который «догонит и перегонит» старичка.
Чего, разумеется, не случилось. Развитие C++ продолжилось — но так как хорошие деньги именно за скорость платили только в области HPC, то приличные компиляторы только там и остались: Intel C++, IBM XLC++, Sun/ Oracle C++ (хотя этот, в последнее время, сдал), потом GCC подтянули (примерно к середине нулевых). Напомню, что Windows на HPC никто не использует: в пресловутом Top500 ни одной Windows-системы сейчас нет, да и «в лучшие времена» их там было в районе 1% — да и, собственно, если вам уж сильно была нужна скорость — всегда можно было использовать Intel C++ (он встраивается в Visual Studio).
А дальше… дальше Apple крупно разосрался со Столлманом (после выхода в 2005м году GPLv3) и вбухал кучу денег в свой собственный компилятор, который, однако Apple не стал «держать при себе», а наоборот, всячески пропогандировал разработку «всем миром» (так что сейчас Apple уже не является даже основным контрибутором). Microsoft это слабо волновало до тех пор пока «всем миром» clang не допилили до состояния почти совместимости с MSVC (сам Apple этим не занимался, но видимо кому-то это сильно нужно было). Chromium, скажем, собрать только с помощью clang'а нельзя — но только за счёт того, что кой-какие расширения не поддерживаются, 99% кода clang собирает и это используется чтобы использовать ASAN.
В какой-то момент Microsoft понял, что стратегия «заставим всех мышей жрать кактус за неимением другой альтернативы» и «сменил пластинку». Балмера турнули, разработчики MSVC снова получили деньги под развитие C++ (думаю не стоит объяснять что замена одного человека круто развернуть компанию размера Microsoft'а не может — увольнение Баллмера было следствием, а не причиной изменения курса). Примерно с MSVC 2013 о компиляторе MSVC уже можно говорить как о «современном компиляторе C++» (то, что было до этого тянуло в лучшем случае на «муляж компилятора C++»), начиная с MSVC 2015 — его уже можно даже с другими сравнивать (хотя он и проигрывает частенько, но, в общем — движение в нужную сторону очень и очень заметно).
Всё вышеописанное — банальности. Как можно считать себя знатоком C++ и всего этого не знать — я просто не понимаю.
Я ведь когда написал «хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код» — то я ведь не издевался. Как мне казалось я просто говорил вещь, которая всем очевидна, известна и которую все учитывают при разработке — а меня вдруг обвинили в «переходе на личности».
P.S. Что касается сравнени «абсолютных скоростей new/delete» — то тут я просто не хочу затевать дискуссию. Боюсь на мат перейти. Просто хочу напомнить что скорость работы, собственно,new
/delete
от компилятора зависит мало (основное время проводится вmalloc
'е), стандартные реализации malloc'а в HPC не используются (популярны вещи типа jemalloc/TCMalloc), да и MingW разработчиков GCC мало волнует вообще (ну не используюется Windows людьми, для которых скорость работы C++ кода превращается в живые деньги — все эти Facebook'и, Google'ы и Yandex'ы используют Linux или, иногда, FreeBSD). Зачем вообще его сюда приплели — мне не очень ясно: как я уже <a href="">говорил — если вам нужен приличный компилятор под Windows — то нужно использовать clang: он пока чуть-чуть отстаёт от GCC, но зато вы всегда можете всегда использовать его только для «тяжёлого» кода, оставив тот же runtime и использовав MSVC в тех местах, где clang пока не тянет.FreeMind2000
06.06.2016 18:22-3Продолжу рефлексию и удивлюсь вашему удивлению…
С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется. Да я не эксперт в области истории развития компиляторов, и могу судить только о тех, с которыми реально сталкивался и имею опыт «боевого» использования в коммерческих продуктах (например первые BC и еще тогда MS Torbo C в свое время активно использовали для промышленных контроллеров с ОС ROM-DOS, VC6 — для мелких прикладных задач c GUI, очень плевался от него, когда перешли на BC6 это стало с точки зрения RAD разработки по сравнению с VC6 небо и земля, скорость реализации проектов ускорилась в разы, хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чем). С gcc когда-то попробовал один раз поиграться и забросил, так как никакой практической пользы в его использовании для себя не увидел…
И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный. Потому что код который он генерирует зависит от того, что написано в программе и насколько хорошо именно эту программу «понимает» компилятор.
Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет. Но а если вы про «специалиста по всем компиляторам c++», то да таковым я не являюсь, поэтому некоторые вещи меня в этом мире могут удивить :)
Про «абсолютных скоростей new/delete» скажу кратко, то, что new использует malloc знают все (а как реализовывать malloc это дело уже компилятора). Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).khim
06.06.2016 20:01+5Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет.
Причём тут профессионализм?
Давайтё я проведу «бытовую аналогию»: будет ли таксист отлично занющий где какие камеры висят и в каких местах можно «нарваться на ментов», но при этом не знающий какие есть автопроизводители в мире и чем отличаются выпускаемые ими машины считать «профессионалом»? Да, наверно. Но когда подобный «профессионал» начинает рассуждать о том как тюнить машину для раллийных гонок — то он выглядит несколько глупо.
И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный.
Свои плюсы и минусы есть, несомненно, у всех компиляторов. Скажем у GCC кодогенерация под x86 — лучше, чем у clang'а, а вот его же версия для arm'а — уже далеко не так хорошо себя ведёт. А есть компиляторы, которых «пугают» дажеinline
функции (на которых реально можно получить выигрыш заменойinline
-функции на макрос) в достаточно несложных ситуациях (к ним относятся в усновном устаревшие поделки типа Borland/Inprise/Embarcadero, Open Watcom'а и прочих — но также и MSVC).
Однако если про устаревшие компиляторы забыть, то споры об «обёртках» сегодня имеют столько же смысла, сколько споры о том, что лучше использовать — указатель или индекс (да, представьте себе, лет 20 назад подобные споры реально имели место было — потому что, удивительно, но факт: тогдашним компиляторам это не было не всё равно): все качественные современные компиляторы уже давно этим «переболели» и однострочные обёртки типаunqiue_ptr
— код не замедляют. Отсюда — и обсуждаемая статья.
А ваши рассуждения об обёртках потому и вызвали столько негатива, что выглядят они… — как у Жванецкого: «давайте спорить о вкусе устриц и кокосовых орехов с теми, кто их ел».
Если вас волнует скорость работы вашего кода — то вы, уж наверное, знаете о том какие компиляторы чего умеют и давно выкинули в помойку компиляторы, которые неспособны генерировать приличный код, если не волнует… то, собственно, кого волнует что вы думаете о том, как будет устроен инструмент, который, как оказывается, вы даже не выбирали?
хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чем
Ну если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно? И кто, соотвественно, следит за этим и знает откуда в современном мире тормоза вылезают часто, а откуда — редко?
С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется.
Такое тоже, бывает, да. Но тут, как бы, «если слаще морковки ничего не пробовал,… то и тыква — фрукт». У нас в проекте есть куча костылей для MSVC, от которого мы не можем, по некоторым причинам, пока отказаться. И выкинуть их не удаётся годами (говорят что начиная с MSVC 15 «всё будет по другому»… посмотрим — на MSVC 15 мы только-только перешли). А вот костылей для GCC и Clang'а — очень мало — и большинство из них существует только год-два пока переход на новую версию не сделает их ненужными. В частности для разработчиков GCC/Clang'а плохо «свернувшаяся» обёртка — это ошибка, которую нужно править, для разработчиков MSVC — ну я не знаю, сейчас, может ситация изменилась, но лет пять назад ничего, кроме «используйте__forceinline
» поддержка сказать не могла (хотя и это тольком не помогало, полностью «свернуть» обёртки MSVC всё равно не мог).
Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).
А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет. А вы не поверите — там бывает не только двигатель, но и трансмиссия и даже, о ужас, коробка передач. И бывает так, что неплохая, в целом, машина проигрывает в ралли из-за неудачного механизма оной коробки передач.
А как реализовывать malloc это дело уже компилятора
В том-то и дело, что нет. Windows — это единственная современная операционка, гдеmalloc
не поставляется «из коробки». На всех остальных операционках runtime (включающий, конечно, иmalloc
) — поставляется с системой. Потому ни GCC, ни Clangmalloc
а просто не имеют. Microsoft, кстати, тоже до этого наконец-то дотопал, но пока MingW этот runtime не использует (потому что, в частности, «из коробки» он доступен только в Windows 10, а пользователи ещё не все на Windows 10 перешли).
Я понятия не имеют что взяли разработчики MingW, чтобы создать нечто, что можно запускать на Windows — скорее всего какой-нибудь древний dmalloc. Неудивительно, что он отстаёт отmalloc
а C++Builder'а — всё-таки C++Builder разрабатывается не кучкой энтузиастов для запуска пары утилит во время разработки, а довольно большой компанией, которая когда-то имела приличную команду разработчиков…FreeMind2000
06.06.2016 23:02Я думаю, что раллийный «профессионал» будет выглядеть не менее глупо, если начнет искать двигатель у запорожца спереди.
x-----x
Ну если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно?
x-----x
— Предоставляю! :) Если вам очень важно, что окно с сообщением «Вы нажали не на ту кнопку!», появится на 1милисекунду позже, чем на другом компиляторе — то это ваш личный фетиш. Вы читайте внимательнее к чему относятся эти 15-20%: «для мелких прикладных задач c GUI». Для задач критичных по времени, (а на МК все такие), идет битва за каждую микросекунду и поверьте, тут уж есть, что потюнинговать, чтобы сократить время рабочего цикла, это вот как раз и есть настоящие «ралли». Причем понятиям «ошибка», «зависание»,«утечка» здесь придается не «философский» смысл — типа перезагрузил программу / ОС, и все можно жить с этим дальше, а довольно «фатальный», не закрылся вовремя клапан / не выключился движек… и все… что-то громко бабахнуло. А избежать этого помогают, только беспощадные тесты на производительность и надежность.
x-----x
«если слаще морковки ничего не пробовал,… то и тыква — фрукт».
x-----x
— Те МК, которыми мы занимаемся уже идут со встроенной ОС, поэтому выбирать тут особо не приходится. А инструменты и библиотеки под них, которые разрабатывались, отлаживались и проверялись годами никто менять на «нечто из огромного выбора», конечно не собирается.
Если инструмент проверенный, работает быстро и надежно — вы правда считаете, что его надо поменять?
x-----x
А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет.
В том-то и дело, что нет. Windows — это единственная современная операционка, где malloc не поставляется «из коробки».
x-----x
— У вас, какой-то прям «дар» невнимательности — я специально написал (по крайней мере для Windows), т.е. именно «кривая» реализация malloc/free и является проблемой new/delete GCC в Win, и это был ответ на ваше более раннее утверждение: «Просто хочу напомнить что скорость работы, собственно, new/delete от компилятора зависит мало». Поверьте, то что происходит под капотом винды, меня весьма интересует.
Думаю на этом можно завершить данную дискуссию. Подведем итоги:
Мое утверждение:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
— Неверно, ибо есть компиляторы, для которых это действительно очевидно, и есть компиляторы, для которых это не настолько очевидно, т.к. используется сильная оптимизация.
Утверждение GamePad64:
Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.
— Неверно, т.к есть компиляторы в которых это по тестам, действительно так (по крайней мере по тем конкретным тестам, которые мы тут проводили). И есть компиляторы в которых это совершенно не так (доказано теми же тестами)
Утверждение khim
Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
— Не выдерживает критики, т.к. здесь приведены тесты в которых код скомпилированный GCC выполняется в 2раза дольше кода BC (на Windows).
Остальные утверждения в частности:
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
Вроде бы никто не оспаривал.
Главный вывод — на данный момент, любое утверждение о компиляторах с++, должно содержать в себе перечисление тех компиляторов к которым оно относится. И если это утверждение пытается как-то сравнивать компиляторы между собой, необходимо приводить критерии и тесты на которых оно основано. Так как любой частный случай/тест не вписывающийся в общую картину — опровергает всё обобщенное утверждение.
Засим и откланиваюсь.
Antervis
07.06.2016 05:40+4а через пару лет, когда все компиляторы научатся генерировать для оберток столь же эффективный код, что и для new/delete, вы будете заставлять джуниоров переписывать new/delete на shared_ptr/unique_ptr?
khim
07.06.2016 06:06+4Хватит уже. Вы спорите с человеком, который на одном компиляторе запускает «пузырьковую сортировку», а на другом «сортировку слиянием» и делает из этого какие-то выводы о качестве компиляторов.
Это либо профнепригодность, либо троллинг — в любом случае обсуждать нечего.FreeMind2000
07.06.2016 13:13-2По моему, главный тролль в вашем лице как раз и обозначился.
Все тесты были приведены выше, они запускались на всех компиляторах без изменений и проверить это может любой.
lemelisk
03.06.2016 10:32Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизации
О, а я был прав. В этом примере поди снова не включили?
cdriper
01.06.2016 08:45-6Мое мнение, что история про «радиоактивный» new высосана из пальца и не нужно быть семи пядей во лбу, чтобы понять кто и зачем это сделал.
Смотрим на WinRT API, где вообще на каждый чих и пук у вас появляется новый shared_ptr (в виде ^) и очевидно, что Microsoft, было очень соблазнительно приучить всех пользовать make_shared, дабы сократить количество аллокаций из кучи в практически любой WinRT программе вдвое. Игра стоит свеч.
Вне WinRT мирочка картина получается несколько иная.
#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.
#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.
#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты. Соответственно, даже крошечного бонуса от возможности использовать auto вы тоже не получите. Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».
Короче, включайте здравый смысл и голову и не видитесь на хайп, который устраивают граждане из Microsoft.
зы. Хотя, если думать, что вся эта история в целом подтолкнет хотя бы некоторое количество ортодоксов и консерваторов (коих, к сожалению, все еще есть огромное количество) просто к использованию умных указателей, то это, несомненно, будет большая польза… Правда, мое мнение, что лучше весь этот сброд «C/C++ професионалов», которые типа знали С и выучили C++ через два новых для себя слова — class и virtual, согнать в зоопарки и показывать там на потеху публике.oYASo
01.06.2016 15:18+4О чем вы вообще говорите, если Microsoft до недавнего времени в поддержки новых стандартов C++ была в отстающих? Сейчас уже намного лучше, но все равно реализовано не все.
Версия с поддержкой shared_ptr, если я ничего не путаю, появилась в GCC версии 4.0, а это 2005 год. WinRT — это что-то около 2011 года. Связь где?
#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.
foo(std::make_shared<T1>(), std::make_shared<T2>());
#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.
Ну вот чтобы далеко не лезть — что делают не так авторы OSG из статьи выше (у них своя реализация shared_ptr)? И как вы предлагаете взамен строить сцену из unique_ptr?
#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты.
unique_ptr да, а что насчет shared_ptr?
Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».
А namespace никто не использует, да?
В «зы» вы вообще какие-то неадекватные причинно-следственные связи делаете. Умные указатели — это хорошо, но использовать их будут те, кто пришел из мира С, а они по умолчанию тупые и недостойные зваться профессионалами, потому что выучили только class и virtual, хоть и пользуются умными указателями? Что-то каша какая-то у вас.
shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами с общим доступом. Нам не нужно следить за его удалением, аккуратной передачей и прочими бестолковыми вещами. В 99% это НИКАК не влияет на производительность и потребление памяти конечного продукта. Если это удобно и эффективно, то это нужно использовать.Antervis
01.06.2016 16:04как раз таки с поддержкой времени жизни долгоживущих объектов проблем нет — их можно либо через raii создавать/удалять, либо (самый простой вариант) выделять на стеке в main. В самых неудобных случаях делаются синглтоны/мультитоны. А вот всякие нетривиальные, «недолгоживущие» и всем потребные сущности без shared_ptr тяжеловато мониторить.
oYASo
01.06.2016 18:03Ну, под долгоживущими объектами я это и имел ввиду. Мы что-то создали, обработали, в некоторый неопределенным момент времени удалили и т.д.
cdriper
01.06.2016 16:06-1WinRT — это что-то около 2011 года. Связь где?
#1. Не надо путать shared_ptr и make_shared. Мой комментарий был именно о последнем в контексте «радиоактивности» new.
#2. GCC тут вообще не при чем, изначально shared_ptr это зверь из boost.
foo(std::make_shared<T1>(), std::make_shared<T2>());
Если мы говорим о старом коде, до make_shared, то ваш пример должен выглядеть примерно так
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ), std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) ) );
Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:
std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) ); std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) ) foo(arg0, arg1);
Ой! А проблема то с безопасностью исключений как-то сама собой ушла!
что делают не так авторы OSG из статьи выше
Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?
Кстати, предлагаемый костыль make_ref ничего хорошего из себя не представляет, так как лишается главной силы make_shared — возможности экономить на аллокации памяти из кучи.
а что насчет shared_ptr?
Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.
А namespace никто не использует, да?
new T() vs make_unique<T>()
Что я еще должен использовать? Может сделать двухбуквенный using, чтобы эта конструкция выглядела не так уродливо??
Что-то каша какая-то у вас
Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.
А вот тех, кто думает, что пишет на C++, и при этом не используется умные указатели нужно гнать из профессии ссаными тряпками.
shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами
Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.oYASo
01.06.2016 18:36std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) ); std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) ) foo(arg0, arg1);
И положили переменные arg0 и arg1, быть может, в ту область видимости, где их быть не должно.
Ничего, кстати, некрасивого тут нет, просто пользуйтесь отступами и выравниванием:
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ), std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) ) );
Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?
А местами не очень понимаю, против какой именно философии вы выступаете.
Если речь о том, что лучше использовать unique_ptr, то вот пример — графический движок + граф сцены. Сюда можно поставить и другой графические движок, там плюс-минус все похоже.
Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.
Конечно, не означает. Но уродливые костыли (кому как, кстати) позволяют избавиться от ненужных аллокаций в клиентском коде.
Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.
Тут я опять же не понимаю, против какой вы философии. Если вы говорите про то, что нужно использовать new T() вместо make_shared(), то, наверное, большинство проектов от этого и правда не умрут (если одна лишняя аллокация им не страшна, что скорее всего).
Если вы говорите, что нужно оперировать везде указателями как тру программист, то я категорически не согласен. При текущем положении дел это не приносит большинству проектов каких-то существенным бонусов, кроме геморроя. Это не всегда так, но в подавляющем большинстве.
Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.
Да конечно лучше, кто же спорит-то?! Нужно использовать. Но это далеко не всегда возможно (опять же, графические движки).cdriper
01.06.2016 20:50Еще раз о моей позиции, она очень проста.
Я против цирка под названием «new есть зло вселенского масштаба», когда на самом деле надо всего лишь сказать, что иногда make_shared может принести ощутимую пользу вашему приложению.
lemelisk
02.06.2016 02:00+2Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:
Только лучше так:
Экономим на лишнем инкременте/декременте счетчика и отвязываем время жизни объектов от времени жизниfoo(std::move(arg0), std::move(arg1));
arg0
иarg1
.
Anton3
01.06.2016 16:32+2В целом, согласен, но могу дополнить по части с Qt.
Как вы правильно заметили в mailing lists, есть 3 основных варианта владения памятью в Qt:
1. Хранение в стеке (тривиально)
2. Отсутствие родителя (предполагается new и delete в пользовательском коде)
3. Есть родитель (только new)
Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:
auto mainWindow = std::make_unique();
По возможности надо размещать на стеке, но если не получается, то лучше так.
В третьем случае родитель управляет памятью, но концепцию «умных указателей» можно применить и тут:
template
using unowned_ptr = T*;
template
unowned_ptr MakeQObject(QObject& parent, Args&&… args);
template
unowned_ptr MakeQObject(QWidget& parent, Args&&… args);
То есть опять-таки, мы можем обращаться с этим unowned_ptr, как с unique_ptr, и QObject корректно удалится.
Конечно, это немного текучая абстракция, так как мы всё равно должны убедиться, что не удалим родителя раньше времени. Но хоть что-то.
Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить. Можно сделать это ошибкой, если вместо using создать специальный класс, но проще не заморачиваться на этот счёт.
Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.
На практике, есть ещё 4 случай, когда объект «подцепляют» к родителю уже после его создания, но при наличии необходимых helpers, 3 вариант будет удобнее и безопаснее.Antervis
01.06.2016 19:50А зачем? QObject'ы Qt в общем случае отличаются только тем, что удалять их правильнее через deleteLater. Ну так какие проблемы передать этот deleteLater прямо в QSharedPointer?
QSharedPointer<MyObject> obj = QSharedPointer<MyObject>(new MyObject, &QObject::deleteLater);
Anton3
01.06.2016 21:36Забыл я про deleteLater, тогда действительно потребуется QSharedPointer, std::unique_ptr уже не спасёт. Тем не менее, можно реализовать функции-обёртки MakeQObject и MakeQChild так, чтобы писать код вроде:
auto obj1 = MakeQObject<MyObject>(); auto obj2 = MakeQChild<MyObject>(parent);
Собственно, один из вопросов, поднятых в статье, — можно ли все выделения памяти в куче записывать в виде:
auto obj = make<MyObject>(arguments);
Где make — подходящая функция создания умного указателя. Ответ — да, можно, причём такой код будет безопасным и унифицированным.
Такие выделения памяти также отлично сочетаются с рекомендациями Herb Sutter.Antervis
02.06.2016 06:28только не надо именовать функции с большой буквы, пожалуйста… )
Что до deleteLater — его можно и в стандартные смартпоинтеры сунуть. Через лямбду в deleter'е
mmatrosov
01.06.2016 23:00Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:
Согласен, выглядит разумно.
Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить.
Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.
Тут вы немного перепутали. В C++ Core Guidelines и в GSL реализован обратный вариант — владеющий указатель owner<T>. Сырой указатель T* считается невладеющим. Об этом Саттер и Страуструп как раз и рассказывали в своих выступлениях (кажется, на GoingNative 2015, могу поискать, если нужно).
По поводу
MakeQObject
. Там фундаментальная проблема в том, что в Qt, как я понимаю, есть мода разделять создание объектов и их прицепление к родителю. Т.к. исключений там нет, это считается безопасным. Насколько такая аргументация убедительно — сказать не могу, опыт работы с Qt у меня небольшой.
В любом случае, я всячески приветствую предложить ваши идеи в тот же самый список рассылки :)
Anton3
02.06.2016 01:47Каюсь, перепутал.
Про проблему, можно рассмотреть 2 случая:
- Объект почти сразу же присоединяется к родителю. Можно переписать через третий вариант, у наследников QObject обычно последним параметром можно передать родителя.
- Объект проделывает долгий путь до прикрепления к родителю. Вначале создаём через make_unique (или через QSharedPointer, как подсказывают), потом отбираем у unique_ptr владение и сразу же прикрепляем к родителю. Этот момент можно вынести в отдельную функцию.
Если я всё правильно понимаю, проблема решается в рамках всё тех же make-функций. Я не думаю, что второй случай очень частый, но опыт работы с Qt у меня тоже небольшой. Но идею в список рассылки попробую закинуть :)
Antervis
02.06.2016 06:32не совсем. Иногда у родителя есть несколько способов владения потомком, в зависимости от того, какого формата отображения мы хотим добиться. Я, например, не видел, чтобы виджеты создавались и передавались куда-то настолько далеко, чтобы оборачивание в смартпоинтер имело смысл
maksqwe
Еще есть очень полезная штука как placament new:
void* operator new (std::size_t size, void* ptr) noexcept;
Который размещает данный объект в память указанную в «ptr» и вызывает его конструктор.
Достаточная простая и удобная оптимизация или просто стратегия хранения сложных объектов которые нуждаются в вызове конструкторов.
burjui
Я бы не рекомендовал использовать placement new без крайней необходимости (реализация GC, пулов памяти и прочих "хакерских штучек"): затуманивает смысл кода и представляет собой отличный экземпляр грабель с концом ручки как раз на уровне паха, т.к. деструктор придётся вызывать вручную, а это легко забыть сделать. К тому же, нужно самому следить, чтобы размер памяти был достаточен для размещения объекта, а не то конструктор инициализирует соседний кусок памяти.
grechnik
grechnik
Тьфу, сконцентировался на deleter и забыл собственно placement new:
khim
Тут у вас три раза употребляется
MyClass
. Что вы предлагаете с этим кодом делать? Копировать? Но тогда вы легко можете изменить только одно упоминание, оставив остальные два неизменными. И это будет работать или падать в зависимости от состояния звёзд.Чтобы это работало надёжно — это всё нужно завернуть в какую-нибудь конструкцию, параметризованною типом
MyClass
. Про что, собственно, и статья :-)grechnik
Я писал конкретно про placement new и автоматический вызов деструктора. Это всё-таки пример, заворачивание в какую-нибудь параметризованную конструкцию дало бы лишь на одну конструкцию, в которую нужно вникать при чтении, больше.
khim
Ну это понятно. Я думаю во всём, что описано в статье рулит rule of three. Ваш placement new — не исключение :-)
alexeibs
Очевидно, что это можно оформить в виде шаблонного класса с интерфейсом подобным смарт-пойнтерам — какой-нибудь StackObject. Я у себя в проекте так и сделал. Помимо экономии на аллокациях памяти, объект можно создавать или не создавать по условию и не заботиться потом об удалении.
alexeibs
Собственно, вот моя поделка:stack_object.h