c++ - ¿Cuál es el mejor tipo de retorno de puntero inteligente para una función de fábrica?
c++11 smart-pointers (4)
¿Cuál sería el mejor tipo de devolución para la función de fábrica?
unique_ptr
sería lo mejor. Evita fugas accidentales y el usuario puede liberar la propiedad del puntero o transferir la propiedad a un shared_ptr
(que tiene un constructor para ese fin), si quiere usar un esquema de propiedad diferente.
¿Cuál es el mejor tipo de parámetro para la función de utilidad?
Una referencia, a menos que el flujo del programa sea tan intrincado que el objeto pueda destruirse durante la llamada a la función, en cuyo caso shared_ptr
o weak_ptr
. (En cualquier caso, puede referirse a una clase base y agregar calificadores const
si así lo desea).
¿Cuál es el mejor tipo de parámetro para la función que mantiene una referencia al objeto?
shared_ptr
o unique_ptr
, si desea que asuma la responsabilidad de la duración del objeto y no se preocupe por ello. Un puntero o referencia sin procesar, si puede (de manera sencilla y confiable) hacer arreglos para que el objeto sobreviva a todo lo que lo usa.
Con respecto a los punteros inteligentes y las nuevas características de C ++ 11/14, me pregunto cuáles serían los valores de retorno de las mejores prácticas y los tipos de parámetros de funciones para las clases que tienen estas características:
Una función de fábrica (fuera de la clase) que crea objetos y los devuelve a los usuarios de la clase. (Por ejemplo, abrir un documento y devolver un objeto que se puede usar para acceder al contenido).
Funciones de utilidad que aceptan objetos de las funciones de fábrica, úselas, pero no tome posesión. (Por ejemplo, una función que cuenta el número de palabras en el documento).
Funciones que mantienen una referencia al objeto una vez que regresan (como un componente de UI que toma una copia del objeto para que pueda dibujar el contenido en la pantalla según sea necesario).
¿Cuál sería el mejor tipo de devolución para la función de fábrica?
- Si se trata de un puntero sin formato, el usuario tendrá que
delete
correctamente, lo que es problemático. - Si devuelve un
unique_ptr<>
, el usuario no puede compartirlo si lo desea. - Si es un
shared_ptr<>
, ¿tendré que pasar los tiposshared_ptr<>
todas partes? Esto es lo que estoy haciendo ahora y está causando problemas ya que obtengo referencias cíclicas, evitando que los objetos se destruyan automáticamente.
¿Cuál es el mejor tipo de parámetro para la función de utilidad?
- Imagino que pasar por referencia evitará incrementar innecesariamente el número de referencia de un puntero inteligente, pero ¿hay algún inconveniente en esto? El principal que me viene a la mente es que me impide pasar clases derivadas a funciones que toman parámetros del tipo de clase base.
- ¿Hay alguna manera de que pueda dejar en claro a la persona que llama que NO copiará el objeto? (Lo ideal es que el código no se compile si el cuerpo de la función intenta copiar el objeto).
- ¿Hay alguna manera de hacerlo independiente del tipo de puntero inteligente en uso? (¿Tal vez tomando un puntero sin formato?)
- ¿Es posible tener un parámetro
const
para dejar en claro que la función no modificará el objeto sin romper la compatibilidad del puntero inteligente?
¿Cuál es el mejor tipo de parámetro para la función que mantiene una referencia al objeto?
- Supongo que
shared_ptr<>
es la única opción aquí, lo que probablemente significa que la clase de fábrica debe devolver unshared_ptr<>
también, ¿no?
Aquí hay un código que compila y con suerte ilustra los puntos principales.
#include <iostream>
#include <memory>
struct Document {
std::string content;
};
struct UI {
std::shared_ptr<Document> doc;
// This function is not copying the object, but holding a
// reference to it to make sure it doesn''t get destroyed.
void setDocument(std::shared_ptr<Document> newDoc) {
this->doc = newDoc;
}
void redraw() {
// do something with this->doc
}
};
// This function does not need to take a copy of the Document, so it
// should access it as efficiently as possible. At the moment it
// creates a whole new shared_ptr object which I feel is inefficient,
// but passing by reference does not work.
// It should also take a const parameter as it isn''t modifying the
// object.
int charCount(std::shared_ptr<Document> doc)
{
// I realise this should be a member function inside Document, but
// this is for illustrative purposes.
return doc->content.length();
}
// This function is the same as charCount() but it does modify the
// object.
void appendText(std::shared_ptr<Document> doc)
{
doc->content.append("hello");
return;
}
// Create a derived type that the code above does not know about.
struct TextDocument: public Document {};
std::shared_ptr<TextDocument> createTextDocument()
{
return std::shared_ptr<TextDocument>(new TextDocument());
}
int main(void)
{
UI display;
// Use the factory function to create an instance. As a user of
// this class I don''t want to have to worry about deleting the
// instance, but I don''t really care what type it is, as long as
// it doesn''t stop me from using it the way I need to.
auto doc = createTextDocument();
// Share the instance with the UI, which takes a copy of it for
// later use.
display.setDocument(doc);
// Use a free function which modifies the object.
appendText(doc);
// Use a free function which doesn''t modify the object.
std::cout << "Your document has " << charCount(doc)
<< " characters./n";
return 0;
}
La mayoría de las otras respuestas lo cubren, pero @TC está relacionado con algunas pautas realmente buenas que me gustaría resumir aquí:
Función de fábrica
Una fábrica que produce un tipo de referencia debe devolver un
unique_ptr
por defecto, o unshared_ptr
si la propiedad se va a compartir con la fábrica. - GotW # 90
Como han señalado otros, usted como destinatario de unique_ptr
puede convertirlo a shared_ptr
si lo desea.
Parámetros de función
No pase un puntero inteligente como parámetro de función a menos que desee usar o manipular el puntero inteligente en sí, como compartir o transferir la propiedad. Prefiere pasar objetos por valor,
*
, o&
, no por un puntero inteligente. - GotW # 91
Esto se debe a que cuando pasa por el puntero inteligente, incrementa el contador de referencia al inicio de la función y lo disminuye al final. Estas son operaciones atómicas, que requieren sincronización a través de múltiples hilos / procesadores, por lo que en el código de múltiples subprocesos la penalización de velocidad puede ser bastante alta.
Cuando estás en la función, el objeto no va a desaparecer porque la persona que llama todavía tiene una referencia (y no puede hacer nada con el objeto hasta que tu función regrese ) por lo que incrementar el recuento de referencias no tiene sentido si no estás va a guardar una copia del objeto después de que la función regrese.
Para funciones que no toman posesión del objeto :
Use un
*
si necesita expresar nulo (sin objeto); de lo contrario, prefiera usar un&
; y si el objeto es solo de entrada, escribaconst widget*
oconst widget&
. - GotW # 91
Esto no obliga a su interlocutor a utilizar un tipo de puntero inteligente en particular; cualquier puntero inteligente se puede convertir en un puntero normal o una referencia. Por lo tanto, si su función no necesita conservar una copia del objeto ni tomar posesión del mismo, utilice un puntero sin formato. Como se mencionó anteriormente, el objeto no desaparecerá en el medio de su función porque la persona que llama todavía se está aferrando a él (excepto en circunstancias especiales, que usted ya sabría si esto es un problema para usted).
Para funciones que se apropian del objeto :
Exprese una función de "sumidero" utilizando un parámetro de valor
unique_ptr
.
void f( unique_ptr<widget> );
Esto deja en claro que la función toma posesión del objeto, y es posible pasarle los punteros sin procesar que podría tener del código heredado.
Para funciones que toman propiedad compartida del objeto :
Exprese que una función almacenará y compartirá la propiedad de un objeto Heap usando un parámetro de valor
shared_ptr
. - GotW # 91
Creo que estas pautas son muy útiles. Lea las páginas de donde provienen las citas para obtener más antecedentes y una explicación detallada, vale la pena.
Voy a responder en orden inverso para comenzar con los casos simples.
Funciones de utilidad que aceptan objetos de las funciones de fábrica, úselas, pero no tome posesión. (Por ejemplo, una función que cuenta el número de palabras en el documento).
Si llama a una función de fábrica, siempre asume la propiedad del objeto creado por la propia definición de una función de fábrica. Creo que lo que quieres decir es que algún otro cliente primero obtiene un objeto de la fábrica y luego desea pasarlo a la función de utilidad que no se apropia de la propiedad.
En este caso, la función de utilidad no debe preocuparse por cómo se gestiona la propiedad del objeto en el que opera. Simplemente debería aceptar una referencia (probablemente const
) o, si "ningún objeto" es una condición válida, un puntero sin conexión que no sea propietario. Esto minimizará el acoplamiento entre sus interfaces y hará que la función de utilidad sea más flexible.
Funciones que mantienen una referencia al objeto una vez que regresan (como un componente de UI que toma una copia del objeto para que pueda dibujar el contenido en la pantalla según sea necesario).
Estos deberían tomar un std::shared_ptr
por valor . Esto deja en claro, a partir de la firma de la función, que asumen la propiedad compartida del argumento.
A veces, también puede ser significativo tener una función que asuma la propiedad exclusiva de su argumento (los constructores vienen a la mente). Esos deberían tomar un std::unique_ptr
por valor (o por referencia rvalue) que también hará que la semántica desaparezca de la firma.
Una función de fábrica (fuera de la clase) que crea objetos y los devuelve a los usuarios de la clase. (Por ejemplo, abrir un documento y devolver un objeto que se puede usar para acceder al contenido).
Este es el más difícil ya que hay buenos argumentos para ambos, std::unique_ptr
y std::shared_ptr
. Lo único claro es que devolver un puntero sin dueño no es bueno.
Devolver un std::unique_ptr
es liviano (sin sobrecarga en comparación con devolver un puntero sin procesar) y transmite la semántica correcta de una función de fábrica. Quien llamó a la función obtiene propiedad exclusiva sobre el objeto fabricado. Si es necesario, el cliente puede construir un std::shared_ptr
de std::unique_ptr
a cambio de una asignación de memoria dinámica.
Por otro lado, si el cliente va a necesitar un std::shared_ptr
todos modos, sería más eficiente tener el uso de fábrica std::make_shared
para evitar la asignación de memoria dinámica adicional. Además, hay situaciones en las que simplemente debe usar std::shared_ptr
por ejemplo, si el destructor del objeto administrado no es virtual
y el puntero inteligente debe convertirse en un puntero inteligente a una clase base. Pero un std::shared_ptr
tiene más sobrecarga que un std::unique_ptr
por lo tanto, si este último es suficiente, preferimos evitarlo si es posible.
Por lo tanto, en conclusión, se me ocurre la siguiente pauta:
- Si necesita un eliminador personalizado, devuelva un
std::shared_ptr
. - De lo contrario, si cree que la mayoría de sus clientes van a necesitar un
std::shared_ptr
todos modos, utilice el potencial de optimización destd::make_shared
. - De lo contrario, devuelve un
std::unique_ptr
.
Por supuesto, puede evitar el problema proporcionando dos funciones de fábrica, una que devuelva un std::unique_ptr
y otra que devuelva un std::shared_ptr
para que cada cliente pueda usar lo que mejor se adapte a sus necesidades. Si necesita esto con frecuencia, creo que puede abstraer la mayor parte de la redundancia con alguna meta-programación inteligente de plantillas.
Yo devolvería un unique_ptr
por valor en la mayoría de las situaciones. La mayoría de los recursos no deben compartirse, ya que eso hace que sea difícil razonar sobre sus vidas. Por lo general, puede escribir su código de tal manera que evite la propiedad compartida. En cualquier caso, puede hacer un shared_ptr
desde unique_ptr
, por lo que no es como si estuviera limitando sus opciones.