¿Cómo devolver punteros inteligentes (shared_ptr), por referencia o por valor?

94

Digamos que tengo una clase con un método que devuelve un shared_ptr.

¿Cuáles son los posibles beneficios e inconvenientes de devolverlo por referencia o por valor?

Dos posibles pistas:

  • Destrucción temprana de objetos. Si devuelvo la shared_ptrreferencia by (const), el contador de referencia no se incrementa, por lo que incurro en el riesgo de que el objeto se elimine cuando se sale del alcance en otro contexto (por ejemplo, otro hilo). ¿Es esto correcto? ¿Qué pasa si el entorno es de un solo subproceso, puede suceder esta situación también?
  • Costo. El paso por valor ciertamente no es gratuito. ¿Vale la pena evitarlo siempre que sea posible?

Gracias a todos.

Vincenzo Pii
fuente

Respuestas:

114

Devuelve 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. Eso por sí solo debería ser razón suficiente para no regresar por referencia. Las interfaces deben ser sólidas.

La preocupación por el costo es hoy en día discutible gracias a la optimización del valor de retorno (RVO), por lo que no incurrirá en una secuencia de incremento-incremento-decremento o algo así en los compiladores modernos. Entonces, la mejor manera de devolver un shared_ptres simplemente regresar por valor:

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

Esta es una oportunidad RVO obvia para los compiladores modernos de C ++. Sé a ciencia cierta que los compiladores de Visual C ++ implementan RVO incluso cuando todas las optimizaciones están desactivadas. Y con la semántica de movimientos 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 un artículo que argumenta a favor de la devolución por valor. Reproduzco un fragmento aquí; Le recomiendo que lea el artículo completo:

Sea honesto: ¿cómo le hace sentir el siguiente código?

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

Francamente, aunque debería saberlo mejor, me pone nervioso. En principio, cuando get_names() regrese, tenemos que copiar una vectorde strings. Luego, necesitamos copiarlo nuevamente cuando inicializamos names, y necesitamos destruir la primera copia. Si hay N strings en el vector, cada copia podría requerir hasta N + 1 asignaciones de memoria y una gran cantidad de accesos de datos no amigables con la caché> a medida que se copia el contenido de la cadena.

En lugar de enfrentarme a ese tipo de ansiedad, a menudo recurro al paso por referencia para evitar copias innecesarias:

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

Desafortunadamente, este enfoque está lejos de ser ideal.

  • El código creció un 150%
  • Hemos tenido que abandonar constporque estamos mutando nombres.
  • Como a los programadores funcionales les gusta recordarnos, 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 una semántica de valores estricta para los nombres.

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

En silico
fuente
No sé si diría que RVO hace que la pregunta sea discutible, ya que regresar por referencia definitivamente hace que RVO sea imposible.
Edward Strange
@CrazyEddie: Es cierto, esa es una de las razones por las que recomiendo que el OP vuelva por valor.
In silico
¿La regla RVO, permitida por el estándar, prevalece sobre las reglas sobre las relaciones de sincronización / ocurre antes, garantizadas por el estándar?
edA-qa mort-ora-y
1
@ edA-qa mort-ora-y: RVO está explícitamente permitido incluso si tiene efectos secundarios. Por ejemplo, si tiene una cout << "Hello World!";declaración en un constructor de copia y predeterminado, no verá dos Hello World!s cuando RVO esté en vigor. Sin embargo, esto no debería ser un problema para los punteros inteligentes diseñados correctamente, incluso con la sincronización.
In silico
23

Con respecto a cualquier puntero inteligente (no solo shared_ptr), no creo que sea aceptable devolver una referencia a uno, y dudaría mucho en pasarlos por referencia o puntero sin formato. ¿Por qué? Porque no puede estar seguro de que no se copiará superficialmente a través de una referencia más adelante. Su primer punto define la razón por la que esto debería ser una preocupación. Esto puede suceder incluso en un entorno de un solo subproceso. No necesita acceso simultáneo a los datos para poner semántica de copia incorrecta en sus programas. Realmente no controlas lo que hacen tus usuarios con el puntero una vez que lo pasas, así que no fomentes el mal uso dando a los usuarios de la API suficiente cuerda 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, ¡no use un puntero inteligente! Pero más allá de esto, también necesitará examinar la arquitectura de concurrencia 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 la llegada de las características más modernas en C ++, modificaría mi respuesta para que acepte mejor los casos en los que simplemente ha escrito una lambda que nunca vive fuera del alcance de la función de llamada, y no lo es copiado en otro lugar. Aquí, si desea ahorrar la mínima sobrecarga de copiar un puntero compartido, sería justo y seguro. ¿Por qué? Porque puede garantizar que la referencia nunca será mal utilizada.

San Jacinto
fuente