2010-02-07 11 views
15

Tengo una clase que acumula información sobre un conjunto de objetos y puede actuar como un funtor o como un iterador de salida. Esto me permite hacer cosas como:Impedir copias innecesarias de los objetos del functor C++

std::vector<Foo> v; 
Foo const x = std::for_each(v.begin(), v.end(), Joiner<Foo>()); 

y

Foo const x = std::copy(v.begin(), v.end(), Joiner<Foo>()); 

Ahora, en teoría, el compilador debe ser capaz de utilizar el copy elision and return-value optimizations por lo que sólo un único Joiner objeto necesita ser creado. En la práctica, sin embargo, la función hace una copia sobre la cual operar y luego copia eso de vuelta al resultado, incluso en versiones completamente optimizadas.

Si creo el funtor como un valor-I, el compilador crea dos copias extra en lugar de uno:

Joiner<Foo> joiner; 
Foo const x = std::copy(v.begin(), v.end(), joiner); 

Si torpemente fuerzo el tipo de plantilla a una referencia que pasa una referencia, pero luego hace una copia de todos modos y devuelve una referencia que cuelga a la (ahora destruido) copia temporal:

x = std::copy<Container::const_iterator, Joiner<Foo>&>(...)); 

que puede hacer las copias baratas utilizando una referencia al estado en lugar del propio estado en el funtor en el estilo de std::inserter, lo que lleva a algo ing así:

Foo output; 
std::copy(v.begin(), v.end(), Joiner<Foo>(output)); 

Pero esto hace que sea imposible utilizar el estilo "funcional" de los objetos inmutables, y en general no es tan agradable.

¿Hay alguna forma de alentar al compilador a eludir las copias temporales, o hacer que pase una referencia hasta el final y devolver esa misma referencia?

+0

Lo beneficioso o posible de RVO depende probablemente de su definición de 'Joiner'. Dicho esto, ¿estás realmente dispuesto a renunciar a la sintaxis simple y limpia 'Foo const x = std :: for_each (v.begin(), v.end(), Joiner ());' con algo posiblemente mucho más feo? – GManNickG

+0

for_each/copy debería devolver un Joiner , no un Foo, ¿verdad? Tu ejemplo me confunde. A menos que esté tratando de implicar que Joiner es convertible a Foo? –

+2

Escribí tres respuestas diferentes para demostrar que estás equivocado y finalmente entendí que no. Muy útil, gracias por la pregunta. +1. –

Respuesta

14

Ha tropezado con un comportamiento a menudo quejado con <algorithm>. No hay restricciones sobre lo que pueden hacer con el funtor, por lo que la respuesta a su pregunta es no: no hay forma de alentar al compilador a eludir las copias. No es (siempre) el compilador, es la implementación de la biblioteca. Simplemente les gusta pasar los funtores por valor (piense en std :: sort haciendo un qsort, pasando el functor por valor a llamadas recursivas, etc.).

También ha tropezado con la solución exacta que todo el mundo utiliza: haga que un functor mantenga una referencia al estado, por lo que todas las copias se refieren al mismo estado cuando se desee.

yo encontramos este irónico:

pero esto hace que sea imposible utilizar el estilo "funcional" de los objetos inmutables, y en general no es tan agradable.

... ya que toda esta pregunta se basa en que tiene un functor con estado complicado, donde la creación de copias es problemática. Si estuvieras usando objetos inmutables de estilo "funcional", esto no tendría importancia, las copias adicionales no serían un problema, ¿o sí?

+4

+1 por notar la ironía. – MSN

+0

¿Sabe si se ha hecho algún trabajo para solucionar esto en C++ 0x. Lo encontré muy molesto también. De hecho, la implementación de mi biblioteca (en ese momento) insistía en que los funtores fueran constructables por defecto, así que realmente no había manera de que pudiera tener ningún estado en ellos. – Omnifarious

+0

No, parece lo mismo en C++ 0x. Creo que es "por diseño" (lenguaje no restrictivo que otorga más libertad a los implementadores de la biblioteca) –

1

RVO es solo eso - return valor optimizado. La mayoría de los compiladores, hoy en día, lo tienen activado por defecto. Sin embargo, el argumento que pasa es no devolviendo un valor. Posiblemente no se puede esperar que una optimización encaje en todas partes.

se refieren a condiciones para elision copia se define claramente en 12,8, párrafo 15, punto 3.

cuando un objeto de clase temporal que no ha sido atado a una referencia (12.2) sería copiado a un objeto de clase con la misma cv-incondicional tipo, la operación de copia se puede omitir por construir el objeto temporal directamente en el objetivo de la omitido copia

[el énfasis es mío]

El LHS Foo es const cualificado, el temporal no es. En mi humilde opinión, esto excluye la posibilidad de copia-elisión.

+0

@Tim Sylvester: Actualicé mi respuesta. – dirkgently

+0

De hecho, declarar explícitamente el tipo de devolución como un valor no const del tipo de functor hace que RVO se active. Eso lo explica, aunque parece extraño que deba declarar una variable automática adicional para que el compilador optimice lejos una copia. En mi ejemplo original, el valor asignado no es el tipo de functor. Hubiera pensado que el valor de retorno temporal sin nombre podría ser el objetivo de un RVO ya que su tipo implícito sería el mismo que el parámetro r-value. –

+0

@Tim Sylvester: Gracias por consultar esto. Como marqué, * es * sobre tipos exactos y no puedo encontrar nada que diga que se realiza ninguna conversión (que IMO no tiene sentido en una operación de copia - está copiando valores entre objetos de _ tipo_ tipo). – dirkgently

3

Si usted tiene un compilador reciente (Al menos Visual Studio 2008 SP1 o GCC 4.4 creo) se puede utilizar std :: ref/std :: CREF

#include <string> 
#include <vector> 
#include <functional> // for std::cref 
#include <algorithm> 
#include <iostream> 

template <typename T> 
class SuperHeavyFunctor 
{ 
    std::vector<char> v500mo; 
    //ban copy 
    SuperHeavyFunctor(const SuperHeavyFunctor&); 
    SuperHeavyFunctor& operator=(const SuperHeavyFunctor&); 
public: 
    SuperHeavyFunctor():v500mo(500*1024*1024){} 
    void operator()(const T& t) const { std::cout << t << std::endl; } 
}; 

int main() 
{ 
    std::vector<std::string> v; v.push_back("Hello"); v.push_back("world"); 
    std::for_each(v.begin(), v.end(), std::cref(SuperHeavyFunctor<std::string>())); 
    return 0; 
} 

Editar: En realidad, la implementación de la MSVC10 reference_wrapper no parece saber cómo deducir el tipo de retorno del operador de objeto de función(). Tuve que derivar SuperHeavyFunctor de std::unary_function<T, void> para hacerlo funcionar.

+0

Intenté esto con 'boost :: ref' y no funciona. Con 'std :: tr1 :: ref' (MSVC9) funciona para los algoritmos que esperan un funtor como' for_each', pero no para aquellos que esperan un iterador como 'copy'. Tal vez debería abandonar el uso de 'copy' para este propósito. ¡Gracias! –

+0

Sí, lo siento, debería haber agregado que boost :: ref no funciona en este caso. Eso es porque boost :: reference_wrapper operator() no reenvía al operador de objeto de función subyacente() (boost :: reference_wrapper ni siquiera tiene un operador() :) Y sobre el objeto de función que decae en un iterador de salida. .. bueno, es la primera vez que escucho un diseño como ese. Suena bastante extraño. –

2

Sólo una nota rápida, for_each, acumulan, transformada(segunda forma), no ofrecen ninguna garantía de orden cuando se atraviesa el rango previsto.

Esto hace sentido para los implementadores para proporcionar versiones multiproceso/simultáneas de estas funciones.

Por lo tanto es razonable que el algoritmo sea capaz de proporcionar una instancia equivalente (una nueva copia) de la funtor aprobada en.

Tenga cuidado al hacer funtores con estado.

0

Para una solución que funcione con el código pre-C++ 11, puede considerar el uso de la función boost :: junto con boost :: ref (como boost::reference_wrapper alone doesn't has an overloaded operator(), a diferencia de std :: reference_wrapper que sí lo hace). Desde esta página http://www.boost.org/doc/libs/1_55_0/doc/html/function/tutorial.html#idp95780904, puede envolver doblemente su functor dentro de un boost :: ref luego un objeto boost :: function. Intenté con esa solución y funcionó a la perfección.

Para C++ 11, puede simplemente ir con std :: ref y hará el trabajo.

Cuestiones relacionadas