2012-05-17 22 views
61

Digamos que tengo una clase con un método que devuelve shared_ptr.Cómo devolver punteros inteligentes (shared_ptr), por referencia o por valor?

¿Cuáles son los posibles beneficios y desventajas de devolverlo por referencia o por valor?

Dos pistas posibles:

  • destrucción de objetos temprana. Si devuelvo la referencia shared_ptr por (const), el contador de referencia no se incrementa, por lo que incurro en el riesgo de que se elimine el objeto cuando sale del alcance en otro contexto (por ejemplo, otro hilo). ¿Es esto correcto? ¿Qué sucede si el entorno tiene un solo hilo? ¿Puede ocurrir también esta situación?
  • Costo. Pass-by-value ciertamente no es gratis. ¿Vale la pena evitarlo siempre que sea posible?

Gracias a todos.

Respuesta

80

Devuelva los punteros inteligentes por valor.

Como ha dicho, si lo devuelve por referencia, no aumentará correctamente el recuento de referencias, lo que abre el riesgo de eliminar algo en el momento inadecuado. Solo eso debería ser motivo suficiente para no regresar por referencia. Las interfaces deben ser robustas.

La preocupación por el costo es actualmente discutible gracias a return value optimization (RVO), por lo que no incurrirá en una secuencia de incremento-incremento-decremento o algo así en los compiladores modernos. Así que la mejor manera de devolver un shared_ptr es simplemente volver por valor:

shared_ptr<T> Foo() 
{ 
    return shared_ptr<T>(/* acquire something */); 
}; 

Esta es una oportunidad RVO-muertos obvio para los modernos compiladores de C++. Sé con certeza que los compiladores de Visual C++ implementan RVO incluso cuando todas las optimizaciones están desactivadas. Y con la semántica del movimiento de C++ 11, esta preocupación es aún menos relevante. (Pero la única forma de estar seguro es perfilar y experimentar)

Si aún no está convencido, Dave Abrahams tiene an article que hace un argumento para devolver por valor. Reproduzco un fragmento aquí; Le recomiendo que lea el artículo completo:

Sea sincero: ¿cómo se siente el siguiente código?

std::vector<std::string> get_names(); 
... 
std::vector<std::string> const names = get_names(); 

Francamente, aunque debería saber mejor, me pone nervioso. En principio, cuando devuelve get_names() , tenemos que copiar un vector de string s. Entonces, tenemos que copiarlo nuevamente cuando inicialicemos names, y tenemos que destruir la primera copia. Si hay N string s en el vector, cada copia podría requerir tantas como N + 1 asignaciones de memoria y una gran cantidad de accesos a datos no compatibles con la memoria caché> a medida que se copian los contenidos de la cadena.

En lugar de enfrentarse a ese tipo de ansiedad, a menudo he vuelto a caer en el paso por referencia para evitar copias innecesarias :

get_names(std::vector<std::string>& out_param); 
... 
std::vector<std::string> names; 
get_names(names); 

Por desgracia, este enfoque está lejos de ser ideal.

  • El código creció en un 150%
  • Hemos tenido que dejar const -ness porque estamos mutando nombres.
  • Como los programadores funcionales nos recuerdan, la mutación hace que el código sea más complejo para razonar al socavar la transparencia referencial y el razonamiento ecuacional.
  • Ya no tenemos semántica de valor estricto para los nombres.

¿Pero es realmente necesario ensuciar nuestro código de esta manera para ganar eficiencia? Afortunadamente, la respuesta resulta ser no (y especialmente no si está usando C++ 0x).

+0

No sé si yo diría que RVO hace que la pregunta sea discutible ya que regresar por referencia hace que RVO sea imposible. –

+0

@CrazyEddie: Es cierto que esa es una de las razones por las que recomiendo que el OP devuelva por valor. –

+0

¿La regla de RVO, permitida por el estándar, prevalece sobre las reglas sobre sincronización/relaciones de pase previo garantizadas por el estándar? –

17

En cuanto cualquier puntero inteligente (no sólo shared_ptr), no creo que es siempre aceptable para devolver una referencia a uno, y yo estaría muy reacios a pasarlas alrededor por referencia o puntero del crudo. ¿Por qué? Porque no puede estar seguro de que no se copiará superficialmente a través de una referencia posterior. Su primer punto define la razón por la cual esto debería ser una preocupación. Esto puede suceder incluso en un entorno de subproceso único. No necesita acceso simultáneo a datos para poner semántica de copia incorrecta en sus programas. En realidad, no controlas lo que hacen los usuarios con el puntero una vez que lo pasas, así que no animes a usarlo incorrectamente para que los usuarios de API tengan suficiente cuerda como para ahorcarse.

En segundo lugar, observe la implementación de su puntero inteligente, si es posible. La construcción y la destrucción deberían ser casi insignificantes. Si esta sobrecarga no es aceptable, ¡entonces no use un puntero inteligente! Pero más allá de esto, también deberá examinar la arquitectura de simultaneidad que tiene, porque el acceso mutuamente exclusivo al mecanismo que rastrea los usos del puntero lo ralentizará más que la mera construcción del objeto shared_ptr.

Editar, 3 años después: con el advenimiento de las características más modernas en C++, modificaría mi respuesta para ser más receptivo a los casos cuando simplemente has escrito una lambda que nunca vive fuera del alcance de la función de llamada, y no se copia en otro lado. Aquí, si quisiera ahorrar la mínima carga general de copiar un puntero compartido, sería justo y seguro. ¿Por qué? Porque puede garantizar que la referencia nunca será mal utilizada.

Cuestiones relacionadas