Практика кодирования: возврат по значению или по ссылке в матричном умножении?

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

Что мне нужно

У меня есть класс 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) быстрыми, поэтому, вероятно, прохождение по стоимости не должно быть варианта.

Какие решения я изучил

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

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


person tunnuz    schedule 25.01.2009    source источник


Ответы (5)


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

Matrix& operator=(Matrix const& m);

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

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

Matrix operator*(Matrix const& m) const;

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

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

person j_random_hacker    schedule 25.01.2009
comment
Я согласен с этим - максимально используйте const, а затем полагайтесь на такие вещи, как RVO (оптимизация возвращаемого значения), чтобы избежать как можно большего количества посторонних копий. Конечно, если вам нужна более высокая производительность, вы можете взглянуть на семантику копирования при записи, как упомянул Петерхен. - person Sumudu Fernando; 25.01.2009
comment
второй фрагмент должен быть оператором матрицы * (Matrix const & m) const; на самом деле, если вы собираетесь реализовать его как оператор-член с корректностью const. - person Greg Rogers; 26.01.2009

Вам действительно стоит прочитать Эффективный 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;
}

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

person vava    schedule 25.01.2009
comment
+1, оба хороших предложения. (Хотя на самом деле, если OP не выполняет динамическое выделение памяти внутри Matrix, лучше не иметь ни оператора = (), ни ctor копирования и просто использовать значения по умолчанию.) Я думаю, это небольшая вещь. в последнем фрагменте кода перед этим должен стоять *. - person j_random_hacker; 25.01.2009
comment
Я также согласен - гораздо проще сначала определить операторы стиля * =, а затем получить необновляющие, чем наоборот. Кроме того: вы должны объявлять материал как вызывающий-const, когда это возможно: оператор матрицы * (const Matrix & m) const - person Sumudu Fernando; 25.01.2009
comment
Я согласен с тем, что сначала лучше иметь * =, но как бы вы определили это для матрицы, не делая копии (помните, каждое значение используется несколько раз) - person falstro; 25.01.2009

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

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

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

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

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

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

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

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

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

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();
  }
}
person peterchen    schedule 25.01.2009
comment
Я использую простой C ++ под UNIX. Как только я получу компиляцию библиотеки и работу с механизмом передачи по значению, я подумаю об оптимизации. Спасибо за предложение. - person tunnuz; 25.01.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 объектах.

person Konrad Rudolph    schedule 25.01.2009
comment
Это предложение было очень полезным! Я пытаюсь решить некоторые проблемы, но считаю, что это правильный путь. - person tunnuz; 25.01.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);

техника.

person tunnuz    schedule 25.01.2009
comment
Вы вызываете какой-либо из этих 4 методов внутри оператора operator = () или operator * ()? Если нет, то у вас не будет проблем - простое объявление параметра const не делает переданный ему аргумент константным, если он еще не был! - person j_random_hacker; 25.01.2009
comment
Если вы вызываете любой из этих методов либо из operator = (), либо из operator * (), то, поскольку вы говорите, что L, U и P являются кэшированными значениями, вероятно, безопасно объявить их изменяемыми, что означает, что они могут быть изменены даже в const объект. Именно для этого было разработано ключевое слово mutable. - person j_random_hacker; 25.01.2009
comment
Только не называйте их на m, это не const. Или отметьте кеш как изменяемый. - person vava; 25.01.2009