2010-03-23 19 views
40

Estoy intentando aprender C++ y tratando de comprender el retorno de objetos. Parece que veo 2 maneras de hacer esto y necesito entender cuál es la mejor práctica.Práctica recomendada de C++: devolución de referencia contra el objeto

Opción 1:

QList<Weight *> ret; 
Weight *weight = new Weight(cname, "Weight"); 
ret.append(weight); 
ret.append(c); 
return &ret; 

Opción 2:

QList<Weight *> *ret = new QList(); 
Weight *weight = new Weight(cname, "Weight"); 
ret->append(weight); 
ret->append(c); 
return ret; 

(por supuesto, no puede entender esto todavía tampoco).

¿Qué camino se considera la mejor práctica y se debe seguir?

+1

Ambas variantes se ven terriblemente mal (aunque solo la primera está realmente rota). –

+0

Si se trata de 'QList' de la biblioteca' Qt', entonces tienen propiedades especiales de recuento de recuento de referencias que no son del todo típicas para la biblioteca estándar u otros tipos regulares. – woolstar

Respuesta

46

La opción 1 está defectuosa. Cuando declara un objeto

QList<Weight *> ret; 

solo vive en el ámbito local. Se destruye cuando la función sale. Sin embargo, usted puede hacer este trabajo con

return ret; // no "&" 

Ahora, aunque ret se destruye, se hace una copia primero y pasa de nuevo a la persona que llama.

Esta es la metodología generalmente preferida. De hecho, la operación de copiar y destruir (que no logra nada, en realidad) suele ser elided, or optimized out y obtienes un programa rápido y elegante.

Opción 2 funciona, pero luego tiene un puntero al montón. Una forma de ver C++ es que el objetivo del lenguaje es evitar la administración manual de memoria, como esa. A veces uno desea administrar objetos en el montón, pero la opción 1 permite todavía que:

QList<Weight *> *myList = new QList<Weight *>(getWeights()); 

donde getWeights es su función de ejemplo. (En este caso, puede que tenga que definir un constructor de copia QList::QList(QList const &), pero al igual que en el ejemplo anterior, probablemente no se llamará).

Del mismo modo, probablemente deba evitar tener una lista de punteros. La lista debe almacenar los objetos directamente. Intente usar std::list ... la práctica con las características del lenguaje es más importante que practicar la implementación de estructuras de datos.

6

Utilice la opción n. ° 1 con un ligero cambio; en lugar de devolver una referencia al objeto creado localmente, devuelva su copia.

es decir return ret;

La mayoría de los compiladores de C++ realizan Return value optimization (RVO) para optimizar distancia del objeto temporal creado para contener el valor de retorno de una función.

+1

Puedo confirmar este comportamiento en g ++ sin ningún indicador: optimiza la declaración de devolución y simplemente devuelve la dirección al objeto en lugar de copiarlo (en general). –

+0

E incluso sin RVO 'QList' está haciendo copy-on-write internamente de todos modos, por lo que una copia de un temporal es más o menos gratuita. –

5

En general, nunca debe devolver una referencia o un puntero. En su lugar, devuelva una copia del objeto o devuelva una clase de puntero inteligente que posee el objeto. En general, use la asignación de almacenamiento estático a menos que el tamaño varíe en tiempo de ejecución o la duración del objeto requiera que se asigne mediante la asignación dinámica de almacenamiento.

Como se ha señalado, su ejemplo de devolución por referencia devuelve una referencia a un objeto que ya no existe (ya que se ha salido del alcance) y, por lo tanto, invoca un comportamiento indefinido.Esta es la razón por la que nunca deberías devolver una referencia. Nunca debe devolver un puntero sin procesar, porque la propiedad no está clara.

También debe tenerse en cuenta que el retorno por valor es increíblemente barato debido a la optimización del valor de retorno (RVO), y pronto será incluso más económico debido a la introducción de referencias rvalue.

+0

E incluso sin semántica de movimiento o RVO 'QList' está haciendo copy-on-write internamente de todos modos, por lo que una copia de un temporal es más o menos gratuita. –

1

pasando & devolver referencias invita a responsabilidad.! Debe tener cuidado de que cuando modifique algunos valores no haya efectos secundarios. lo mismo en el caso de los punteros. Te recomiendo que vuelvas a objetos. (BUT IT VERY-MUCH DEPENDS ON WHAT EXACTLY YOU WANT TO DO)

En su Opción 1, usted devuelve la dirección y eso es MUY malo ya que esto podría conducir a un comportamiento indefinido. (Se cancela la asignación ret, pero la dirección del y'll ret de acceso en la función llamada)

a fin de utilizar return ret;

1

Por lo general es una mala práctica de asignar memoria que tiene que ser liberado en otro lugar. Esa es una de las razones por las que tenemos C++ en lugar de solo C. (Pero los programadores expertos escribían código orientado a objetos en C mucho antes de la Era de Stroustrup). Los objetos bien construidos tienen operadores de copia y asignación rápidos (a veces usando recuento de referencias) , y automáticamente liberan la memoria que "poseen" cuando se liberan y su DTOR se llama automáticamente. Así que puede tirarlos alegremente, en lugar de usar punteros para ellos.

Por lo tanto, dependiendo de lo que quiera hacer, la mejor práctica es muy probable "ninguna de las anteriores". Siempre que sienta la tentación de usar "nuevo" en cualquier lugar que no sea CTOR, piense en ello. Probablemente no quieras usar "nuevo" en absoluto. Si lo hace, el puntero resultante probablemente debería estar envuelto en algún tipo de puntero inteligente. Puede ir por semanas y meses sin llamar "nuevo", porque el "nuevo" y "eliminar" se tienen en cuenta en clases estándar o plantillas de clase como std :: list y std :: vector.

Una excepción es cuando está utilizando una biblioteca antigua como OpenCV que a veces requiere que cree un nuevo objeto, y le pasa un puntero al sistema, que toma posesión.

Si QList y peso están correctamente escritas para limpiar ellos mismos en sus DTORS, lo que quiere es,

QList<Weight> ret(); 
Weight weight(cname, "Weight"); 
ret.append(weight); 
ret.append(c); 
return ret; 
1

Como ya se ha mencionado, es mejor evitar la asignación de memoria que se debe cancelar la asignación a otra parte. Esto es lo que yo prefiero hacer (... estos días):

void someFunc(QList<Weight *>& list){ 
    // ... other code 
    Weight *weight = new Weight(cname, "Weight"); 
    list.append(weight); 
    list.append(c); 
} 

// ... later ... 

QList<Weight *> list; 
someFunc(list) 

Incluso mejor - new evitar por completo y utilizando std::vector:

void someFunc(std::vector<Weight>& list){ 
    // ... other code 
    Weight weight(cname, "Weight"); 
    list.push_back(weight); 
    list.push_back(c); 
} 

// ... later ... 

std::vector<Weight> list; 
someFunc(list); 

siempre se puede utilizar un bool o enum si quieres para devolver una bandera de estado.

+0

El primero de esos fragmentos de código probablemente sea incorrecto. Si QList está escrito correctamente, el fragmento tiene una pérdida de memoria.Si el DTOR para QList está escrito para eliminar los objetos de peso a los que apunta (no es la mejor práctica), entonces el primer fragmento es correcto. –

+0

@Jive, nunca he tocado una QList en toda mi vida. Solo estaba ilustrando que prefiero asignar contenedores en la pila y pasarlos como argumentos en lugar de tener un puntero devuelto a mí. –

-3

Todas estas son respuestas válidas, evite los punteros, use copia constructores, etc. A menos que necesite crear un programa que necesite un buen rendimiento, en mi experiencia la mayoría de los problemas relacionados con el rendimiento son con los constructores de copia y causado por ellos. (Y los punteros inteligentes no son mejores en este campo, eliminaría todo mi código de impulso y haría la eliminación manual porque tomaría demasiados milisegundos para hacer su trabajo).

Si está creando un programa "simple" (aunque "simple" significa que debe ir con java o C#), utilice constructores de copia, evite los punteros y utilice punteros inteligentes para desasignar la memoria utilizada, si está creando un programa complejo o necesita un buen rendimiento, utilice punteros por todas partes y evite copiar constructores (si es posible), simplemente cree su conjunto de reglas para eliminar punteros y use valgrind para detectar fugas de memoria,

Tal vez lo haga obtener algunos puntos negativos, pero creo que tendrá que obtener la imagen completa para tomar sus decisiones de diseño.

Creo que decir "si devuelve punteros, su diseño es incorrecto" es poco engañoso. Los parámetros de salida tienden a ser confusos porque no es una opción natural para "devolver" los resultados.

Sé que esta pregunta es antigua, pero no veo ningún otro argumento que señale el rendimiento general de las opciones de diseño.

1

Según la experiencia, no utilice punteros simples, ya que puede olvidar fácilmente agregar los mecanismos de destrucción adecuados.

Si se quiere evitar la copia, usted puede ir para la aplicación de la clase peso con el constructor de copia y copiar operador deshabilitado:

class Weight { 
protected: 
    std::string name; 
    std::string desc; 
public: 
    Weight (std::string n, std::string d) 
     : name(n), desc(d) { 
     std::cout << "W c-tor\n"; 
    } 
    ~Weight (void) { 
     std::cout << "W d-tor\n"; 
    } 

    // disable them to prevent copying 
    // and generate error when compiling 
    Weight(const Weight&); 
    void operator=(const Weight&); 
}; 

Entonces, para la clase que implementa el contenedor, utilice shared_ptr o unique_ptr a implementar el miembro de datos:

template <typename T> 
class QList { 
protected: 
    std::vector<std::shared_ptr<T>> v; 
public: 
    QList (void) { 
     std::cout << "Q c-tor\n"; 
    } 
    ~QList (void) { 
     std::cout << "Q d-tor\n"; 
    } 

    // disable them to prevent copying 
    QList(const QList&); 
    void operator=(const QList&); 

    void append(T& t) { 
     v.push_back(std::shared_ptr<T>(&t)); 
    } 
}; 

Su función para agregar un elemento haría uso o Devolvería la optimización del valor y no llamaría al constructor de copia (que no está definido):

QList<Weight> create (void) { 
    QList<Weight> ret; 
    Weight& weight = *(new Weight("cname", "Weight")); 
    ret.append(weight); 
    return ret; 
} 

Al añadir un elemento, el dejar que el contenedor de tomar la propiedad del objeto, por lo que no desasignar que:

QList<Weight> ql = create(); 
ql.append(*(new Weight("aname", "Height"))); 

// this generates segmentation fault because 
// the object would be deallocated twice 
Weight w("aname", "Height"); 
ql.append(w); 

O, mejor, obligar al usuario a pasar su implementación QList punteros inteligentes: Sólo

void append(std::shared_ptr<T> t) { 
    v.push_back(t); 
} 

Y fuera de clase QList podrás usarlo como:

Weight * pw = new Weight("aname", "Height"); 
ql.append(std::shared_ptr<Weight>(pw)); 

Al usar shared_ptr también podría 'tomar' objetos de la colección, hacer copias, eliminarlos de la colección pero usarlos localmente, detrás de las escenas sería el único objeto único.

Cuestiones relacionadas