Práctica de codificación: ¿retorno por valor o por referencia en la multiplicación de matrices?

StackOverflow https://stackoverflow.com/questions/477452

  •  20-08-2019
  •  | 
  •  

Pregunta

Estoy escribiendo esta pregunta con referencia a este que escribí ayer. Después de un poco de documentación, me parece claro que lo que quería hacer (y lo que creía posible) es casi imposible, si no imposible. Hay varias formas de implementarlo, y como no soy un programador experimentado, le pregunto qué opción tomaría. Explico nuevamente mi problema, pero ahora tengo algunas soluciones para explorar.

Lo que necesito

Tengo una clase Matrix y quiero implementar la multiplicación entre matrices para que el uso de la clase sea muy intuitivo:

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;

Lo que tuve ayer

En este momento sobrecargué los dos operadores operator * y operator = y hasta ayer por la noche se definieron de esta manera:

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

El operador * crea una instancia de un nuevo objeto Matrix ( Matrix return = new Matrix (...) ) en el montón, establece los valores y luego simplemente:

return *result;

Lo que tengo hoy

Después de la discusión decidí implementar de una manera "diferente" para evitar que el usuario se moleste por los punteros de cualquier tipo y para mantener el uso sin cambios. La "manera diferente" es pasar el valor de retorno del operador * por valor:

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

El operador * crea una instancia de return en la pila, establece los valores y luego devuelve el objeto.

Hay un problema con este enfoque: no funciona. El operador = espera una matriz & amp; y el operador * devuelve una matriz. Además, este enfoque no me parece tan bueno por otra razón: estoy tratando con matrices, que pueden ser muy grandes y los objetivos de esta biblioteca eran 1) lo suficientemente bueno para mi proyecto 2) rápido, por lo que probablemente pase por valor no debería ser una opción.

Qué soluciones he explorado

Bueno, siguiendo las sugerencias de la discusión anterior Leí algunas cosas sobre punteros inteligentes, se ven geniales pero aún no puedo encontrar la manera de resolver mi problema con ellos. Se ocupan de la liberación de memoria y la copia del puntero, pero básicamente estoy usando referencias, por lo que no me parecen la opción correcta. Pero puedo estar equivocado.

Quizás la única solución es pasar por valor, quizás no pueda obtener tanto eficiencia como una buena interfaz. Pero de nuevo, usted es el experto, y me gustaría saber su opinión.

¿Fue útil?

Solución

El problema que tiene es que la expresión a * b crea un objeto temporal , y en C ++, no se permite que un temporal se una a una no constante referencia, que es lo que su Matrix & amp; operador = (Matriz & amp; m) toma. Si lo cambia a:

Matrix& operator=(Matrix const& m);

El código ahora debe compilarse. Además del beneficio obvio de producir código compilable :), agregar el const también comunica a las personas que llaman que no modificará el argumento m , que puede ser información útil .

También debe hacer lo mismo para su operador * () :

Matrix operator*(Matrix const& m) const;

[EDIT: El const adicional al final indica que el método promete no alterar * this , el objeto en el lado izquierdo de la multiplicación, tampoco. Esto es necesario para hacer frente a expresiones como a * b * c : la subexpresión a * b crea un temporal y no se unirá sin el const al final. Gracias a Greg Rogers por señalar esto en los comentarios. ]

P.S. La razón por la cual C ++ no permite que un temporal se una a una referencia no constante es porque los temporales existen (como su nombre lo indica) por un tiempo muy corto, y en la mayoría de los casos, sería un error intentar modificarlos.

Otros consejos

Realmente deberías leer C ++ efectivo de Scott Meyers, tiene excelente temas sobre eso. Como ya se dijo, las mejores firmas para operator = y operator * son

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

pero debo decir que debe implementar el código de multiplicación en

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

y simplemente reutilícelo en operator*

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

de esa manera el usuario podría multiplicarse sin crear nuevas matrices cuando lo desee. Por supuesto, para que este código funcione, también debe tener un constructor de copia :)

Nota: Comience con las sugerencias de Vadims. La siguiente discusión es discutible si hablamos solo de matrices muy pequeñas, p. si te limitas a matrices 3x3 o 4x4. También espero no estar tratando de meter muchas ideas sobre ti :)

Como una matriz es potencialmente un objeto pesado, definitivamente debe evitar las copias profundas. Los punteros inteligentes son una utilidad para implementar eso, pero no resuelven su problema de inmediato.

En su escenario, hay dos enfoques comunes. En ambos casos, cualquier copia (como a = b ), solo copia una referencia e incrementa el contador de referencia (eso es lo que los punteros inteligentes pueden hacer por usted).

Con Copiar al escribir , la copia profunda se retrasa hasta que se realice una modificación. p.ej. llamar a una función miembro como void Matrix.TransFormMe () en b vería que los datos reales son referenciados por dos objetos (ayb), y crear una copia profunda antes haciendo la transformación.

El efecto neto es que su clase de matriz actúa como un " normal " objeto, pero el número de copias profundas realmente realizadas se reduce drásticamente.

El otro enfoque son Objetos inmutables donde la API en sí misma nunca modifica un objeto existente; cualquier modificación crea un nuevo objeto. Entonces, en lugar de un miembro void TransformMe () 'que transforma la matriz contenida, Matrix contiene solo un miembro Matrix GetTransformed () `, que devuelve una copia de los datos.

Qué método es mejor depende de los datos reales. En MFC, CString es copia en escritura, en .NET una String es inmutable. Las clases inmutables a menudo necesitan una clase de generador (como StringBuilder ) que evita las copias de muchas modificaciones secuenciales. Los objetos Copy-On-Write necesitan un diseño cuidadoso para que en la API quede claro qué miembro modifica los miembros internos y cuál devuelve una copia.

Para las matrices, dado que hay muchos algoritmos que pueden modificar la matriz en el lugar (es decir, el algoritmo en sí no necesita la copia), la copia en escritura podría ser la mejor solución.

Una vez intenté construir un puntero de copia en escritura encima de los punteros inteligentes de impulso, pero no lo he tocado para resolver problemas de subprocesos, etc. El pseudocódigo se vería así:

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();
  }
}
  

& # 8230; Son casi constantes ya que todos llaman (si es necesario):

void lupp();
     

Eso actualiza los L , U y P en caché. Lo mismo significa get_inverse () que llama a lupp () y también establece Matrix * Matrix :: inverse . Esto causa un problema con:

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

técnica.

Por favor explique cómo eso causa problemas. No debería, por lo general. Además, si usa variables miembro para almacenar en caché los resultados temporales, hágalos mutables . Luego puede modificarlos incluso en objetos const .

Sí, su sugerencia es buena y admito que no conocía el problema del objeto temporal con referencias no constantes. Pero mi clase Matrix también contiene facilidades para obtener la factorización LU (Eliminación Gaussiana):

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

Son todos menos const ya que todos llaman (si es necesario):

void lupp();

Eso actualiza las memorias caché L, U y P. Lo mismo significa get_inverse () que llama a lupp () y también establece Matrix * Matrix :: inverse . Esto causa un problema con:

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

técnica.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top