Практика кодирования:возвращать по значению или по ссылке при матричном умножении?
Вопрос
Я пишу этот вопрос со ссылкой на этот о котором я написал вчера.После небольшой документации мне кажется очевидным, что то, что я хотел сделать (и то, что я считал возможным), почти невозможно, если не невозможно вообще.Есть несколько способов реализовать это, и поскольку я не опытный программист, я спрашиваю вас, какой выбор вы бы выбрали.Я снова объясняю свою проблему, но теперь у меня есть несколько решений, которые нужно изучить.
То, что мне нужно
У меня есть класс 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 return = new Matrix(...)
) в куче установите значения, а затем просто:
return *result;
То, что у меня есть сегодня
После того, как Обсуждение Я решил реализовать это "по-другому", чтобы пользователя не беспокоили указатели любого типа и сохранить использование неизменным."Другой способ" заключается в передаче возвращаемого значения operator* по значению:
Matrix operator*(Matrix& m);
Matrix& operator=(Matrix& m);
Оператор* создает экземпляр return
в стеке установите значения, а затем верните объект.
С таким подходом связана проблема:это не работает.Оператор= ожидает матрицу&, а оператор* возвращает матрицу.Более того, этот подход мне кажется не очень хорошим по другой причине:Я имею дело с матрицами, которые могут быть очень большими, и цели этой библиотеки должны были быть 1) достаточно хорошими для моего проекта 2) быстрыми, поэтому, вероятно, передача по значению не должна быть вариантом.
Какие решения я исследовал
Что ж, следуя предложениям, приведенным в предыдущем Обсуждение Я кое-что прочитал об умных указателях, они выглядят великолепно, но я до сих пор не могу понять, как с их помощью решить мою проблему.Они имеют дело с освобождением памяти и копированием указателя, но я в основном использую ссылки, поэтому они не кажутся мне правильным выбором.Но я могу ошибаться.
Возможно, единственное решение - передавать по значению, возможно, я не могу получить одновременно эффективность и хороший интерфейс.Но опять же, вы эксперт, и я хотел бы знать ваше мнение.
Решение
Проблема, с которой вы столкнулись, заключается в том, что выражение a * b
создает временный object, а в 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 ++ не разрешает временную привязку к непостоянной ссылке, заключается в том, что временные объекты существуют (как следует из названия) лишь очень короткое время, и в большинстве случаев было бы ошибкой пытаться их модифицировать.
Другие советы
Вы должны действительно прочитать Эффективный C ++ Скотта Мейерса, у него есть отличные темы по этому.
Как уже говорилось, лучшие подписи для operator=
и operator*
являются
Matrix& operator=(Matrix const& m);
Matrix operator*(Matrix const& m) const;
но я должен сказать, что вы должны реализовать код умножения в
Matrix& operator*=(Matrix const& m);
и просто используйте его в <=>
Matrix operator*(Matrix const &m) const {
return Matrix(*this) *= m;
}
Таким образом, пользователь мог бы умножать без создания новых матриц, когда он хочет. Конечно, чтобы этот код работал, у вас также должен быть конструктор копирования:)
Примечание. Начните с предложений Vadims. Следующее обсуждение является спорным, если мы говорим только об очень маленьких матрицах, например, если вы ограничиваете себя матрицами 3х3 или 4х4. Кроме того, я надеюсь, что я не пытаюсь втиснуть в вас много идей:)
Поскольку матрица является потенциально тяжелым объектом, вам определенно следует избегать глубоких копий. Умные указатели являются одной утилитой для ее реализации, но они не решают вашу проблему «из коробки».
В вашем сценарии есть два общих подхода. В обоих случаях любая копия (например, a=b
) копирует только ссылку и увеличивает счетчик ссылок (это то, что умные указатели могут сделать для вас). Р>
При Копировать при записи глубокое копирование задерживается до внесения изменений. например вызов функции-члена, такой как void Matrix.TransFormMe()
on b
, увидит, что на фактические данные ссылаются два объекта (a и b), и создаст глубокую копию перед выполнением преобразования.
Чистый эффект в том, что ваш матричный класс действует как " normal " объект, но количество глубоких копий, фактически сделанных, значительно сокращается.
Другой подход - это неизменяемые объекты , где сам API никогда не модифицирует существующий объект - любая модификация создает новый объект. Таким образом, вместо элемента void TransformMe()' member transforming the contained matrix, Matrix contains only a
Matrix GetTransformed () `, возвращающего копию данных.
Какой метод лучше, зависит от фактических данных. В MFC CString
является копированием при записи, в .NET a 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();
}
}
... Являются всеми, кроме const, поскольку все они вызывают (при необходимости):
void lupp();
Это обновляет кэшированный
L
,U
иP
.То же самое означает иget_inverse()
этот звонокlupp()
а также наборыMatrix* Matrix::inverse
.Это вызывает проблему с:Matrix& operator=(Matrix const& m); Matrix operator*(Matrix const& m);
техника.
Пожалуйста, объясните, как это вызывает проблемы.Обычно так не должно быть.Кроме того, если вы используете переменные-члены для кэширования временных результатов, сделайте их mutable
.Тогда вы можете изменять их даже в const
Объекты.
Да, ваше предложение хорошее, и я признаю, что я не знал о проблеме с временными объектами с неконстантными ссылками.Но мой класс 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);
техника.