Практика кодирования: возврат по значению или по ссылке в матричном умножении? - PullRequest
3 голосов
/ 25 января 2009

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

Что мне нужно

У меня есть класс Matrix, и я хочу реализовать умножение между матрицами, чтобы использование класса было очень интуитивным:

Matrix a(5,2);
a(4,1) = 6 ;
a(3,1) = 9.4 ;           
...                   // And so on ...

Matrix b(2,9);
b(0,2) = 3;
...                   // And so on ...

// After a while
Matrix i = a * b;

Что у меня было вчера

В данный момент я перегрузил два оператора operator* и operator=, и до вчерашнего вечера они были определены следующим образом:

Matrix& operator*(Matrix& m);
Matrix& operator=(Matrix& m);

Оператор * создает экземпляр нового объекта Matrix (Matrix return = new Matrix(...)) в куче, устанавливает значения и затем просто:

return *result;

Что у меня сегодня

После обсуждения я решил реализовать его «по-другому», чтобы пользователь не беспокоился по поводу указателей любого типа и чтобы сохранить использование без изменений. «Другой способ» - передать возвращаемое значение оператора * по значению:

Matrix operator*(Matrix& m);
Matrix& operator=(Matrix& m);

Оператор * создает return в стеке, устанавливает значения и затем возвращает объект.

Есть проблема с этим подходом: он не работает. Оператор = ожидает Матрицу &, а оператор * возвращает Матрицу. Более того, этот подход не выглядит так хорошо для меня по другой причине: я имею дело с матрицами, которые могут быть очень большими, и целью этой библиотеки было: 1) достаточно хорошо для моего проекта 2) быстро, так что, вероятно, прохождение по значению не должно быть опцией.

Какие решения я исследовал

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

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

Ответы [ 5 ]

9 голосов
/ 25 января 2009

Вы действительно должны прочитать Effective C ++ Скотта Мейерса, у него есть отличные темы на эту тему. Как уже говорилось, лучшие подписи для operator= и operator*:

Matrix& operator=(Matrix const& m);
Matrix operator*(Matrix const& m) const;

но я должен сказать, что вы должны реализовать код умножения в

Matrix& operator*=(Matrix const& m);

и просто повторно использовать его в operator*

Matrix operator*(Matrix const &m) const {
    return Matrix(*this) *= m;
}

таким образом, пользователь мог бы умножать без создания новых матриц, когда он хочет. Конечно, чтобы этот код работал, у вас также должен быть конструктор копирования:)

9 голосов
/ 25 января 2009

Проблема, с которой вы столкнулись, заключается в том, что выражение a * b создает временный объект, а в C ++ временному не разрешается привязываться к непостоянной ссылке, как и ваша Matrix& operator=(Matrix& m) берет. Если вы измените его на:

Matrix& operator=(Matrix const& m);

Код должен скомпилироваться. Помимо очевидного преимущества создания компилируемого кода :), добавление const также сообщает вашим вызывающим, что вы не будете изменять аргумент m, который может быть полезной информацией.

Вы должны сделать то же самое для вашего operator*():

Matrix operator*(Matrix const& m) const;

[РЕДАКТИРОВАТЬ: Дополнительная const в конце указывает, что метод обещает не изменять *this, объект на левой стороне умножения, или. Это необходимо для работы с такими выражениями, как a * b * c - подвыражение a * b создает временное выражение и не будет привязываться без const в конце. Спасибо Грегу Роджерсу за указание на это в комментариях. ]

P.S. Причина, по которой C ++ не позволяет временным связываться с непостоянной ссылкой, заключается в том, что временные файлы существуют (как следует из названия) только в течение очень короткого времени, и в большинстве случаев было бы ошибкой пытаться изменить их.

3 голосов
/ 25 января 2009

… Все, кроме const, так как все они вызывают (при необходимости):

void lupp();

Обновляет кэшированные L, U и P. То же самое означает get_inverse(), который вызывает lupp(), а также устанавливает Matrix* Matrix::inverse. Это вызывает проблемы с:

Matrix& operator=(Matrix const& m);
Matrix operator*(Matrix const& m);

техника.

Пожалуйста, объясните, как это вызывает проблемы. Обычно не должно быть. Кроме того, если вы используете переменные-члены для кэширования временных результатов, сделайте их mutable. Тогда вы можете изменять их даже в const объектах.

3 голосов
/ 25 января 2009

Примечание: начните с предложений Вадима. Следующее обсуждение является спорным, если мы говорим только об очень маленьких матрицах, например, если вы ограничиваете себя матрицами 3х3 или 4х4. Кроме того, я надеюсь, что я не пытаюсь втиснуть в вас много идей:)

Поскольку матрица является потенциально тяжелым объектом, вам определенно следует избегать глубоких копий. Умные указатели - это одна утилита для ее реализации, но они не решают вашу проблему «из коробки».

В вашем сценарии есть два общих подхода. В обоих случаях любая копия (например, a=b) копирует только ссылку и увеличивает счетчик ссылок (это то, что умные указатели могут сделать для вас).

При Копирование при записи , глубокое копирование задерживается до внесения изменений. например вызов функции-члена, такой как void Matrix.TransFormMe() на b, увидит, что на фактические данные ссылаются два объекта (a и b), и создаст глубокую копию перед выполнением преобразования.

Чистый эффект состоит в том, что ваш матричный класс действует как «нормальный» объект, но количество фактически сделанных глубоких копий резко сокращается.

Другой подход: Неизменяемые объекты , где сам API никогда не модифицирует существующий объект - любая модификация создает новый объект. Таким образом, вместо элемента void TransformMe()' member transforming the contained matrix, Matrix contains only a Matrix GetTransformed () `возвращается копия данных.

Какой метод лучше, зависит от фактических данных. В MFC CString является копированием при записи, в .NET String является неизменным. Неизменяемые классы часто нуждаются в классе конструктора (например, StringBuilder), который избегает копий многих последовательных модификаций. Объекты Copy-On-Write требуют тщательного проектирования, чтобы в API было понятно, какой элемент изменяет внутренние элементы, а какой возвращает копию.

Для матриц, поскольку существует много алгоритмов, которые могут модифицировать матрицу на месте (т. Е. Сам алгоритм не требует копирования), копирование при записи может быть лучшим решением.

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

class CowPtr<T>
{
     refcounting_ptr<T> m_actualData;
   public:
     void MakeUnique()
     {
        if (m_actualData.refcount() > 1)
           m_actualData = m_actualData.DeepCopy();
     }
     // ...remaining smart pointer interface...
}

class MatrixData // not visible to user
{
  std::vector<...> myActualMatrixData;
}

class Matrix
{
  CowPtr<MatrixData> m_ptr; // the simple reference that will be copied on assignment

  double operator()(int row, int col)  const
  {  // a non-modifying member. 
     return m_ptr->GetElement(row, col);
  }

  void Transform()
  {
    m_ptr.MakeUnique(); // we are going to modify the data, so make sure 
                        // we don't modify other references to the same MatrixData
    m_ptr->Transform();
  }
}
0 голосов
/ 25 января 2009

Да, ваши предложения хороши, и я признаю, что не знал о проблеме временных объектов с неконстантными ссылками. Но мой класс Matrix также содержит средства для получения факторизации LU (исключения Гаусса):

const Matrix& get_inverse();
const Matrix& get_l();
const Matrix& get_u();
const Matrix& get_p();

Все, кроме const, так как все они вызывают (при необходимости):

void lupp();

Это обновляет кэшированные L, U и P. То же самое означает get_inverse(), который вызывает lupp (), а также устанавливает Matrix* Matrix::inverse. Это вызывает проблемы с:

Matrix& operator=(Matrix const& m);
Matrix operator*(Matrix const& m);

техника.

...