Russian Qt Forum
Сентябрь 30, 2024, 22:37 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: [1] 2   Вниз
  Печать  
Автор Тема: [РЕШЕНО] Перегрузка операторов: управление ресурсами (памятью)  (Прочитано 10665 раз)
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 будет ли освобождена память, выделенная в операторе сложения?
« Последнее редактирование: Май 12, 2013, 23:44 от Schmidt » Записан
alex312
Хакер
*****
Offline Offline

Сообщений: 606



Просмотр профиля
« Ответ #1 : Май 10, 2013, 12:33 »

Во-первых, хотелось бы видеть код функции
Код
C++ (Qt)
Matrix* addMatrix(Matrix* other);
.
Во-вторых, при выходе из блока, вызываются деструкторы локальных переменных. А уж чего вы там в деструкторе наосвобождаете, то и освободится.
Записан
schmidt
Гость
« Ответ #2 : Май 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;
« Последнее редактирование: Май 10, 2013, 13:17 от Schmidt » Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #3 : Май 10, 2013, 13:28 »

Это не очень эффективный код.
Во-первых у вас при операциях над матрицами создаются временные объекты.. Для больших матриц и длинных выражениях, например:
m1  + m2 + m3 - m4 +..
это будет очень медленно работать и отъедать много памяти..
Я как то уже писал о том, как эту проблему можно решить с помощью "шаблонов выражений". 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).
Для таких объектов, как матрицы, они могут быть очень полезны.
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
VPS
Гость
« Ответ #4 : Май 10, 2013, 13:55 »

3. В вызывающем коде
Код:
Matrix m3 = m1 + m2;

m3 является ссылкой на локальный объект и получает значение, возвращенное из operator+.

4. После окончания работы вызывается деструктор для локального объекта m3 и память из кучи корректно освобождается.

Или нет? Может должно быть так?

Код:
Matrix & m3 = m1 + m2;

Если я правильно понимаю, то у Вас будет утечка памяти, т.к:
1. в случае 3 у Вас вызывается копирующий конструктор. И соответственно память выделенная в через new не будет освобождена.
2. в случае 4 - это ссылка на объект расположенный в динамической памяти, который тоже надо зачищать руками...
Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #5 : Май 10, 2013, 14:02 »

3. В вызывающем коде
Код:
Matrix m3 = m1 + m2;

m3 является ссылкой на локальный объект и получает значение, возвращенное из operator+.

4. После окончания работы вызывается деструктор для локального объекта m3 и память из кучи корректно освобождается.

Или нет? Может должно быть так?

Код:
Matrix & m3 = m1 + m2;


Если я правильно понимаю, то у Вас будет утечка памяти, т.к:
1. в случае 3 у Вас вызывается копирующий конструктор. И соответственно память выделенная в через new не будет освобождена.
2. в случае 4 - это ссылка на объект расположенный в динамической памяти, который тоже надо зачищать руками...

Да, и память будет течь, поскольку временный объекты никто не удаляет..
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
VPS
Гость
« Ответ #6 : Май 10, 2013, 14:08 »

Я о том и говорю, что никто не удаляет временный объект... Подмигивающий

П.С.: в общем лучше использовать новые возможности c++11, как Вы m_ax и говорили, ну или вместо new использовать локальный (временный) объект в стеке. При приеме его по ссылке он будет жить дальше...
« Последнее редактирование: Май 10, 2013, 14:24 от vps » Записан
schmidt
Гость
« Ответ #7 : Май 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++, значит должен существовать способ "безопасного", корректного во всех отношениях их использования?
Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #8 : Май 10, 2013, 22:46 »


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

С чего это? Всё равно память будет утекать..

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

В интернете сплошь и рядом видел примеры таких реализаций операторов:
Чушь полная.. Покажите, где (в серьёзных библиотеках) так пишут в отношении таких объектов, как матрицы? 
Я же писал выше, что это проблема обходится.. И что создание временных объектов в вашем случае очень, очень не рационально.. Это очень плохо(


Но во-первых я здесь насчитал аж 2 операции копирования: инициализация Object result и копирование результата в вызывающий модуль:

Код:
Object o3 = o1 + o2;

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

Покурив Майерса, понял такую вещь: работа с указателями - это часть языка Си. Арифметические операторы - фишка языка C++. Задним умом уже чую, что бесшовно слить эти 2 техники не получится  Улыбающийся Тогда возникает другой вопрос - если операторы были изобретены консорциумом C++, значит должен существовать способ "безопасного", корректного во всех отношениях их использования?

Наверное, не стоит вам курить Майерса, если он такие примеры публикует( Но я всё же думаю, что это просто вы чего то не дополняли.. (не доводилось читать Майерса просто).

Ещё раз повторю, почитайте о move assignment operator и move constructor.. А также о "шаблонах выражений"
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
schmidt
Гость
« Ответ #9 : Май 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, а также о "шаблонах выражений", спасибо Улыбающийся
Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #10 : Май 11, 2013, 00:04 »

Вместо auto_ptr лучше использовать std::unique_ptr

Но я всё же не понимаю, как это вас спасёт от временных объектов? м?)
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #11 : Май 11, 2013, 00:43 »

Я о том и говорю, что никто не удаляет временный объект... Подмигивающий

П.С.: в общем лучше использовать новые возможности c++11, как Вы m_ax и говорили, ну или вместо new использовать локальный (временный) объект в стеке. При приеме его по ссылке он будет жить дальше...

Ну честно говоря, создание временных объектов все и пытаются обойти.. Поскольку в данном случае, такой подход будет крайне не эффективен. Ну вот если у нас, например, матрица 100 на 100 какого-нибудь типа супер доубле.. И мы хотим в один приём написать выражение типа:
m1+m2+m3+m4+m5+m6
Тогда будет создано 5 временных объектов, размером в 100 на 100 супер доубле.. ( Не очень айс( А что если подобных выражений в коде очень много..? Вот тогда точно не айс, если мы претендуем на производительность..

Тогда есть только два пути (ну на сколько мне известно) решить эту проблему..
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
vintik
Гость
« Ответ #12 : Май 11, 2013, 20:38 »

Я бы порекомендовал прочитать целиком правило 21 (Не пытайтесь вернуть ссылку, когда должны вернуть объект) из упомянутой книжки. Там есть пример, который объясняет почему не стоит возвращать ссылку на сконструированный в куче объект
Записан
schmidt
Гость
« Ответ #13 : Май 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. В целом варианты решения проблемы найдены, тему можно считать решенной, всем участникам спасибо Улыбающийся
Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #14 : Май 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, считать временные объекты - это как то.. (стёр)
Но вот если объекты по жирнее (вроде матриц) тогда стоит задуматься.. 
« Последнее редактирование: Май 13, 2013, 01:03 от m_ax » Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
Страниц: [1] 2   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.163 секунд. Запросов: 23.