¿Existen casos de uso válidos para usar punteros sin formato nuevos y eliminados o matrices de estilo C con C++ moderno?

Resuelto user0042 asked hace 7 años • 0 respuestas

Aquí hay un video notable ( Deje de enseñar C ) sobre el cambio de paradigma que se debe realizar al enseñar el lenguaje C++.

Y una publicación de blog también notable.

Tengo un sueño ...

Estoy soñando con los llamados cursos/clases/curriculas de C++ que dejarán de enseñar (exigir) a sus estudiantes que usen: ...

Desde C++ 11 como estándar establecido, tenemos las funciones de administración de memoria dinámica , también conocidas como punteros inteligentes .
Incluso a partir de estándares anteriores, tenemos la biblioteca de contenedores estándar de C++ como un buen reemplazo para las matrices sin formato (asignadas con new T[]) (en particular, el uso de matrices de caracteres terminadas std::stringen lugar de estilo C ).NUL

Pregunta(s) en negrita :

Dejando de lado la newanulación de ubicación, ¿existe algún caso de uso válido que no se pueda lograr usando punteros inteligentes o contenedores estándar, sino solo usando newy deletedirectamente (además de la implementación de dichas clases de contenedor/puntero inteligente, por supuesto)?

A veces se rumorea (como aquí o aquí ) que usar newy deleteenrollar manualmente puede ser "más eficiente" en ciertos casos. ¿Cuáles son estos en realidad? ¿No es necesario que estos casos extremos realicen un seguimiento de las asignaciones de la misma manera que lo deben hacer los contenedores estándar o los punteros inteligentes?

Casi lo mismo para las matrices de tamaño fijo estilo C sin formato: existe std::arrayhoy en día, lo que permite todo tipo de asignaciones, copias, referencias, etc. de manera fácil y sintácticamente consistente como lo esperan todos. ¿Existe algún caso de uso para elegir una T myArray[N];matriz de estilo C en lugar de std::array<T,N> myArray;?


Respecto a la interacción con bibliotecas de terceros:

Se supone que una biblioteca de terceros devuelve punteros sin formato asignados con newme gusta

MyType* LibApi::CreateNewType() {
    return new MyType(someParams);
}

siempre puedes ajustarlo a un puntero inteligente para asegurarte de que deletese llame:

std::unique_ptr<MyType> foo = LibApi::CreateNewType();

incluso si la API requiere que llames a su función heredada para liberar el recurso como

void LibApi::FreeMyType(MyType* foo);

todavía puedes proporcionar una función de eliminación:

std::unique_ptr<MyType, LibApi::FreeMyType> foo = LibApi::CreateNewType();

Estoy especialmente interesado en casos de uso válidos "cotidianos" en contraste con los requisitos y restricciones con fines académicos/educativos , que no están cubiertos por las instalaciones estándar mencionadas.
Eso newy deletepuede usarse en marcos de gestión de memoria/recolector de basura o en la implementación de contenedores estándar está fuera de discusión 1 .


Una motivación importante...

... hacer esta pregunta es dar un enfoque alternativo frente a cualquier pregunta (tarea), que está restringida al uso de cualquiera de las construcciones mencionadas en el título, pero preguntas serias sobre el código listo para producción.

A menudo se hace referencia a estos como los conceptos básicos de la gestión de la memoria, lo cual, en mi opinión, es descaradamente incorrecto/mal entendido como adecuado para conferencias y tareas para principiantes .


1) Agregar: Con respecto a ese párrafo, esto debería ser un indicador claro de que newno deletees para estudiantes principiantes de C++, sino que debe dejarse para los cursos más avanzados.

user0042 avatar Oct 28 '17 22:10 user0042
Aceptado

Cuando la propiedad no debería ser local.

Por ejemplo, es posible que un contenedor de punteros no desee que la propiedad de los punteros que contiene resida en los propios punteros. Si intenta escribir una lista vinculada con ptrs únicos directos, en el momento de la destrucción puede volar fácilmente la pila.

Un vectorcontenedor similar a punteros propietarios puede ser más adecuado para almacenar operaciones de eliminación en el nivel de contenedor o subcontenedor, y no en el nivel de elemento.

En esos y otros casos similares, usted envuelve la propiedad como lo hace un puntero inteligente, pero lo hace en un nivel superior. Muchas estructuras de datos (gráficos, etc.) pueden tener problemas similares, donde la propiedad reside adecuadamente en un punto más alto que donde están los punteros, y es posible que no se correspondan directamente con un concepto de contenedor existente.

En algunos casos, puede resultar fácil factorizar la propiedad del contenedor del resto de la estructura de datos. En otros puede que no.

A veces tienes vidas increíblemente complejas, no locales y sin referencias contadas. No existe un lugar sensato para colocar el indicador de propiedad en esos casos.

Determinar la corrección aquí es difícil, pero no imposible. Existen programas que son correctos y tienen una semántica de propiedad tan compleja.


Todos estos son casos extremos y pocos programadores deberían encontrarse con ellos más de un puñado de veces en su carrera.

Yakk - Adam Nevraumont avatar Oct 28 '2017 15:10 Yakk - Adam Nevraumont

Voy a ser contrario y dejar constancia de que dije "no" (al menos a la pregunta que estoy bastante seguro de que realmente pretendías hacer, para la mayoría de los casos que se han citado).

Lo que parecen casos de uso obvios para el uso newy delete(por ejemplo, memoria sin procesar para un montón de GC, almacenamiento para un contenedor) realmente no lo son. Para estos casos, desea almacenamiento "sin procesar", no un objeto (o una matriz de objetos, que es lo que newproporcionan new[]respectivamente).

Dado que desea almacenamiento sin formato, realmente necesita/desea utilizar operator newy operator deleteadministrar el almacenamiento sin formato en sí. Luego usa la ubicación newpara crear objetos en ese almacenamiento sin procesar e invoca directamente al destructor para destruir los objetos. Dependiendo de la situación, es posible que quieras usar un nivel de indirección para eso; por ejemplo, los contenedores en la biblioteca estándar usan una clase Allocator para manejar estas tareas. Esto se pasa como un parámetro de plantilla, que proporciona un punto de personalización (por ejemplo, una forma de optimizar la asignación en función del patrón de uso típico de un contenedor en particular).

Entonces, para estas situaciones, terminas usando la newpalabra clave (tanto en la ubicación new como en la invocación de operator new), pero no algo como T *t = new T[N];, que es sobre lo que estoy bastante seguro que pretendías preguntar.

Jerry Coffin avatar Oct 28 '2017 15:10 Jerry Coffin

Un caso de uso válido es tener que interactuar con código heredado. Especialmente si se pasan punteros sin procesar a funciones que se apropian de ellos.

Es posible que no todas las bibliotecas que utilice utilicen punteros inteligentes y, para utilizarlos, es posible que deba proporcionar o aceptar punteros sin formato y administrar su vida útil manualmente. Este puede ser incluso el caso dentro de su propio código base si tiene una larga historia.

Otro caso de uso es tener que interactuar con C que no tiene punteros inteligentes.

Jesper Juhl avatar Oct 28 '2017 15:10 Jesper Juhl

Es posible que algunas API esperen que usted cree objetos, newpero asumirán la propiedad del objeto. La biblioteca Qt, por ejemplo, tiene un modelo padre-hijo donde el padre elimina a sus hijos. Si utiliza un puntero inteligente, se encontrará con problemas de doble eliminación si no tiene cuidado.

Ejemplo:

{
    // parentWidget has no parent.
    QWidget parentWidget(nullptr);

    // childWidget is created with parentWidget as parent.
    auto childWidget = new QWidget(&parentWidget);
}
// At this point, parentWidget is destroyed and it deletes childWidget
// automatically.

En este ejemplo particular, aún puedes usar un puntero inteligente y estará bien:

{
    QWidget parentWidget(nullptr);
    auto childWidget = std::make_unique<QWidget>(&parentWidget);
}

porque los objetos se destruyen en orden inverso a la declaración. unique_ptrse eliminará childWidgetprimero, lo que hará que childWidgetse dé de baja parentWidgety así evitará la doble eliminación. Sin embargo, la mayoría de las veces no se tiene esa pulcritud. Hay muchas situaciones en las que el padre se destruirá primero y, en esos casos, los hijos se eliminarán dos veces.

En el caso anterior, somos propietarios del padre en ese ámbito y, por lo tanto, tenemos control total de la situación. En otros casos, es posible que el padre no tenga horas, pero le estamos entregando la propiedad de nuestro widget secundario a ese padre, que vive en otro lugar.

Quizás estés pensando que para resolver esto, sólo tienes que evitar el modelo padre-hijo y crear todos tus widgets en la pila y sin padre:

QWidget childWidget(nullptr);

o con un puntero inteligente y sin padre:

auto childWidget = std::make_unique<QWidget>(nullptr);

Sin embargo, esto también te explotará en la cara, ya que una vez que comiences a usar el widget, es posible que lo vuelvan a controlar a tus espaldas. Una vez que otro objeto se convierte en padre, obtienes una doble eliminación cuando usas unique_ptry una eliminación de pila cuando lo creas en la pila.

La forma más sencilla de trabajar con esto es utilizar new. Cualquier otra cosa es generar problemas, o más trabajo, o ambas cosas.

Estas API se pueden encontrar en software moderno y no obsoleto (como Qt) y se desarrollaron hace años, mucho antes de que existieran los punteros inteligentes. No se pueden cambiar fácilmente ya que eso rompería el código existente de las personas.

Nikos C. avatar Nov 04 '2017 03:11 Nikos C.

El OP pregunta específicamente cómo y cuándo el manejo manual será más eficiente en un caso de uso cotidiano, y lo abordaré.

Suponiendo un compilador/stl/plataforma moderno, no existe un uso diario en el que el uso manual de nuevas y eliminaciones sea más eficiente. Para el casoshared_ptr creo que será marginal. En un bucle extremadamente estrecho, podría haber algo que ganar simplemente usando raw new para evitar el conteo de referencias (y encontrar algún otro método de limpieza, a menos que se le imponga de alguna manera, elija usarshared_ptr por una razón), pero ese no es un ejemplo cotidiano ni común. Para Unique_ptr en realidad no hay ninguna diferencia, por lo que creo que es seguro decir que se trata más de un rumor y folklore y que en cuanto al rendimiento en realidad no importará en absoluto (la diferencia no será mensurable en casos normales).

Hay casos en los que no es deseable o posible utilizar una clase de puntero inteligente como ya lo han hecho otras.

darune avatar Nov 03 '2017 14:11 darune