Russian Qt Forum

Qt => Общие вопросы => Тема начата: schmidt от Май 10, 2013, 11:57



Название: [РЕШЕНО] Перегрузка операторов: управление ресурсами (памятью)
Отправлено: schmidt от Май 10, 2013, 11:57
Добрый день, уважаемые,

Читаю "Эффективное использование С++" Майерса, серьёзный интерес вызвали вопросы корректного использования памяти, предотвращения утечек. Хочу понять, как ведут себя объекты, создаваемые внутри перегруженных операторов сложения/умножения и возвращаемые как результат, например в таком случае:

Код:
class Matrix {
...
const Matrix & operator+(const Matrix & other);
/* Функция, складывающая матрицы по указателям.
 * Динамически выделяет память под матрицу-результат
 * и возвращает указатель на нее, не модифицируя исходных операндов. */
Matrix* addMatrix(Matrix* other);
...
}
...
const Matrix & Matrix::operator+(const Matrix & other) {
    Matrix* result = this->addMatrix(&other);

    return *result;
}

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

Код:
Matrix m1(15,15);
m1.fillWithRandValues();

Matrix m2(15,15);
m2.fillWithRandValues();

Matrix m3 = m1 + m2;

Иными словами, при удалении локального объекта m3 будет ли освобождена память, выделенная в операторе сложения?


Название: Re: Перегрузка операторов: управление ресурсами (памятью)
Отправлено: alex312 от Май 10, 2013, 12:33
Во-первых, хотелось бы видеть код функции
Код
C++ (Qt)
Matrix* addMatrix(Matrix* other);
.
Во-вторых, при выходе из блока, вызываются деструкторы локальных переменных. А уж чего вы там в деструкторе наосвобождаете, то и освободится.


Название: Re: Перегрузка операторов: управление ресурсами (памятью)
Отправлено: schmidt от Май 10, 2013, 12:37
Код:
//-----------------------------------------------------------------------------
ArrayMatrix* ArrayMatrix::addMatrix(const ArrayMatrix* other) const {
// Если размеры матриц совпадают
    if(this->rowCount() == other->rowCount() &&
            this->columnCount() == this->columnCount()) {
        // Инициализировать матрицу-результат
        int result_rows = this->rowCount();
        int result_columns = this->columnCount();
        ArrayMatrix* result_matrix = new ArrayMatrix(result_rows, result_columns);

        // Выполнить поэлементное сложение
        for(int row = 0; row < this->rowCount(); row++) {
            for(int column = 0; column < this->columnCount(); column++) {
                double sum = this->value(row, column) + other->value(row, column);
                    result_matrix->setValue(row, column, sum);
            }
        }

        return result_matrix;
    }

    return NULL;
}


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

То есть, если я правильно понимаю, все выглядит так:

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++ (Qt)
class Matrix
{
...
friend const Matrix & operator+(const Matrix &, const Matrix &);
};
 
const Matrix & operator+(const Matrix & m1, const Matrix & m2);
 
 
Но это лишь, как рекомендуемая форма.. 

И ещё, здесь уместно вспомнить о новых возможностях 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;
std::clog << "m4 [" << &m4 << "]" << endl;
const ArrayMatrix &ref_m5 = m1 + m2;
std::clog << "ref_m5 [" << &ref_m5 << "]" << endl;

При таком коде в случае с m4 происходит копирование (адрес объекта другой), а ref_m5 принимает адрес объекта в динамической куче. Если я правильно понимаю, корректно и без утечек отработает только объект ref_m5, т.к. в нем записан адрес той самой матрицы в куче, созданной в operator+. Деструктор ref_m5 подчистит всё в куче.

Выходит, что клиентский код отработает без утечек только если будет всегда принимать результат операторов по ссылке. Если же программист напишет "Matrix m1 = ..." вместо "const Matrix & m1 = ...", то его программа будет вознаграждена утечками от каждого вызова оператора сложения. Это же абсолютно непригодный вариант :)

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

В интернете сплошь и рядом видел примеры таких реализаций операторов:

Код:
const Object& Object::operator+(const Object& other) {
    Object result = (*this);
    result += other;
    return result;
}

Но во-первых я здесь насчитал аж 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() {
  Object* ptr_obj = new Object();

  // ... Здесь может возникнуть исключение и delete не будет вызван ...

  delete ptr_obj;
}

Предлагая поступать так:

Код:
void f() {
  std::auto_ptr<Object> smart_ptr( new Object() );

  // ...
  // Вызов delete больше не нужен, его вызовет деструктор smart_ptr
}

То есть я могу таким способом избежать утечек памяти, работая с указателями, но не с объектами в контексте операторов.
Я Вас понял, стоит почитать про 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;
Rational b(1, 2);
Rational c = a * b;

То есть создавать объект, определенный выражением return, в памяти, выделенной под объект c. С такой оптимизацией можно обойтись всего одним вызовом конструктора.

И как еще одна (незаслуженно забытая) альтернатива заключается в том, чтобы просто объявить operator встроенным (inline):

Код:
inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational( lhs.numerator() * rhs.numerator(),
                            lhs.denominator() * rhs.denominator() );
}

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

P.S. В целом варианты решения проблемы найдены, тему можно считать решенной, всем участникам спасибо :)


Название: Re: Перегрузка операторов: управление ресурсами (памятью)
Отправлено: m_ax от Май 13, 2013, 00:58

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

Код:
Rational a = 10;
Rational b(1, 2);
Rational c = a * b;

То есть создавать объект, определенный выражением return, в памяти, выделенной под объект c. С такой оптимизацией можно обойтись всего одним вызовом конструктора.

И как еще одна (незаслуженно забытая) альтернатива заключается в том, чтобы просто объявить operator встроенным (inline):

Код:
inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational( lhs.numerator() * rhs.numerator(),
                            lhs.denominator() * rhs.denominator() );
}

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

Не спешите)
А что если мы чуть-чуть усложним ситуацию:
Код
C++ (Qt)
Rational c = a * b * d;
 
Вы уверены, что временных объектов создано не будет? м?)

Хотя, конечно, для таких объектов, как Rational, считать временные объекты - это как то.. (стёр)
Но вот если объекты по жирнее (вроде матриц) тогда стоит задуматься.. 


Название: Re: Перегрузка операторов: управление ресурсами (памятью)
Отправлено: twp от Май 13, 2013, 18:45
То есть я могу таким способом избежать утечек памяти, работая с указателями, но не с объектами в контексте операторов.
Я Вас понял, стоит почитать про move assignment operator и move constructor, а также о "шаблонах выражений", спасибо :)
Еще наверно стоит почитать про Implicit Sharing (http://qt-project.org/doc/qt-4.8/implicit-sharing.html).
При этом подходе не используются ни шаблоны, ни возможности 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).
При этом подходе не используются ни шаблоны, ни возможности C++11, но при этом копирование происходит потоко-безопасно и без больших накладных расходах. На этом подходе базируются многие классы Qt начиная с QString и заканчивая классов контейнеров. А скажем QDomNode использует explicit sharing т.е. поведение указателей в отличие от implicit sharing.
Так-то оно так, но код предполагающий имплисит шару легко может оказаться непортабельным за пределы Qt. Поэтому мне кажется привыкать к этой "маминой сисе" не стоит.

Возвращаясь к теме: если Вы определили оператор +, то возврат по значению нужен, избегать его не стоит - такой монструозный оператор уже будет нечто другое. Хотите без временных - используйте +=.

"Перегруженные операторы должны иметь точно такой же смысл как в С" - так пишут в книжках и я с ними (в данном случае) полностью согласен. Вообще Ваш пример неестественный - что такое + для матрицы? Чему это соответствует в матричных операциях? Слишком много надумано 


Название: 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++ (Qt)
class Matrix
{
...
friend const Matrix & operator+(const Matrix &, const Matrix &);
};
 
const Matrix & operator+(const Matrix & m1, const Matrix & m2);
 
 
Но это лишь, как рекомендуемая форма.. 

И ещё, здесь уместно вспомнить о новых возможностях c++11, а именно о перемещающем конструкторе (move constructor) и перемещающем операторе присваивания (move assignment operator).
Для таких объектов, как матрицы, они могут быть очень полезны.
Полностью согласен с вами)