2010-01-26 11 views
8

He visto numerosos argumentos de que usar un valor de retorno es preferible a los parámetros de salida. Estoy convencido de las razones por las cuales evitarlos, pero no estoy seguro de si me estoy encontrando con casos en los que es inevitable.Cómo evitar los parámetros?

Parte 1 de mi pregunta es: ¿Cuáles son algunas de sus formas favoritas/comunes de moverse utilizando un parámetro out? Cosas como las siguientes: Hombre, en las revisiones por pares siempre veo a otros programadores hacer esto cuando podrían haberlo hecho de esta manera.

Parte Dos Parte de mi pregunta se refiere a algunos casos específicos que he encontrado donde me gustaría evitar un parámetro de salida pero no puedo pensar en una manera limpia de hacerlo.

Ejemplo 1: Tengo una clase con una copia costosa que me gustaría evitar. Se puede trabajar en el objeto y esto crea el objeto que será costoso de copiar. El trabajo para construir los datos tampoco es trivial. Actualmente, pasaré este objeto a una función que modificará el estado del objeto. Para mí, esto es preferible a actualizar el objeto interno a la función de trabajador y devolverlo, ya que me permite mantener las cosas en la pila. código

class ExpensiveCopy //Defines some interface I can't change. 
{ 
public: 
    ExpensiveCopy(const ExpensiveCopy toCopy){ /*Ouch! This hurts.*/ }; 
    ExpensiveCopy& operator=(const ExpensiveCopy& toCopy){/*Ouch! This hurts.*/}; 

    void addToData(SomeData); 
    SomeData getData(); 
} 

class B 
{ 
public: 
    static void doWork(ExpensiveCopy& ec_out, int someParam); 
    //or 
    // Your Function Here. 
} 

Usando mi función, consigo llamar así:

const int SOME_PARAM = 5; 
ExpensiveCopy toModify; 
B::doWork(toModify, SOME_PARAM); 

me gustaría tener algo como esto:

ExpensiveCopy theResult = B::doWork(SOME_PARAM); 

Pero no sé si esto es posible.

Segundo ejemplo: Tengo una matriz de objetos. Los objetos en la matriz son de un tipo complejo, y necesito trabajar en cada elemento, trabajo que me gustaría mantener separado del bucle principal que accede a cada elemento. El código actualmente se ve así:

std::vector<ComplexType> theCollection; 
for(int index = 0; index < theCollection.size(); ++index) 
{ 
    doWork(theCollection[index]); 
} 

void doWork(ComplexType& ct_out) 
{ 
    //Do work on the individual element. 
} 

¿Alguna sugerencia sobre cómo lidiar con algunas de estas situaciones? Trabajo principalmente en C++, pero estoy interesado en ver si otros idiomas facilitan una configuración más sencilla. Me he encontrado con RVO como una posible solución, pero necesito leer más sobre él y me parece una característica específica del compilador.

+0

La semántica de un parámetro de referencia no constante es que se (puede) modificarse en la función. Si no fuera a cambiarlo, lo pasaría como una referencia constante, después de todo. – Bill

+0

* "pero estoy interesado en ver si otros idiomas facilitan una configuración más sencilla" *: los lenguajes como Java y C# simplemente no tienen ese problema, ya que siempre hacen referencias a objetos a su alrededor. –

+0

Maravillosas respuestas de todos. Tenía la sensación de que en mis ejemplos específicos no habría muchas otras opciones.He visto publicaciones en SO de personas que son tan inflexibles sobre cómo evitan los parámetros a toda costa. Esperaba que tal vez uno de ellos pudiera explicar cómo lograron tal hazaña. En cuanto a RVO, tengo cierto reparo en usar funciones específicas del compilador, ya que sería difícil explicar a mis jefes por qué tengo que rediseñar la mitad de mi código porque ya no funciona debido a una característica que falta como resultado de una cambio/actualización del compilador –

Respuesta

7

No estoy seguro de por qué está tratando de evitar pasar referencias aquí. Son casi todas estas situaciones que existen semánticas paso a paso.

El código

static void doWork(ExpensiveCopy& ec_out, int someParam); 

se ve perfectamente bien para mí.

Si realmente desea modificarlo entonces usted tiene un par de opciones

  1. DoWork Mover por lo que es que es un miembro de ExpensiveCopy (que se dice que no puede hacer, por lo que está fuera)
  2. devuelve un puntero (inteligente) de doWork en lugar de copiarlo.(Lo que no quiere hacer lo que desea mantener las cosas en la pila)
  3. depender de RVO (que otros han señalado con el apoyo de prácticamente todos los compiladores modernos)
+0

De acuerdo. Solo una cosa, la opción 1 que mencionas no está disponible, ya que el OP dice que 'ExpensiveCopy' no se puede modificar. – stakx

+0

@stakx, tienes razón, me lo perdí. Arreglará eso – Glen

+0

@stakx: Sé que el OP mencionó que no puede modificar la clase. ¿Pero por qué no puede subclasificarlo y agregar allí el método doWork()? – slebetman

1

A no ser que se va por la ruta "todo es inmutable", que no se sienta muy bien con C++. no puedes evitar fácilmente los parámetros. La Biblioteca Estándar de C++ los usa, y lo que es lo suficientemente bueno para eso es lo suficientemente bueno para mí.

+0

Estoy totalmente en desacuerdo con la última afirmación: desafortunadamente la Biblioteca Estándar de C++ no está cerca de lo ideal (solo por mi cabeza: std :: cadena de locuras de miembros, flujos excesivamente elaborados, etc.) No quiero comenzar la guerra pero yo Supongo que no es la mejor idea justificar algo debido a un ejemplo bien conocido. –

+2

Por supuesto que tiene sus verrugas, pero en general me parece extremadamente potente y fácil de usar, solo deseo que mi propio código esté tan bien diseñado. –

0

En cuanto a su primer ejemplo: return value optimization a menudo permitirá que el objeto devuelto se cree directamente en el lugar, en lugar de tener que copiar el objeto. Todos los compiladores modernos hacen esto.

3

Cada compilador hace útil (optimización del valor de retorno) OVR si se habilitan optimizaciones, por lo tanto la siguiente manera efectiva no da lugar a la copia:

Expensive work() { 
    // ... no branched returns here 
    return Expensive(foo); 
} 

Expensive e = work(); 

En algunos casos, los compiladores pueden aplicar NRVO, llamado optimización valor de retorno, así:

Expensive work() { 
    Expensive e; // named object 
    // ... no branched returns here 
    return e; // return named object 
} 

sin embargo, esto no es exactamente fiable, sólo funciona en los casos más triviales y tendría que ser probado. Si no está listo para probar cada caso, solo use los parámetros de salida con referencias en el segundo caso.

2

OMI lo primero que debe hacerse es si la copia de ExpensiveCopy realmente es tan prohibitivo costoso. Y para responder eso, generalmente necesitarás un generador de perfiles. A menos que un generador de perfiles le diga que la copia realmente es un cuello de botella, simplemente escriba el código que sea más fácil de leer: ExpensiveCopy obj = doWork(param);.

Por supuesto, hay casos en los que los objetos no se pueden copiar por rendimiento u otras razones. Luego se aplica Neil's answer.

0

¿En qué plataforma estás trabajando?

La razón que pido es que muchas personas han sugerido Volver Optimización del valor, que es una optimización del compilador muy práctico presente en casi todos los compilador. Además, Microsoft e Intel implementan lo que denominan optimización de valor de retorno con nombre, que es aún más útil.

en la optimización del valor devuelto estándar de su estado de retorno es una llamada al constructor de un objeto, que le dice al compilador para eliminar los valores temporales (no necesariamente la operación de copia).

en la optimización del valor devuelto nombre que se pueden devolver un valor por su nombre y el compilador va a hacer lo mismo. La ventaja de NRVO es que puede realizar operaciones más complejas en el valor creado (como funciones de llamada en él) antes de devolverlo.

Si bien ninguno de estos realmente eliminar una copia caro si sus datos devuelto es muy grande, sí ayudan.

En términos de evitar la copia, la única manera real de hacerlo es con punteros o referencias porque su función necesita modificar los datos en el lugar donde desea que termine. Eso significa que probablemente quiera tener una parámetro de paso por referencia.

también que la figura debería señalar que la referencia pass-by-es muy común en el código de alto rendimiento para esta razón específica. Copiar datos puede ser increíblemente costoso, y a menudo es algo que las personas pasan por alto al optimizar su código.

2

Además de todos los comentarios aquí me gustaría mencionar que en C++ 0x que rara vez se tendría que utilizar el parámetro de salida con fines de optimización - a causa de Constructores Mover (ver here)

+0

De hecho, recientemente intenté configurar un sistema para admitir lecturas de archivos que usaban la semántica de movimiento una vez. Funcionó muy bien, pero me quemé al final debido a incompatibilidades con diferentes compiladores. Tuve que cambiarlo todo a parámetros que fue un verdadero fastidio. –

0

Por lo que yo puedo ver , las razones para preferir valores devueltos a parámetros externos son que es más claro y funciona con programación funcional pura (puede obtener algunas garantías agradables si una función depende solo de parámetros de entrada, devuelve un valor y no tiene efectos secundarios). La primera razón es estilística, y en mi opinión no es tan importante. El segundo no encaja bien con C++. Por lo tanto, no trataría de distorsionar nada para evitar los parámetros.

El hecho simple es que algunas funciones tienen que devolver varias cosas, y en la mayoría de los lenguajes esto sugiere parámetros. Common Lisp tiene multiple-value-bind y multiple-value-return, en el que el enlace proporciona una lista de símbolos y se devuelve una lista de valores. En algunos casos, una función puede devolver un valor compuesto, como una lista de valores que luego se deconstruirá, y no es gran cosa que una función de C++ devuelva std::pair. Devolver más de dos valores de esta manera en C++ se torna incómodo. Siempre es posible definir una estructura, pero definirla y crearla a menudo será más complicada que los parámetros.

En algunos casos, el valor de retorno se sobrecarga. En C, getchar() devuelve un int, con la idea de que hay más valores int que char (verdadero en todas las implementaciones que conozco, falso en algunos que puedo imaginar fácilmente), por lo que uno de los valores puede usarse para denotar de archivo. atoi() devuelve un número entero, ya sea el número entero representado por la cadena que se pasa o cero si no hay ninguno, por lo que devuelve lo mismo para "0" y "rana". (Si desea saber si hubo un valor int o no, use strtol(), que tiene un parámetro de salida.)

Siempre existe la técnica de lanzar una excepción en caso de error, pero no todos los valores de retorno múltiples son errores, y no todos los errores son excepcionales.

Por lo tanto, los valores de retorno sobrecargados causan problemas, los retornos de múltiples valores no son fáciles de usar en todos los idiomas, y los retornos únicos no siempre existen. Lanzar una excepción a menudo es inapropiado. El uso de parámetros es a menudo la solución más limpia.

0

Pregúntese por qué tiene un método que realiza el trabajo en este objeto costoso de copiar en primer lugar. Digamos que tienes un árbol, ¿enviarías el árbol a algún método de construcción o le darías al árbol su propio método de construcción? Situaciones como esta surgen constantemente cuando tienes un poco de diseño pero tienden a doblarse dentro de ti cuando lo tienes abajo.

Sé con practicidad que no siempre tenemos la posibilidad de cambiar todos los objetos, pero pasar los parámetros es una operación de efectos secundarios, y hace que sea mucho más difícil averiguar qué está pasando, y usted nunca tiene realmente para hacerlo (excepto como forzado trabajando dentro de marcos de código de otros).

A veces es más fácil, pero definitivamente no es deseable usarlo sin ningún motivo (si ha sufrido algunos proyectos grandes donde siempre hay media docena de parámetros sabrá a qué me refiero).

Cuestiones relacionadas