Название: [РЕШЕНО] Перегрузка операторов: управление ресурсами (памятью) Отправлено: schmidt от Май 10, 2013, 11:57 Добрый день, уважаемые,
Читаю "Эффективное использование С++" Майерса, серьёзный интерес вызвали вопросы корректного использования памяти, предотвращения утечек. Хочу понять, как ведут себя объекты, создаваемые внутри перегруженных операторов сложения/умножения и возвращаемые как результат, например в таком случае: Код: class Matrix { Я специально возвращаю объект по ссылке, чтобы избежать вызова копирования. Вопрос у меня только один: будет ли при такой реализации операторов корректно (без утечек) работать следующий код: Код: Matrix m1(15,15); Иными словами, при удалении локального объекта m3 будет ли освобождена память, выделенная в операторе сложения? Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: alex312 от Май 10, 2013, 12:33 Во-первых, хотелось бы видеть код функции
Код . Во-вторых, при выходе из блока, вызываются деструкторы локальных переменных. А уж чего вы там в деструкторе наосвобождаете, то и освободится. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: schmidt от Май 10, 2013, 12:37 Код: //----------------------------------------------------------------------------- при выходе из блока, вызываются деструкторы локальных переменных. А уж чего вы там в деструкторе наосвобождаете, то и освободится. То есть, если я правильно понимаю, все выглядит так: 1. В addMatrix() выделяется память под матрицу, возвращается указатель на нее 2. Из operator+ возвращается ссылка на эту самую матрицу в куче (без какого либо копирования) 3. В вызывающем коде Код: Matrix m3 = m1 + m2; m3 является ссылкой на локальный объект и получает значение, возвращенное из operator+. 4. После окончания работы вызывается деструктор для локального объекта m3 и память из кучи корректно освобождается. Или нет? Может должно быть так? Код: Matrix & m3 = m1 + m2; Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 10, 2013, 13:28 Это не очень эффективный код.
Во-первых у вас при операциях над матрицами создаются временные объекты.. Для больших матриц и длинных выражениях, например: m1 + m2 + m3 - m4 +.. это будет очень медленно работать и отъедать много памяти.. Я как то уже писал о том, как эту проблему можно решить с помощью "шаблонов выражений". http://www.prog.org.ru/topic_21540_0.html (http://www.prog.org.ru/topic_21540_0.html) Но лучше ещё подсмотреть, как это всё реализовано в boost'е. Во-вторых, всё же лучше бинарные операторы (такие как +, -) делать не членами класса, т.е. Код
Но это лишь, как рекомендуемая форма.. И ещё, здесь уместно вспомнить о новых возможностях c++11, а именно о перемещающем конструкторе (move constructor) и перемещающем операторе присваивания (move assignment operator). Для таких объектов, как матрицы, они могут быть очень полезны. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: VPS от Май 10, 2013, 13:55 3. В вызывающем коде Код: Matrix m3 = m1 + m2; m3 является ссылкой на локальный объект и получает значение, возвращенное из operator+. 4. После окончания работы вызывается деструктор для локального объекта m3 и память из кучи корректно освобождается. Или нет? Может должно быть так? Код: Matrix & m3 = m1 + m2; Если я правильно понимаю, то у Вас будет утечка памяти, т.к: 1. в случае 3 у Вас вызывается копирующий конструктор. И соответственно память выделенная в через new не будет освобождена. 2. в случае 4 - это ссылка на объект расположенный в динамической памяти, который тоже надо зачищать руками... Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 10, 2013, 14:02 3. В вызывающем коде Код: Matrix m3 = m1 + m2; m3 является ссылкой на локальный объект и получает значение, возвращенное из operator+. 4. После окончания работы вызывается деструктор для локального объекта m3 и память из кучи корректно освобождается. Или нет? Может должно быть так? Код: Matrix & m3 = m1 + m2; Если я правильно понимаю, то у Вас будет утечка памяти, т.к: 1. в случае 3 у Вас вызывается копирующий конструктор. И соответственно память выделенная в через new не будет освобождена. 2. в случае 4 - это ссылка на объект расположенный в динамической памяти, который тоже надо зачищать руками... Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: VPS от Май 10, 2013, 14:08 Я о том и говорю, что никто не удаляет временный объект... ;)
П.С.: в общем лучше использовать новые возможности c++11, как Вы m_ax и говорили, ну или вместо new использовать локальный (временный) объект в стеке. При приеме его по ссылке он будет жить дальше... Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: schmidt от Май 10, 2013, 22:20 Код: ArrayMatrix m4 = m1 + m2; При таком коде в случае с m4 происходит копирование (адрес объекта другой), а ref_m5 принимает адрес объекта в динамической куче. Если я правильно понимаю, корректно и без утечек отработает только объект ref_m5, т.к. в нем записан адрес той самой матрицы в куче, созданной в operator+. Деструктор ref_m5 подчистит всё в куче. Выходит, что клиентский код отработает без утечек только если будет всегда принимать результат операторов по ссылке. Если же программист напишет "Matrix m1 = ..." вместо "const Matrix & m1 = ...", то его программа будет вознаграждена утечками от каждого вызова оператора сложения. Это же абсолютно непригодный вариант :) Но с другой стороны двоичные операторы так или иначе требуют выделения памяти динамически - от этого никуда не деться, если мы хотим оставить исходные операнды без изменений. В интернете сплошь и рядом видел примеры таких реализаций операторов: Код: const Object& Object::operator+(const Object& other) { Но во-первых я здесь насчитал аж 2 операции копирования: инициализация Object result и копирование результата в вызывающий модуль: Код: Object o3 = o1 + o2; Чем я больше думаю над этим, тем меньше у меня понимания - как вообще можно жить с операторами, если это тянет за собой многочисленные вызовы конструкторов копирования и потери динамической памяти? ??? Покурив Майерса, понял такую вещь: работа с указателями - это часть языка Си. Арифметические операторы - фишка языка C++. Задним умом уже чую, что бесшовно слить эти 2 техники не получится :) Тогда возникает другой вопрос - если операторы были изобретены консорциумом C++, значит должен существовать способ "безопасного", корректного во всех отношениях их использования? Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 10, 2013, 22:46 Выходит, что клиентский код отработает без утечек только если будет всегда принимать результат операторов по ссылке. Если же программист напишет "Matrix m1 = ..." вместо "const Matrix & m1 = ...", то его программа будет вознаграждена утечками от каждого вызова оператора сложения. Это же абсолютно непригодный вариант :) Но с другой стороны двоичные операторы так или иначе требуют выделения памяти динамически - от этого никуда не деться, если мы хотим оставить исходные операнды без изменений. Чушь полная.. Покажите, где (в серьёзных библиотеках) так пишут в отношении таких объектов, как матрицы? В интернете сплошь и рядом видел примеры таких реализаций операторов: Я же писал выше, что это проблема обходится.. И что создание временных объектов в вашем случае очень, очень не рационально.. Это очень плохо( Но во-первых я здесь насчитал аж 2 операции копирования: инициализация Object result и копирование результата в вызывающий модуль: Код: Object o3 = o1 + o2; Чем я больше думаю над этим, тем меньше у меня понимания - как вообще можно жить с операторами, если это тянет за собой многочисленные вызовы конструкторов копирования и потери динамической памяти? ??? Покурив Майерса, понял такую вещь: работа с указателями - это часть языка Си. Арифметические операторы - фишка языка C++. Задним умом уже чую, что бесшовно слить эти 2 техники не получится :) Тогда возникает другой вопрос - если операторы были изобретены консорциумом C++, значит должен существовать способ "безопасного", корректного во всех отношениях их использования? Наверное, не стоит вам курить Майерса, если он такие примеры публикует( Но я всё же думаю, что это просто вы чего то не дополняли.. (не доводилось читать Майерса просто). Ещё раз повторю, почитайте о move assignment operator и move constructor.. А также о "шаблонах выражений" Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: schmidt от Май 10, 2013, 23:44 Цитировать Наверное, не стоит вам курить Майерса, если он такие примеры публикует( Но я всё же думаю, что это просто вы чего то не дополняли.. (не доводилось читать Майерса просто). Ещё раз повторю, почитайте о move assignment operator и move constructor.. А также о "шаблонах выражений" Да нет, Майерс таких примеров не публикует, это все мои эксперименты ) Просто у Майерса читал тему об управлении ресурсами, где приводился совет об использовании smart-указателей для управления ресурсами во избежание подобных ситуаций: Код: void f() { Предлагая поступать так: Код: void f() { То есть я могу таким способом избежать утечек памяти, работая с указателями, но не с объектами в контексте операторов. Я Вас понял, стоит почитать про move assignment operator и move constructor, а также о "шаблонах выражений", спасибо :) Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 11, 2013, 00:04 Вместо auto_ptr лучше использовать std::unique_ptr
Но я всё же не понимаю, как это вас спасёт от временных объектов? м?) Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 11, 2013, 00:43 Я о том и говорю, что никто не удаляет временный объект... ;) П.С.: в общем лучше использовать новые возможности c++11, как Вы m_ax и говорили, ну или вместо new использовать локальный (временный) объект в стеке. При приеме его по ссылке он будет жить дальше... Ну честно говоря, создание временных объектов все и пытаются обойти.. Поскольку в данном случае, такой подход будет крайне не эффективен. Ну вот если у нас, например, матрица 100 на 100 какого-нибудь типа супер доубле.. И мы хотим в один приём написать выражение типа: m1+m2+m3+m4+m5+m6 Тогда будет создано 5 временных объектов, размером в 100 на 100 супер доубле.. ( Не очень айс( А что если подобных выражений в коде очень много..? Вот тогда точно не айс, если мы претендуем на производительность.. Тогда есть только два пути (ну на сколько мне известно) решить эту проблему.. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: vintik от Май 11, 2013, 20:38 Я бы порекомендовал прочитать целиком правило 21 (Не пытайтесь вернуть ссылку, когда должны вернуть объект) из упомянутой книжки. Там есть пример, который объясняет почему не стоит возвращать ссылку на сконструированный в куче объект
Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: schmidt от Май 12, 2013, 23:43 Я бы порекомендовал прочитать целиком правило 21 (Не пытайтесь вернуть ссылку, когда должны вернуть объект) из упомянутой книжки. Там есть пример, который объясняет почему не стоит возвращать ссылку на сконструированный в куче объект Всё верно, это правило говорит: "Не стоит так делать" :) Но не отвечает на вопрос "Как все-таки быть?". Нашел ответ в следующей книге "Наиболее эффективное использование C++" :) Идея состоит в следующем: Цитировать Очень часто функции, которые возвращают объекты используются таким образом, что компиляторы могут устранить затраты на создание временных объектов. Тонкость заключается в том, чтобы возвращать аргументы конструктора вместо объектов: const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational( lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator() ); } ... Правила языка С++ позволяют компиляторам выполнять оптимизацию засчет удаления временных объектов. Это значит, что компиляторы, поддерживающие т.н. "оптимизацию возвращаемого значения" могут схитрить и ничего не копировать а напрямую вызвать конструктор для построения объекта в подобном случае как: Код: Rational a = 10; То есть создавать объект, определенный выражением return, в памяти, выделенной под объект c. С такой оптимизацией можно обойтись всего одним вызовом конструктора. И как еще одна (незаслуженно забытая) альтернатива заключается в том, чтобы просто объявить operator встроенным (inline): Код: inline const Rational operator*(const Rational& lhs, const Rational& rhs) { Тогда вызов конструктора будет один, независимо от поддержки компилятором оптимизации. P.S. В целом варианты решения проблемы найдены, тему можно считать решенной, всем участникам спасибо :) Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 13, 2013, 00:58 Это значит, что компиляторы, поддерживающие т.н. "оптимизацию возвращаемого значения" могут схитрить и ничего не копировать а напрямую вызвать конструктор для построения объекта в подобном случае как: Код: Rational a = 10; То есть создавать объект, определенный выражением return, в памяти, выделенной под объект c. С такой оптимизацией можно обойтись всего одним вызовом конструктора. И как еще одна (незаслуженно забытая) альтернатива заключается в том, чтобы просто объявить operator встроенным (inline): Код: inline const Rational operator*(const Rational& lhs, const Rational& rhs) { Тогда вызов конструктора будет один, независимо от поддержки компилятором оптимизации. А что если мы чуть-чуть усложним ситуацию: Код Вы уверены, что временных объектов создано не будет? м?) Хотя, конечно, для таких объектов, как Rational, считать временные объекты - это как то.. (стёр) Но вот если объекты по жирнее (вроде матриц) тогда стоит задуматься.. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: twp от Май 13, 2013, 18:45 То есть я могу таким способом избежать утечек памяти, работая с указателями, но не с объектами в контексте операторов. Еще наверно стоит почитать про Implicit Sharing (http://qt-project.org/doc/qt-4.8/implicit-sharing.html).Я Вас понял, стоит почитать про move assignment operator и move constructor, а также о "шаблонах выражений", спасибо :) При этом подходе не используются ни шаблоны, ни возможности C++11, но при этом копирование происходит потоко-безопасно и без больших накладных расходах. На этом подходе базируются многие классы Qt начиная с QString и заканчивая классов контейнеров. А скажем QDomNode использует explicit sharing т.е. поведение указателей в отличие от implicit sharing. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: Igors от Май 14, 2013, 10:05 Еще наверно стоит почитать про Implicit Sharing (http://qt-project.org/doc/qt-4.8/implicit-sharing.html). Так-то оно так, но код предполагающий имплисит шару легко может оказаться непортабельным за пределы Qt. Поэтому мне кажется привыкать к этой "маминой сисе" не стоит.При этом подходе не используются ни шаблоны, ни возможности C++11, но при этом копирование происходит потоко-безопасно и без больших накладных расходах. На этом подходе базируются многие классы Qt начиная с QString и заканчивая классов контейнеров. А скажем QDomNode использует explicit sharing т.е. поведение указателей в отличие от implicit sharing. Возвращаясь к теме: если Вы определили оператор +, то возврат по значению нужен, избегать его не стоит - такой монструозный оператор уже будет нечто другое. Хотите без временных - используйте +=. "Перегруженные операторы должны иметь точно такой же смысл как в С" - так пишут в книжках и я с ними (в данном случае) полностью согласен. Вообще Ваш пример неестественный - что такое + для матрицы? Чему это соответствует в матричных операциях? Слишком много надумано Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: twp от Май 14, 2013, 10:40 Так-то оно так, но код предполагающий имплисит шару легко может оказаться непортабельным за пределы Qt. Поэтому мне кажется привыкать к этой "маминой сисе" не стоит. Тема была создана в разделе Qt, а не pure C++, так что никаких противоречий не вижу. И вообще с таким подходом можно все Qt обозвать "маминой сисей". А что ж тогда использовать, stl или взрывающие мозг boost или loki, а может нравится изобретать велосипеды? Ничего не имею против - каждый выбирает что ему ближе.Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 14, 2013, 10:48 "Перегруженные операторы должны иметь точно такой же смысл как в С" - так пишут в книжках и я с ними (в данном случае) полностью согласен. Вообще Ваш пример неестественный - что такое + для матрицы? Чему это соответствует в матричных операциях? Слишком много надумано Значит оператор + для Color - это вполне в порядке вещей, а для матриц оператор + (которым все математики пользуются уже давным давно) - это уже нонсенс, да?) Кстати, Implicit Sharing, это, конечно, хорошо, но от временных объектов он опять-таки не поможет.. Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: Igors от Май 14, 2013, 12:07 Тема была создана в разделе Qt, а не pure C++, так что никаких противоречий не вижу. И вообще с таким подходом можно все Qt обозвать "маминой сисей". И это очень верно - развивается эффект привыкания, типа "великий спец, но... только при наличии готовых Qt классов - а без них и пукнуть не может". А что ж тогда использовать, stl или взрывающие мозг boost или loki, а может нравится изобретать велосипеды? Ничего не имею против - каждый выбирает что ему ближе. Тоже согласен, выбирать так или иначе придется, только "ближе к языку и меньше зависимостей" = лучше. Кстати обойти STL вряд ли удастся Значит оператор + для Color - это вполне в порядке вещей, а для матриц оператор + (которым все математики пользуются уже давным давно) - это уже нонсенс, да?) Операция + для Color совершенно интуитивна, ведь Вы смешивали краски в детстве :)Название: Re: [РЕШЕНО] Перегрузка операторов: управление ресурсами (памятью) Отправлено: m_ax от Май 14, 2013, 12:29 И ещё,по-поводу implicit sharing.. В готовых решениях, товарищ navrocky, это уже реализовал без использования Qt.. http://www.prog.org.ru/topic_15029_0.html (http://www.prog.org.ru/topic_15029_0.html)
А для тех, у кого бустофобия, сейчас уже можно заменить boost::shared_ptr и boost::make_shared на их аналоги из stl Название: Re: Перегрузка операторов: управление ресурсами (памятью) Отправлено: keekdown от Май 19, 2013, 14:07 Это не очень эффективный код. Полностью согласен с вами)Во-первых у вас при операциях над матрицами создаются временные объекты.. Для больших матриц и длинных выражениях, например: m1 + m2 + m3 - m4 +.. это будет очень медленно работать и отъедать много памяти.. Я как то уже писал о том, как эту проблему можно решить с помощью "шаблонов выражений". http://www.prog.org.ru/topic_21540_0.html (http://www.prog.org.ru/topic_21540_0.html) Но лучше ещё подсмотреть, как это всё реализовано в boost'е. Во-вторых, всё же лучше бинарные операторы (такие как +, -) делать не членами класса, т.е. Код
Но это лишь, как рекомендуемая форма.. И ещё, здесь уместно вспомнить о новых возможностях c++11, а именно о перемещающем конструкторе (move constructor) и перемещающем операторе присваивания (move assignment operator). Для таких объектов, как матрицы, они могут быть очень полезны. |