¿Hay algún uso para unique_ptr con array?

238

std::unique_ptr tiene soporte para matrices, por ejemplo:

std::unique_ptr<int[]> p(new int[10]);

pero es necesario? probablemente sea más conveniente usar std::vectoro std::array.

¿Encuentra algún uso para esa construcción?

pantano
fuente
66
Para completar, debo señalar que no hay std::shared_ptr<T[]>, pero debería haberlo, y probablemente estará en C ++ 14 si alguien pudiera molestarse en escribir una propuesta. Mientras tanto, siempre hay boost::shared_array.
Seudónimo
13
std::shared_ptr<T []> está en c ++ 17 ahora.
力 力
Puedes encontrar múltiples formas de hacer cualquier cosa en una computadora. Esta construcción tiene uso, especialmente en una ruta activa, porque erradica la sobrecarga de las operaciones de contenedor si sabe exactamente cómo apuntar a su matriz. Además, crea matrices de caracteres sin ninguna duda de almacenamiento contiguo.
kevr

Respuestas:

256

Algunas personas no tienen el lujo de usar std::vector, incluso con asignadores. Algunas personas necesitan una matriz de tamaño dinámico, por lo que std::arrayestá fuera. Y algunas personas obtienen sus matrices de otro código que se sabe que devuelve una matriz; y ese código no se reescribirá para devolver un vectoro algo.

Al permitir unique_ptr<T[]>, usted atiende esas necesidades.

En resumen, usas unique_ptr<T[]>cuando necesita . Cuando las alternativas simplemente no van a funcionar para usted. Es una herramienta de último recurso.

Nicol Bolas
fuente
27
@NoSenseEtAl: No estoy seguro de qué parte de "algunas personas no pueden hacer eso" se te escapa. Algunos proyectos tienen requisitos muy específicos, y entre ellos puede estar "no se puede usar vector". Puede discutir si esos son requisitos razonables o no, pero no puede negar que existen .
Nicol Bolas
21
No hay ninguna razón en el mundo por la que alguien no pueda usar std::vectorsi puede usar std::unique_ptr.
Miles Rout
66
He aquí una razón para no usar vector: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Arvid
13
@DanNissenbaum Estos proyectos existen. En algunas industrias que están bajo un escrutinio muy estricto, como por ejemplo la aviación o la defensa, la biblioteca estándar está prohibida porque es difícil de verificar y demostrar que es correcta para cualquier órgano de gobierno que establezca las regulaciones. Puede argumentar que la biblioteca estándar está bien probada y que estaría de acuerdo con usted, pero usted y yo no establecemos las reglas.
Emily L.
16
@DanNissenbaum Además, algunos sistemas en tiempo real no pueden utilizar la asignación dinámica de memoria, ya que el retraso que provoca una llamada del sistema puede no estar teóricamente limitado y no puede probar el comportamiento en tiempo real del programa. O el límite puede ser demasiado grande, lo que rompe su límite de WCET. Aunque no es aplicable aquí, ya que tampoco lo usarían unique_ptr, pero ese tipo de proyectos realmente existen.
Emily L.
125

Hay compensaciones, y usted elige la solución que coincide con lo que desea. La parte superior de mi cabeza:

Tamaño inicial

  • vectory unique_ptr<T[]>permitir que se especifique el tamaño en tiempo de ejecución
  • array solo permite especificar el tamaño en tiempo de compilación

Redimensionando

  • arrayy unique_ptr<T[]>no permitir cambiar el tamaño
  • vector hace

Almacenamiento

  • vectory unique_ptr<T[]>almacenar los datos fuera del objeto (generalmente en el montón)
  • array almacena los datos directamente en el objeto

Proceso de copiar

  • arrayy vectorpermitir copiar
  • unique_ptr<T[]> no permite copiar

Intercambiar / mover

  • vectory unique_ptr<T[]>tener O (1) tiempo swapy operaciones de movimiento
  • arraytiene O (n) tiempo swapy operaciones de movimiento, donde n es el número de elementos en la matriz

Invalidación de puntero / referencia / iterador

  • array garantiza que los punteros, las referencias y los iteradores nunca se invaliden mientras el objeto esté activo, incluso en swap()
  • unique_ptr<T[]>no tiene iteradores; los punteros y las referencias solo se invalidan swap()mientras el objeto está activo. (Después del intercambio, los punteros apuntan a la matriz con la que intercambió, por lo que siguen siendo "válidos" en ese sentido).
  • vector puede invalidar punteros, referencias e iteradores en cualquier reasignación (y proporciona algunas garantías de que la reasignación solo puede ocurrir en ciertas operaciones).

Compatibilidad con conceptos y algoritmos.

  • arrayy vectorson ambos contenedores
  • unique_ptr<T[]> no es un contenedor

Debo admitir que parece una oportunidad para refactorizar el diseño basado en políticas.

Seudónimo
fuente
1
No estoy seguro de entender lo que quiere decir en el contexto de la invalidación del puntero . ¿Se trata de punteros a los objetos mismos, o punteros a los elementos? ¿O algo mas? ¿Qué tipo de garantía obtienes de una matriz que no obtienes de un vector?
jogojapan
3
Suponga que tiene un iterador, un puntero o una referencia a un elemento de a vector. Luego, aumenta el tamaño o la capacidad de vectortal manera que obliga a una reasignación. Entonces ese iterador, puntero o referencia ya no apunta a ese elemento de vector. Esto es lo que queremos decir con "invalidación". Este problema no sucede arrayporque no hay "reasignación". En realidad, acabo de notar un detalle con eso, y lo he editado a la medida.
Seudónimo
1
Ok, no puede haber invalidación como resultado de la reasignación en una matriz o unique_ptr<T[]>porque no hay reasignación. Pero, por supuesto, cuando la matriz queda fuera de alcance, los punteros a elementos específicos seguirán siendo invalidados.
jogojapan
Sí, todas las apuestas están desactivadas si el objeto ya no está vivo.
Seudónimo
1
@rubenvb Claro que puedes, pero no puedes (por ejemplo) usar directamente los bucles basados ​​en rangos. Por cierto, a diferencia de lo normal T[], el tamaño (o información equivalente) debe estar en algún lugar para operator delete[]destruir correctamente los elementos de la matriz. Sería bueno si el programador tuviera acceso a eso.
Seudónimo
73

Una razón por la que puede usar a unique_ptres si no desea pagar el costo de tiempo de ejecución de la inicialización del valor de la matriz.

std::vector<char> vec(1000000); // allocates AND value-initializes 1000000 chars

std::unique_ptr<char[]> p(new char[1000000]); // allocates storage for 1000000 chars

El std::vectorconstructor y el std::vector::resize()valor inicializarán T, pero newno lo harán siT es un POD.

Ver objetos inicializados en C ++ 11 y std :: vector constructor

Tenga en cuenta que vector::reserveno es una alternativa aquí: ¿Es seguro acceder al puntero sin formato después de std :: vector :: reserve?

Es la misma razón un programador C podría elegir mallocmás calloc.

Charles Salvia
fuente
Pero esta razón no es la única solución .
Ruslan
@Ruslan En la solución vinculada, los elementos de la matriz dinámica todavía tienen un valor inicializado, pero la inicialización del valor no hace nada. Estoy de acuerdo en que un optimizador que no se da cuenta de que no hacer nada 1000000 veces sin ningún código no vale ni un centavo, pero uno podría preferir no depender de esta optimización en absoluto.
Marc van Leeuwen
Otra posibilidad más es proporcionar std::vectorun asignador personalizado que evite la construcción de tipos que son std::is_trivially_default_constructibley la destrucción de objetos que son std::is_trivially_destructible, aunque estrictamente esto viola el estándar C ++ (ya que dichos tipos no se inicializan por defecto).
Walter
Tampoco std::unique_ptrproporciona ninguna comprobación encuadernada, al contrario de muchas std::vectorimplementaciones.
diapir
@diapir No se trata de la implementación: std::vectorel Estándar requiere que se registren los límites .at(). Supongo que querías decir que algunas implementaciones tienen modos de depuración que también se registrarán .operator[], pero considero que eso es inútil para escribir un buen código portátil.
underscore_d
30

Se std::vectorpuede copiar una, mientras que unique_ptr<int[]>permite expresar la propiedad única de la matriz. std::array, por otro lado, requiere que el tamaño se determine en tiempo de compilación, lo que puede ser imposible en algunas situaciones.

Andy Prowl
fuente
2
El hecho de que algo se pueda copiar no significa que tenga que ser así.
Nicol Bolas
44
@ NicolasBolas: No entiendo. Es posible que desee evitar eso por la misma razón por la que se usaría en unique_ptrlugar de shared_ptr. ¿Me estoy perdiendo de algo?
Andy Prowl
44
unique_ptrhace más que solo prevenir el mal uso accidental. También es más pequeño y más bajo que shared_ptr. El punto es que, si bien es bueno tener semántica en una clase que evite el "mal uso", esa no es la única razón para usar un tipo en particular. Y vectores mucho más útil como almacenamiento de matriz que unique_ptr<T[]>, si no por otra razón que no sea el hecho de que tiene un tamaño .
Nicol Bolas
3
Pensé que había dejado claro el punto: hay otras razones para usar un tipo particular además de eso. Al igual que hay razones para preferir vectora unique_ptr<T[]>donde sea posible, en lugar de simplemente decir "no se puede copiar" y, por lo tanto, elegir unique_ptr<T[]>cuando no desee copias. Evitar que alguien haga algo incorrecto no es necesariamente la razón más importante para elegir una clase.
Nicol Bolas
8
std::vectortiene más gastos generales que un std::unique_ptr: utiliza ~ 3 punteros en lugar de ~ 1. std::unique_ptrbloquea la construcción de la copia pero habilita la construcción del movimiento, que si semánticamente los datos con los que está trabajando solo se pueden mover pero no copiar, infecta el classcontenido de los datos. Tener una operación sobre datos que no son válidos en realidad empeora su clase de contenedor, y "simplemente no lo use" no elimina todos los pecados. Tener que poner cada instancia de tu std::vectoren una clase donde deshabilitas manualmente movees un dolor de cabeza. std::unique_ptr<std::array>tiene un size.
Yakk - Adam Nevraumont
22

Scott Meyers tiene esto que decir en Effective Modern C ++

La existencia de std::unique_ptrmatrices for debería ser de solo interés intelectual para usted, ya que std::array, virtualmente std::vector, std::stringson mejores opciones de estructura de datos que las matrices sin procesar. La única situación que puedo concebir cuando una std::unique_ptr<T[]>tendría sentido sería cuando está utilizando una API tipo C que devuelve un puntero sin formato a una matriz de montón de la que asume la propiedad.

Sin embargo, creo que la respuesta de Charles Salvia es relevante: esa std::unique_ptr<T[]>es la única forma de inicializar una matriz vacía cuyo tamaño no se conoce en el momento de la compilación. ¿Qué diría Scott Meyers sobre esta motivación para usar std::unique_ptr<T[]>?

newling
fuente
44
Parece que simplemente no imaginó algunos casos de uso, a saber, un búfer cuyo tamaño es fijo pero desconocido en el momento de la compilación, y / o un búfer para el que no permitimos copias. También hay eficiencia como una posible razón para preferirlo a vector stackoverflow.com/a/24852984/2436175 .
Antonio
17

Contrariamente a std::vectory std::array, std::unique_ptrpuede poseer un puntero NULL.
Esto es útil cuando se trabaja con API de C que esperan una matriz o NULL:

void legacy_func(const int *array_or_null);

void some_func() {    
    std::unique_ptr<int[]> ptr;
    if (some_condition) {
        ptr.reset(new int[10]);
    }

    legacy_func(ptr.get());
}
Jorge
fuente
10

Solía unique_ptr<char[]>implementar una agrupación de memoria preasignada utilizada en un motor de juego. La idea es proporcionar agrupaciones de memoria preasignadas utilizadas en lugar de asignaciones dinámicas para devolver resultados de solicitudes de colisión y otras cosas como la física de partículas sin tener que asignar / liberar memoria en cada cuadro. Es bastante conveniente para este tipo de escenarios en los que necesita grupos de memoria para asignar objetos con un tiempo de vida limitado (generalmente uno, 2 o 3 cuadros) que no requieren lógica de destrucción (solo desasignación de memoria).

Simon Ferquel
fuente
9

Se puede encontrar un patrón común en algunas llamadas de la API de Windows Win32 , en el que el uso de std::unique_ptr<T[]>puede ser útil, por ejemplo, cuando no sabe exactamente qué tan grande debe ser un búfer de salida cuando se llama a alguna API de Win32 (que escribirá algunos datos dentro ese búfer):

// Buffer dynamically allocated by the caller, and filled by some Win32 API function.
// (Allocation will be made inside the 'while' loop below.)
std::unique_ptr<BYTE[]> buffer;

// Buffer length, in bytes.
// Initialize with some initial length that you expect to succeed at the first API call.
UINT32 bufferLength = /* ... */;

LONG returnCode = ERROR_INSUFFICIENT_BUFFER;
while (returnCode == ERROR_INSUFFICIENT_BUFFER)
{
    // Allocate buffer of specified length
    buffer.reset( BYTE[bufferLength] );
    //        
    // Or, in C++14, could use make_unique() instead, e.g.
    //
    // buffer = std::make_unique<BYTE[]>(bufferLength);
    //

    //
    // Call some Win32 API.
    //
    // If the size of the buffer (stored in 'bufferLength') is not big enough,
    // the API will return ERROR_INSUFFICIENT_BUFFER, and the required size
    // in the [in, out] parameter 'bufferLength'.
    // In that case, there will be another try in the next loop iteration
    // (with the allocation of a bigger buffer).
    //
    // Else, we'll exit the while loop body, and there will be either a failure
    // different from ERROR_INSUFFICIENT_BUFFER, or the call will be successful
    // and the required information will be available in the buffer.
    //
    returnCode = ::SomeApiCall(inParam1, inParam2, inParam3, 
                               &bufferLength, // size of output buffer
                               buffer.get(),  // output buffer pointer
                               &outParam1, &outParam2);
}

if (Failed(returnCode))
{
    // Handle failure, or throw exception, etc.
    ...
}

// All right!
// Do some processing with the returned information...
...
Mr.C64
fuente
Podrías usar std::vector<char>en estos casos.
Arthur Tacca
@ArthurTacca - ... si no te importa que el compilador inicialice cada personaje en tu búfer a 0 uno por uno.
TED
9

Me enfrenté a un caso en el que tenía que usar std::unique_ptr<bool[]>, que estaba en la biblioteca HDF5 (una biblioteca para el almacenamiento eficiente de datos binarios, que se usaba mucho en ciencia). Algunos compiladores (Visual Studio 2015 en mi caso) proporcionan compresiónstd::vector<bool> (mediante el uso de 8 bools en cada byte), lo cual es una catástrofe para algo como HDF5, que no se preocupa por esa compresión. Constd::vector<bool> , HDF5 finalmente estaba leyendo basura debido a esa compresión.

¿Adivina quién estuvo allí para el rescate, en un caso en el std::vectorque no funcionó, y necesitaba asignar una matriz dinámica limpiamente? :-)

El físico cuántico
fuente
9

En pocas palabras: es, con mucho, el más eficiente en memoria.

A std::stringviene con un puntero, una longitud y un búfer de "optimización de cadenas cortas". Pero mi situación es que necesito almacenar una cadena que casi siempre está vacía, en una estructura que tengo cientos de miles. En C, solo usaría char *, y sería nulo la mayor parte del tiempo. Lo que también funciona para C ++, excepto que a char *no tiene destructor y no sabe eliminarse. Por el contrario, a std::unique_ptr<char[]>se eliminará a sí mismo cuando salga del alcance. Un vacío std::stringocupa 32 bytes, pero un vacíostd::unique_ptr<char[]> ocupa 8 bytes, es decir, exactamente el tamaño de su puntero.

El mayor inconveniente es que cada vez que quiero saber la longitud de la cadena, tengo que recurrir strlena ella.

jorgbrown
fuente
3

Para responder a las personas que piensan que "tiene que" usarlas en vectorlugar de unique_ptrtener un caso en la programación CUDA en la GPU, cuando asigna memoria en el Dispositivo, debe elegir una matriz de puntero (con cudaMalloc). Luego, al recuperar estos datos en el Host, debe volver a buscar un puntero y unique_ptrestá bien manejarlo fácilmente. El costo adicional de la conversión double*a vector<double>es innecesario y conduce a una pérdida de rendimiento.

Romain Laneuville
fuente
3

Una razón adicional para permitir y usar std::unique_ptr<T[]> , que no se ha mencionado en las respuestas hasta ahora: le permite declarar hacia adelante el tipo de elemento de matriz.

Esto es útil cuando quieres minimizar el encadenado #include declaraciones en los encabezados (para optimizar el rendimiento de compilación)

Por ejemplo -

myclass.h:

class ALargeAndComplicatedClassWithLotsOfDependencies;

class MyClass {
   ...
private:
   std::unique_ptr<ALargeAndComplicatedClassWithLotsOfDependencies[]> m_InternalArray;
};

myclass.cpp:

#include "myclass.h"
#include "ALargeAndComplicatedClassWithLotsOfDependencies.h"

// MyClass implementation goes here

Con la estructura de código anterior, cualquier persona puede #include "myclass.h"y el uso MyClass, sin tener que incluir las dependencias de implementación internos requeridos por MyClass::m_InternalArray.

Si m_InternalArrayse declarara como a std::array<ALargeAndComplicatedClassWithLotsOfDependencies>, o a std::vector<...>, respectivamente, el resultado sería un intento de uso de un tipo incompleto, que es un error en tiempo de compilación.

Boris Shpungin
fuente
Para este caso de uso particular, optaría por el patrón Pimpl para romper la dependencia: si se usa solo de forma privada, la definición puede diferirse hasta que se implementen los métodos de clase; si se usa públicamente, entonces los usuarios de la clase ya deberían haber tenido el conocimiento concreto sobre class ALargeAndComplicatedClassWithLotsOfDependencies. Entonces, lógicamente, no deberías toparte con tales escenarios.
3

No puedo estar en desacuerdo con el espíritu de la respuesta aceptada con suficiente fuerza. ¿"Una herramienta de último recurso"? ¡Lejos de ahi!

A mi modo de ver, una de las características más fuertes de C ++ en comparación con C y con otros lenguajes similares es la capacidad de expresar restricciones para que puedan verificarse en el momento de la compilación y se pueda evitar el uso accidental. Entonces, al diseñar una estructura, pregúntese qué operaciones debería permitir. Todos los demás usos deberían estar prohibidos, y es mejor si tales restricciones pueden implementarse estáticamente (en el momento de la compilación) para que el mal uso provoque un error de compilación.

Entonces, cuando se necesita una matriz, las respuestas a las siguientes preguntas especifican su comportamiento: 1. ¿Es su tamaño a) dinámico en tiempo de ejecución, o b) estático, pero solo conocido en tiempo de ejecución, o c) estático y conocido en tiempo de compilación? 2. ¿Se puede asignar la matriz en la pila o no?

Y en base a las respuestas, esto es lo que veo como la mejor estructura de datos para dicha matriz:

       Dynamic     |   Runtime static   |         Static
Stack std::vector      unique_ptr<T[]>          std::array
Heap  std::vector      unique_ptr<T[]>     unique_ptr<std::array>

Sí, creo unique_ptr<std::array> que también debería considerarse, y tampoco es una herramienta de último recurso. Solo piense qué se adapta mejor a su algoritmo.

Todos estos son compatibles con API C simples a través del puntero sin formato a la matriz de datos ( vector.data()/ array.data()/ uniquePtr.get()).

PD Además de las consideraciones anteriores, también hay una de propiedad: std::arrayy std::vectortiene una semántica de valor (tiene soporte nativo para copiar y pasar por valor), mientras unique_ptr<T[]>que solo se puede mover (impone la propiedad única). Cualquiera puede ser útil en diferentes escenarios. Por el contrario, las matrices estáticas simples ( int[N]) y las matrices dinámicas simples ( new int[10]) no ofrecen ninguna y, por lo tanto, deben evitarse si es posible, lo que debería ser posible en la gran mayoría de los casos. Si eso no fuera suficiente, las matrices dinámicas simples tampoco ofrecen forma de consultar su tamaño, una oportunidad adicional para daños en la memoria y agujeros de seguridad.

Jirafa violeta
fuente
2

Pueden ser la respuesta más correcta posible cuando solo puede introducir un solo puntero a través de una API existente (mensaje de ventana de pensamiento o parámetros de devolución de llamada relacionados con subprocesos) que tienen alguna medida de vida útil después de ser "atrapados" en el otro lado de la escotilla, pero que no está relacionado con el código de llamada:

unique_ptr<byte[]> data = get_some_data();

threadpool->post_work([](void* param) { do_a_thing(unique_ptr<byte[]>((byte*)param)); },
                      data.release());

Todos queremos que las cosas sean buenas para nosotros. C ++ es para las otras veces.

Simon Buchan
fuente
2

unique_ptr<char[]>se puede usar donde desee el rendimiento de C y la conveniencia de C ++. Considere que necesita operar con millones (ok, miles de millones si aún no confía) de cadenas. Almacenar cada uno de ellos en un objeto stringu vector<char>objeto separado sería un desastre para las rutinas de administración de memoria (montón). Especialmente si necesita asignar y eliminar diferentes cadenas muchas veces.

Sin embargo, puede asignar un único búfer para almacenar tantas cadenas. No le gustaría char* buffer = (char*)malloc(total_size);por razones obvias (si no es obvio, busque "por qué usar ptrs inteligentes"). Prefieresunique_ptr<char[]> buffer(new char[total_size]);

Por analogía, las mismas consideraciones de rendimiento y conveniencia se aplican a los no chardatos (considere millones de vectores / matrices / objetos).

Serge Rogatch
fuente
¿Uno no los pone todos en uno grande vector<char>? La respuesta, supongo, es porque se inicializarán en cero cuando cree el búfer, mientras que no lo serán si lo usa unique_ptr<char[]>. Pero esta pepita clave no se encuentra en su respuesta.
Arthur Tacca
2
  • Necesita que su estructura contenga solo un puntero por razones de compatibilidad binaria.
  • Debe interactuar con una API que devuelve la memoria asignada con new[]
  • Su empresa o proyecto tiene una regla general contra el uso std::vector, por ejemplo, para evitar que los programadores descuidados introduzcan copias accidentalmente
  • Desea evitar que los programadores descuidados introduzcan accidentalmente copias en esta instancia.

Existe una regla general de que los contenedores de C ++ deben preferirse a los suyos con punteros. Es una regla general; Tiene excepciones. Hay más; Estos son solo ejemplos.

Jimmy Hartzell
fuente
0

Si necesita una matriz dinámica de objetos que no sean construibles con copia, entonces un puntero inteligente a una matriz es el camino a seguir. Por ejemplo, ¿qué pasa si necesita una matriz de atómicos?

Ilia Minkin
fuente