c++ - how - malloc & placement nuevo vs. nuevo
destroy object c++ (11)
He estado investigando esto durante los últimos días, y hasta ahora no he encontrado nada convincente que no sean argumentos dogmáticos o apelaciones a la tradición (es decir, "¡es la forma de C ++!" ).
Si estoy creando una matriz de objetos, ¿cuál es el motivo convincente (que no sea la facilidad) para usarlo?
#define MY_ARRAY_SIZE 10
// ...
my_object * my_array=new my_object [MY_ARRAY_SIZE];
for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i]=my_object(i);
encima
#define MEMORY_ERROR -1
#define MY_ARRAY_SIZE 10
// ...
my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
if (my_object==NULL) throw MEMORY_ERROR;
for (int i=0;i<MY_ARRAY_SIZE;++i) new (my_array+i) my_object (i);
Por lo que puedo decir, este último es mucho más eficiente que el primero (ya que no inicializar memoria a algunos valores no aleatorios / llamar constructores por defecto innecesariamente), y la única diferencia es el hecho de que uno se limpia con :
delete [] my_array;
y el otro con el que se limpia:
for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();
free(my_array);
Estoy fuera por una razón convincente . Apela al hecho de que es C ++ (no C) y, por lo tanto, no debe utilizarse malloc
y free
no es, por lo que puedo decir, convincente tanto como es dogmático . ¿Hay algo que me falta que hace que el new []
superior al malloc
?
Es decir, lo mejor que puedo decir es que ni siquiera puedes usar new []
nada nuevo para hacer una serie de elementos que no tienen un constructor predeterminado, sin parámetros, mientras que el método malloc
puede ser utilizado.
Estoy fuera por una razón convincente.
Depende de cómo defines "convincente". Muchos de los argumentos que hasta ahora has rechazado son ciertamente convincentes para la mayoría de los programadores de C ++, ya que tu sugerencia no es la forma estándar de asignar matrices desnudas en C ++.
El simple hecho es este: sí, puedes hacer absolutamente las cosas de la manera que describes. No hay ninguna razón para que lo que describes no funcione .
Pero, de nuevo, puede tener funciones virtuales en C. Puede implementar clases y herencia en C simple, si le dedica tiempo y esfuerzo. Esos son completamente funcionales también.
Por lo tanto, lo que importa no es si algo puede funcionar. Pero más sobre cuáles son los costos . Es mucho más propenso a errores implementar herencia y funciones virtuales en C que en C ++. Existen múltiples formas de implementarlo en C, lo que conduce a implementaciones incompatibles. Mientras que, debido a que son características de lenguaje de primera clase de C ++, es muy poco probable que alguien implemente manualmente lo que ofrece el lenguaje. Por lo tanto, la herencia de todos y las funciones virtuales pueden cooperar con las reglas de C ++.
Lo mismo vale para esto. Entonces, ¿cuáles son las ganancias y las pérdidas de la administración manual malloc / array libre?
No puedo decir que nada de lo que voy a decir constituye un "motivo convincente" para usted. Más bien lo dudo, ya que parece que has tomado una decisión. Pero para el registro:
Actuación
Usted reclama lo siguiente:
Por lo que puedo decir, este último es mucho más eficiente que el primero (ya que no inicializar memoria a algunos valores no aleatorios / llamar constructores por defecto innecesariamente), y la única diferencia es el hecho de que uno se limpia con :
Esta afirmación sugiere que la ganancia de eficiencia se basa principalmente en la construcción de los objetos en cuestión. Es decir, a qué constructores se llama. La declaración presupone que no desea llamar al constructor predeterminado; que use un constructor predeterminado solo para crear la matriz, luego use la función de inicialización real para poner los datos reales en el objeto.
Bueno ... ¿y si eso no es lo que quieres hacer? ¿Qué sucede si lo que quieres hacer es crear una matriz vacía , una que está construida por defecto? En este caso, esta ventaja desaparece por completo.
Fragilidad
Supongamos que cada objeto en la matriz necesita tener un constructor especializado o algo llamado, de modo que inicializar la matriz requiere este tipo de cosas. Pero considera tu código de destrucción:
for (int i=0;i<MY_ARRAY_SIZE;++i) my_array[i].~T();
Para un caso simple, esto está bien. Usted tiene una variable macro o const que dice cuántos objetos tiene. Y recorre cada elemento para destruir los datos. Eso es genial para un simple ejemplo.
Ahora considera una aplicación real, no un ejemplo. ¿En cuántos lugares diferentes creará una matriz? Docenas? Cientos? Todos y cada uno necesitarán tener su propio bucle for para inicializar la matriz. Todos y cada uno necesitarán tener su propio bucle for para destruir la matriz.
Escribe incorrectamente esto incluso una vez, y puedes dañar la memoria. O no borrar algo O cualquier cantidad de otras cosas horribles.
Y aquí hay una pregunta importante: para un conjunto determinado, ¿dónde guardas el tamaño ? ¿Sabes cuántos elementos asignados para cada matriz que creas? Cada matriz probablemente tendrá su propia forma de saber cuántos elementos almacena. Por lo tanto, cada bucle de destructor deberá recuperar estos datos correctamente. Si se pone mal ... boom.
Y luego tenemos seguridad de excepción, que es una nueva lata de gusanos. Si uno de los constructores lanza una excepción, los objetos construidos previamente deben ser destruidos. Tu código no hace eso; no es a prueba de excepciones.
Ahora, considera la alternativa:
delete[] my_array;
Esto no puede fallar Siempre destruirá todos los elementos. Realiza un seguimiento del tamaño de la matriz, y es a prueba de excepciones. Por lo tanto, está garantizado que funciona. No puede no funcionar (siempre que lo haya asignado con new[]
).
Por supuesto, podría decir que podría envolver la matriz en un objeto. Eso tiene sentido. Incluso puede moldear el objeto en los elementos de tipo de la matriz. De esa forma, todo el código del destino es el mismo. El tamaño está contenido en el objeto. Y tal vez, solo tal vez, se dé cuenta de que el usuario debería tener cierto control sobre la forma particular en que se asigna la memoria, de modo que no sea solo malloc/free
.
Felicidades: acabas de reinventar std::vector
.
Por eso, muchos programadores de C ++ ni siquiera teclean new[]
.
Flexibilidad
Tu código usa malloc/free
. Pero digamos que estoy haciendo algunos perfiles. Y me doy cuenta de que malloc/free
para ciertos tipos creados con frecuencia es demasiado caro. Creo un administrador de memoria especial para ellos. Pero, ¿cómo conectar todas las asignaciones de matriz a ellos?
Bueno, tengo que buscar en la base de código cualquier ubicación donde cree / destruya matrices de estos tipos. Y luego tengo que cambiar sus asignadores de memoria en consecuencia. Y luego tengo que mirar continuamente la base de código para que otra persona no cambie esos asignadores o introducir un nuevo código de matriz que use diferentes asignadores.
Si en cambio estuviese usando new[]/delete[]
, podría usar la sobrecarga del operador. Simplemente proporciono una sobrecarga para los operadores new[]
y delete[]
para esos tipos. Ningún código tiene que cambiar. Es mucho más difícil para alguien eludir estas sobrecargas; ellos tienen que intentar activamente hacerlo. Etcétera.
De modo que obtengo una mayor flexibilidad y una seguridad razonable de que mis asignaturas se usarán donde deberían usarse.
Legibilidad
Considera esto:
my_object *my_array = new my_object[10];
for (int i=0; i<MY_ARRAY_SIZE; ++i)
my_array[i]=my_object(i);
//... Do stuff with the array
delete [] my_array;
Compáralo con esto:
my_object *my_array = (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE);
if(my_object==NULL)
throw MEMORY_ERROR;
int i;
try
{
for(i=0; i<MY_ARRAY_SIZE; ++i)
new(my_array+i) my_object(i);
}
catch(...) //Exception safety.
{
for(i; i>0; --i) //The i-th object was not successfully constructed
my_array[i-1].~T();
throw;
}
//... Do stuff with the array
for(int i=MY_ARRAY_SIZE; i>=0; --i)
my_array[i].~T();
free(my_array);
Objetivamente hablando, ¿cuál de estos es más fácil de leer y comprender qué está pasando?
Solo mira esta afirmación: (my_object *)malloc(sizeof(my_object) * MY_ARRAY_SIZE)
. Esto es algo de muy bajo nivel. No estás asignando una matriz de nada; estás asignando un pedazo de memoria. Debe calcular manualmente el tamaño del trozo de memoria para que coincida con el tamaño del objeto * la cantidad de objetos que desea. Incluso presenta un molde.
Por el contrario, new my_object[10]
cuenta la historia. new
es la palabra clave C ++ para "crear instancias de tipos". my_object[10]
es una matriz de 10 elementos del tipo my_object
. Es simple, obvio e intuitivo. No hay conversión, no hay computación de tamaños de bytes, nada.
El método malloc
requiere aprender a usar malloc
idiomáticamente. El new
método requiere simplemente entender cómo funciona el new
. Es mucho menos detallado y mucho más obvio lo que está sucediendo.
Además, después de la instrucción malloc
, de hecho no tienes una matriz de objetos. malloc
simplemente devuelve un bloque de memoria que le ha dicho al compilador de C ++ que simule que es un puntero a un objeto (con un molde). No es una matriz de objetos, porque los objetos en C ++ tienen vidas. Y la vida de un objeto no comienza hasta que se construye. Nada en esa memoria ha tenido un constructor que lo haya llamado todavía, y por lo tanto no hay objetos vivos en él.
my_array
en ese punto no es una matriz; es solo un bloque de memoria. No se convierte en una matriz de my_object
s hasta que los construyes en el siguiente paso. Esto es increíblemente intuitivo para un nuevo programador; se necesita una mano experimentada de C ++ (alguien que probablemente aprendió de C) para saber que esos no son objetos vivos y deben tratarse con cuidado. El puntero todavía no se comporta como un my_object*
adecuado, porque aún no apunta a ningún my_object
s.
Por el contrario, sí tiene objetos vivos en el new[]
caso new[]
. Los objetos han sido construidos; ellos son vivos y completamente formados. Puedes usar este puntero como cualquier otro my_object*
.
Aleta
Ninguno de los anteriores dice que este mecanismo no es potencialmente útil en las circunstancias correctas. Pero una cosa es reconocer la utilidad de algo en ciertas circunstancias. Otra cosa diferente es decir que debería ser la forma predeterminada de hacer las cosas.
Deberías usar vectors
.
Dogmático o no, eso es exactamente lo que TODO el contenedor STL hace para asignar e inicializar.
Utilizan un asignador y luego asignan espacio no inicializado y lo inicializan por medio de los constructores del contenedor.
Si esto (como muchas personas solían decir ) " no es c ++ ", ¿cómo puede la biblioteca estándar simplemente implementarse de esa manera?
Si simplemente no desea usar malloc / free, puede asignar "bytes" con solo un new char[]
myobjet* pvext = reinterpret_cast<myobject*>(new char[sizeof(myobject)*vectsize]);
for(int i=0; i<vectsize; ++i) new(myobject+i)myobject(params);
...
for(int i=vectsize-1; i!=0u-1; --i) (myobject+i)->~myobject();
delete[] reinterpret_cast<char*>(myobject);
Esto le permite aprovechar la separación entre la inicialización y la asignación, y sigue tomando el adwantage del new
mecanismo de excepción de asignación.
Tenga en cuenta que, poniendo mi primera y última línea en una myallocator<myobject>
y la segunda y la penúltima en una myvector<myobject>
, hemos ... simplemente reimplementado std::vector<myobject, std::allocator<myobject> >
En mi humilde opinión, es mejor usar vectores. Solo asegúrate de asignar el espacio con anticipación para el rendimiento.
Ya sea:
std::vector<my_object> my_array(MY_ARRAY_SIZE);
Si desea inicializar con un valor predeterminado para todas las entradas.
my_object basic;
std::vector<my_object> my_array(MY_ARRAY_SIZE, basic);
O si no desea construir los objetos pero desea reservar el espacio:
std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);
Luego, si necesita acceder a él como una matriz de punteros C-Style simplemente (solo asegúrese de no agregar cosas mientras mantiene el puntero anterior, pero de todos modos no podría hacerlo con las matrices de estilo c normales).
my_object* carray = &my_array[0];
my_object* carray = &my_array.front(); // Or the C++ way
Acceda a elementos individuales:
my_object value = my_array[i]; // The non-safe c-like faster way
my_object value = my_array.at(i); // With bounds checking, throws range exception
Typedef para bonita:
typedef std::vector<my_object> object_vect;
Paseles las funciones con referencias:
void some_function(const object_vect& my_array);
EDITAR: EN C ++ 11 también hay std :: array. Sin embargo, el problema es que su tamaño se realiza a través de una plantilla, por lo que no puede hacer tamaños diferentes en tiempo de ejecución y no puede pasarlo a funciones a menos que estén esperando exactamente el mismo tamaño (o son funciones de plantilla). Pero puede ser útil para cosas como memorias intermedias.
std::array<int, 1024> my_array;
EDIT2: También en C ++ 11 hay una nueva emplace_back como alternativa a push_back. Esto básicamente le permite ''mover'' su objeto (o construir su objeto directamente en el vector) y le guarda una copia.
std::vector<SomeClass> v;
SomeClass bob {"Bob", "Ross", 10.34f};
v.emplace_back(bob);
v.emplace_back("Another", "One", 111.0f); // <- Note this doesn''t work with initialization lists ☹
Has vuelto a implementar new[]
/ delete[]
aquí, y lo que has escrito es bastante común en el desarrollo de asignaturas especializadas.
La sobrecarga de llamar constructores simples tomará poco tiempo en comparación con la asignación. No necesariamente es "mucho más eficiente", depende de la complejidad del constructor predeterminado y del operator=
.
Una cosa buena que aún no se ha mencionado es que el tamaño de la matriz es conocido por new[]
/ delete[]
. delete[]
hace lo correcto y destruye todos los elementos cuando se le pregunta. Arrastrar una variable adicional (o tres) para que sepa exactamente cómo destruir la matriz es una molestia. Sin embargo, un tipo de colección dedicado sería una buena alternativa.
new[]
/ delete[]
son preferibles por conveniencia. Introducen pocos gastos generales, y pueden salvarte de muchos errores tontos. ¿Está lo suficientemente motivado para eliminar esta funcionalidad y usar una colección / contenedor en todas partes para apoyar su construcción personalizada? Implementé este asignador: el verdadero problema es crear funtores para todas las variaciones de construcción que necesitas en la práctica. En cualquier caso, a menudo tiene una ejecución más exacta a expensas de un programa que a menudo es más difícil de mantener que las expresiones idiomáticas que todo el mundo conoce.
Lo que ha mostrado aquí es realmente el camino a seguir cuando utiliza un asignador de memoria diferente al asignador general del sistema; en ese caso, debe asignar su memoria utilizando el asignador (alloc> malloc (sizeof (my_object))) y luego usar el nuevo operador de colocación para inicializarlo. Esto tiene muchas ventajas en la administración eficiente de la memoria y es bastante común en la biblioteca de plantillas estándar.
Oh, bueno, estaba pensando que dado el número de respuestas no habría ninguna razón para intervenir ... pero creo que estoy atraído como los otros. Vamonos
- Por qué tu solución está rota
- C ++ 11 nuevas instalaciones para manejar la memoria bruta
- Manera más simple de hacer esto
- Consejos
1. Por qué tu solución está rota
En primer lugar, los dos fragmentos que presentó no son equivalentes. new[]
simplemente funciona, el tuyo falla horriblemente en presencia de Excepciones .
Lo new[]
bajo la cubierta es que hace un seguimiento de la cantidad de objetos que se construyeron, de modo que si ocurre una excepción durante, digamos, que la llamada del tercer constructor llama correctamente al destructor para los 2 objetos ya construidos.
Sin embargo, su solución falla horriblemente:
- o no manejas excepciones en absoluto (y pierdes horriblemente)
- o simplemente intenta llamar a los destructores en toda la matriz a pesar de que está medio construida (probablemente falle, pero quién sabe con un comportamiento indefinido)
Entonces los dos claramente no son equivalentes. El tuyo está roto
2. C ++ 11 nuevas instalaciones para manejar la memoria bruta
En C ++ 11, los miembros del comité se han dado cuenta de cuánto nos gustaba jugar con la memoria en bruto y han introducido las instalaciones para ayudarnos a hacerlo de manera más eficiente y más segura.
Verifique la <memory>
breve de cppreference. Este ejemplo muestra los nuevos objetos (*):
#include <iostream>
#include <string>
#include <memory>
#include <algorithm>
int main()
{
const std::string s[] = {"This", "is", "a", "test", "."};
std::string* p = std::get_temporary_buffer<std::string>(5).first;
std::copy(std::begin(s), std::end(s),
std::raw_storage_iterator<std::string*, std::string>(p));
for(std::string* i = p; i!=p+5; ++i) {
std::cout << *i << ''/n'';
i->~basic_string<char>();
}
std::return_temporary_buffer(p);
}
Tenga en cuenta que get_temporary_buffer
es no-throw, devuelve el número de elementos para los cuales la memoria ha sido asignada como un segundo miembro del pair
(por lo tanto, el .first
para obtener el puntero).
(*) O tal vez no tan nuevo como comentó MooingDuck.
3. Manera más simple de hacer esto
En lo que a mí respecta, lo que realmente parece estar pidiendo es un tipo de grupo de memoria tipeado, donde algunos emplazamientos no podrían haber sido inicializados.
¿Sabes sobre boost::optional
?
Básicamente es un área de memoria sin formato que puede ajustarse a un elemento de un tipo dado (parámetro de plantilla) pero que no tiene nada en su lugar. Tiene una interfaz similar a un puntero y le permite consultar si la memoria está realmente ocupada o no. Finalmente, usando las fábricas en el lugar puede usarlo sin copiar objetos si es una preocupación.
Bueno, tu caso de uso realmente se parece a un std::vector< boost::optional<T> >
para mí (¿o quizás un deque
?)
4. Consejos
Finalmente, en caso de que realmente desee hacerlo por su cuenta, ya sea para aprender o porque ningún contenedor STL realmente le conviene, sugiero que lo termine en un objeto para evitar que el código se expanda por todas partes.
No lo olvides: ¡ No te repitas!
Con un objeto (con plantilla) puede capturar la esencia de su diseño en un solo lugar, y luego reutilizarlo en todas partes.
Y, por supuesto, ¿por qué no aprovechar las nuevas instalaciones de C ++ 11 mientras lo hace :)?
Si está escribiendo una clase que imita la funcionalidad de std::vector
o necesita control sobre la asignación de memoria / creación de objetos (inserción en matriz / eliminación, etc.), ese es el camino a seguir. En este caso, no se trata de "no llamar al constructor predeterminado". Se trata de ser capaz de "asignar memoria sin procesar, memmove
objetos viejos allí y luego crear nuevos objetos en las direcciones antiguas", cuestión de poder usar alguna forma de realloc
etc. Indiscutiblemente, la asignación personalizada + ubicación new
son mucho más flexibles ... Lo sé, estoy un poco borracho, pero std::vector
es para chicas ... Sobre la eficiencia, uno puede escribir su propia versión de std::vector
que será AL MENOS tan rápido (y probablemente más pequeño, en términos de tamaño de sizeof()
) que la mayoría de las funciones de 80% de std::vector
utilicen, probablemente, en menos de 3 horas.
Si no desea que su memoria se inicialice mediante llamadas de constructor implícitas, y solo necesita una asignación de memoria segura para la placement new
entonces está perfectamente bien utilizar malloc
y free
lugar de new[]
y delete[]
.
Las razones convincentes de usar new
over malloc
es que new
proporciona inicialización implícita a través de llamadas de constructor, ahorrándole memset
adicional o llamadas a funciones relacionadas publicando un malloc
Y que para el new
no necesita verificar NULL
después de cada asignación, solo adjuntar manejadores de excepciones lo hará haz el trabajo ahorrándote una verificación de errores redundante a diferencia de malloc
.
Estas dos razones imperiosas no se aplican a su uso.
cuál de ellos sea eficiente en cuanto a rendimiento solo puede determinarse mediante un perfil, no hay nada de malo en el enfoque que tiene ahora. En una nota lateral, no veo una razón convincente sobre por qué usar malloc
sobre new[]
tampoco.
Yo diría que ninguno.
La mejor manera de hacerlo sería:
std::vector<my_object> my_array;
my_array.reserve(MY_ARRAY_SIZE);
for (int i=0;i<MY_ARRAY_SIZE;++i)
{ my_array.push_back(my_object(i));
}
Esto se debe a que internamente el vector probablemente esté haciendo la colocación nueva para usted. También gestiona todos los demás problemas asociados con la gestión de memoria que no está teniendo en cuenta.
my_object * my_array=new my_object [10];
Esta será una matriz con objetos.
my_object * my_array=(my_object *)malloc(sizeof(my_object)*MY_ARRAY_SIZE);
Esta será una matriz del tamaño de tus objetos, pero pueden estar "rotos". Si su clase tiene funcitons virtuales, por ejemplo, no podrá llamarlos. Tenga en cuenta que no solo los datos de sus miembros pueden ser inconsistentes, sino que el objeto completo está "roto" (a falta de una palabra mejor).
No digo que esté mal hacer el segundo, siempre y cuando sepas esto.