Existe algum uso para unique_ptr com array?

238

std::unique_ptr tem suporte para matrizes, por exemplo:

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

mas é necessário? provavelmente é mais conveniente usar std::vectorou std::array.

Você encontra algum uso para essa construção?

fen
fonte
6
Para ser completo, devo salientar que não existe std::shared_ptr<T[]>, mas deveria existir e provavelmente estará no C ++ 14 se alguém se desse ao trabalho de escrever uma proposta. Enquanto isso, sempre há boost::shared_array.
Pseudônimo
13
std::shared_ptr<T []> está em c ++ 17 agora.
陳力
Você pode encontrar várias maneiras de fazer qualquer coisa em um computador. Essa construção tem utilidade, especialmente em um caminho quente, porque erradica a sobrecarga das operações de contêiner, se você souber exatamente como direcionar sua matriz. Além disso, cria matrizes de caracteres sem nenhuma dúvida de armazenamento contíguo.
kevr 19/04

Respostas:

256

Algumas pessoas não têm o luxo de usar std::vector, mesmo com alocadores. Algumas pessoas precisam de uma matriz de tamanho dinâmico, então std::arrayestá fora. E algumas pessoas obtêm suas matrizes de outro código conhecido por retornar uma matriz; e esse código não será reescrito para retornar um vectorou algo assim.

Ao permitir unique_ptr<T[]>, você atende a essas necessidades.

Em suma, você usa unique_ptr<T[]>quando precisa . Quando as alternativas simplesmente não funcionarão para você. É uma ferramenta de último recurso.

Nicol Bolas
fonte
27
@NoSenseEtAl: Não sei ao certo que parte de "algumas pessoas não têm permissão para fazer isso" escapa de você. Alguns projetos têm requisitos muito específicos e, entre eles, pode ser "você não usa vector". Você pode argumentar se esses requisitos são razoáveis ​​ou não, mas não pode negar que eles existem .
Nicol Bolas
21
Não há razão no mundo para alguém não poder usar std::vectorse puder std::unique_ptr.
Route de milhas
66
aqui está uma razão para não usar o vetor: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Arvid
13
@DanNissenbaum Estes projetos existem. Algumas indústrias que estão sob um exame minucioso, como por exemplo aviação ou defesa, a biblioteca padrão está fora dos limites, porque é difícil verificar e provar que está correta para qualquer órgão regulador que estabeleça os regulamentos. Você pode argumentar que a biblioteca padrão é bem testada e eu concordo com você, mas você e eu não fazemos as regras.
Emily L.
16
@DanNissenbaum Também alguns sistemas rígidos em tempo real não têm permissão para usar a alocação dinâmica de memória, pois o atraso causado por uma chamada do sistema pode não ser teoricamente limitado e você não pode provar o comportamento em tempo real do programa. Ou o limite pode ser muito grande, o que ultrapassa o limite do seu WCET. Embora não seja aplicável aqui, como eles também não usariam unique_ptr, mas esses tipos de projetos realmente existem.
Emily L.
124

Existem compensações e você escolhe a solução que corresponde ao que deseja. Em cima da minha cabeça:

Tamanho inicial

  • vectore unique_ptr<T[]>permitir que o tamanho seja especificado no tempo de execução
  • array permite apenas que o tamanho seja especificado em tempo de compilação

Redimensionando

  • arraye unique_ptr<T[]>não permite redimensionar
  • vector faz

Armazenamento

  • vector e unique_ptr<T[]> armazene os dados fora do objeto (normalmente na pilha)
  • array armazena os dados diretamente no objeto

Copiando

  • array e vector permitir copiar
  • unique_ptr<T[]> não permite copiar

Trocar / mover

  • vectore unique_ptr<T[]>tem tempo O (1)swap operações de e movimentação
  • arraypossui O (n) swapoperações de tempo e movimentação, em que n é o número de elementos na matriz

Anulação de ponteiro / referência / iterador

  • array garante que ponteiros, referências e iteradores nunca serão invalidados enquanto o objeto estiver ativo, mesmo em swap()
  • unique_ptr<T[]>não possui iteradores; ponteiros e referências são invalidados apenas swap()enquanto o objeto estiver ativo. (Após a troca, os ponteiros apontam para a matriz com a qual você trocou, portanto eles ainda são "válidos" nesse sentido.)
  • vector pode invalidar ponteiros, referências e iteradores em qualquer realocação (e fornece algumas garantias de que a realocação só pode ocorrer em determinadas operações).

Compatibilidade com conceitos e algoritmos

  • array e vector são ambos contêineres
  • unique_ptr<T[]> não é um contêiner

Eu tenho que admitir, isso parece uma oportunidade para alguma refatoração com design baseado em políticas.

Pseudônimo
fonte
1
Não sei se entendi o que você quer dizer no contexto da invalidação de ponteiro . Trata-se de ponteiros para os próprios objetos ou ponteiros para os elementos? Ou alguma outra coisa? Que tipo de garantia você obtém de uma matriz que não recebe de um vetor?
Jogojapan
3
Suponha que você tenha um iterador, um ponteiro ou uma referência a um elemento de a vector. Depois, você aumenta o tamanho ou a capacidade de vectortal forma que força uma realocação. Em seguida, esse iterador, ponteiro ou referência não aponta mais para esse elemento do vector. É isso que queremos dizer com "invalidação". Esse problema não ocorre array, porque não há "realocação". Na verdade, eu só notei um detalhe com isso e o editei para se adequar.
Pseudônimo
1
Ok, não pode haver invalidação como resultado da realocação em uma matriz ou unique_ptr<T[]>porque não há realocação. Mas é claro que, quando a matriz ficar fora do escopo, os ponteiros para elementos específicos ainda serão invalidados.
Jogojapan
Sim, todas as apostas serão desativadas se o objeto não estiver mais ativo.
Pseudônimo
1
@rubenvb Claro que você pode, mas você não pode (digamos) usar loops baseados em intervalo diretamente. Aliás, diferentemente do normal T[], o tamanho (ou informações equivalentes) deve estar em algum lugar para operator delete[]destruir corretamente os elementos da matriz. Seria bom se o programador tivesse acesso a isso.
Pseudônimo
73

Um motivo para usar a unique_ptré se você não quiser pagar o custo de tempo de execução da inicialização de valor da 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

O std::vectorconstrutor e std::vector::resize()inicializará o valor T- mas newnão fará isso se Tfor um POD.

Consulte Objetos com valor inicializado em C ++ 11 e construtor std :: vector

Observe que vector::reservenão é uma alternativa aqui: O acesso ao ponteiro bruto após std :: vector :: reserve é seguro?

É a mesma razão que um programador C pode escolher mallocmais calloc.

Charles Salvia
fonte
Mas esse motivo não é a única solução .
Ruslan
@Ruslan Na solução vinculada, os elementos da matriz dinâmica ainda são inicializados por valor, mas a inicialização do valor não faz nada. Concordo que um otimizador que não percebe que não fazer nada 1000000 vezes pode ser implementado por nenhum código não vale um centavo, mas pode-se preferir não depender dessa otimização.
Marc van Leeuwen 25/03
ainda outra possibilidade é fornecer a std::vectorum alocador personalizado que evite a construção de tipos que são std::is_trivially_default_constructiblee a destruição de objetos que são std::is_trivially_destructible, embora isso estritamente viole o padrão C ++ (já que esses tipos não são inicializados por padrão).
285 Walter
Também std::unique_ptrnão fornece nenhuma verificação vinculada, contrária a muitas std::vectorimplementações.
Dia26
@diapir Não se trata da implementação: std::vectoré exigido pela Norma para verificar os limites .at(). Eu acho que você quis dizer que algumas implementações também têm modos de depuração .operator[], mas considero isso inútil para escrever código bom e portátil.
underscore_d
30

Um std::vectorpode ser copiado, enquanto unique_ptr<int[]>permite expressar a propriedade exclusiva da matriz. std::array, por outro lado, exige que o tamanho seja determinado no tempo de compilação, o que pode ser impossível em algumas situações.

Andy Prowl
fonte
2
Só porque algo pode ser copiado não significa que precisa ser.
Nicol Bolas
4
@ NicolBolas: Eu não entendo. Pode-se querer impedir isso pela mesma razão pela qual se usaria em unique_ptrvez de shared_ptr. Estou esquecendo de algo?
Andy Prowl
4
unique_ptrfaz mais do que apenas impedir o uso indevido acidental. Também é menor e menor sobrecarga do que shared_ptr. O ponto é que, embora seja bom ter semântica em uma classe que impeça o "uso indevido", esse não é o único motivo para usar um tipo específico. E vectoré muito mais útil como armazenamento de matriz do que unique_ptr<T[]>, se não por outro motivo, a não ser pelo fato de ter um tamanho .
Nicol Bolas # 23/13
3
Eu pensei que havia deixado claro o ponto: existem outras razões para usar um tipo específico além disso. Assim como há razões para preferir vectormais unique_ptr<T[]>, sempre que possível, em vez de apenas dizer, "você não pode copiá-lo" e, portanto, escolher unique_ptr<T[]>quando você não quer cópias. Impedir que alguém faça a coisa errada não é necessariamente o motivo mais importante para escolher uma aula.
Nicol Bolas
8
std::vectortem mais sobrecarga que a std::unique_ptr- usa ~ 3 ponteiros em vez de ~ 1. std::unique_ptrbloqueia a construção da cópia, mas permite a construção da movimentação, que se semanticamente os dados com os quais você está trabalhando só podem ser movidos, mas não copiados, infectam a classcontendo os dados. Ter uma operação com dados que não são válidos realmente piora a sua classe de contêineres, e "simplesmente não o use" não lava todos os pecados. Ter que colocar todas as suas instâncias std::vectorem uma classe em que você desabilita manualmente moveé uma dor de cabeça. std::unique_ptr<std::array>tem um size.
Yakk - Adam Nevraumont
22

Scott Meyers tem isso a dizer em Effective Modern C ++

A existência de std::unique_ptrmatrizes for deve ter apenas interesse intelectual para você, porque std::array, virtualmente std::vector, std::stringhá sempre melhores opções de estrutura de dados do que matrizes brutas. Sobre a única situação que posso imaginar quando std::unique_ptr<T[]>faria sentido quando você está usando uma API do tipo C que retorna um ponteiro bruto para uma matriz de heap da qual você assume a propriedade.

Penso que a resposta de Charles Salvia é relevante: essa std::unique_ptr<T[]>é a única maneira de inicializar uma matriz vazia cujo tamanho não é conhecido em tempo de compilação. O que Scott Meyers tem a dizer sobre essa motivação para usar std::unique_ptr<T[]>?

newling
fonte
4
Parece que ele simplesmente não imaginou alguns casos de uso, ou seja, um buffer cujo tamanho é fixo, mas desconhecido no momento da compilação, e / ou um buffer para o qual não permitimos cópias. Também há eficiência, como um possível motivo para preferir, ao vector stackoverflow.com/a/24852984/2436175 .
Antonio
17

Ao contrário de std::vectore std::array, std::unique_ptrpode possuir um ponteiro NULL.
Isso é útil ao trabalhar com APIs C que esperam uma matriz ou 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());
}
George
fonte
10

Eu usei unique_ptr<char[]>para implementar um pool de memória pré-alocado usado em um mecanismo de jogo. A idéia é fornecer pools de memória pré-alocados usados ​​em vez de alocações dinâmicas para retornar resultados de solicitações de colisão e outras coisas, como física de partículas, sem ter que alocar / liberar memória em cada quadro. É bastante conveniente para esse tipo de cenário em que você precisa de conjuntos de memória para alocar objetos com tempo de vida útil limitado (geralmente um, 2 ou 3 quadros) que não exigem lógica de destruição (apenas desalocação de memória).

Simon Ferquel
fonte
9

Um padrão comum pode ser encontrado em algumas chamadas da API do Windows Win32 , nas quais o uso std::unique_ptr<T[]>pode ser útil, por exemplo, quando você não sabe exatamente qual deve ser o tamanho de um buffer de saída ao chamar alguma API do Win32 (que grava alguns dados dentro esse buffer):

// 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
fonte
Você poderia apenas usar std::vector<char>nesses casos.
Arthur Tacca
@ArthurTacca - ... se você não se importa com o compilador inicializando cada caractere no seu buffer para 0 um por um.
TED
9

Eu enfrentei um caso em que eu tinha que usar std::unique_ptr<bool[]>, que estava na biblioteca HDF5 (uma biblioteca para armazenamento eficiente de dados binários, muito usada na ciência). Alguns compiladores (Visual Studio 2015 no meu caso) fornecem compactaçãostd::vector<bool> (usando 8 bools em cada byte), que é uma catástrofe para algo como o HDF5, que não se importa com essa compactação. Comstd::vector<bool> , o HDF5 acabou lendo o lixo por causa dessa compressão.

Adivinha quem estava lá para o resgate, em um caso em std::vectorque não funcionava, e eu precisava alocar uma matriz dinâmica de maneira limpa? :-)

O físico quântico
fonte
9

Em poucas palavras: é de longe o mais eficiente em termos de memória.

A std::stringvem com um ponteiro, um comprimento e um buffer de "otimização de cadeia curta". Mas minha situação é que preciso armazenar uma string quase sempre vazia, em uma estrutura da qual tenho centenas de milhares. Em C, eu usaria apenas char *, e seria nulo na maioria das vezes. O que também funciona para C ++, exceto que a char *não possui destruidor e não sabe se excluir. Por outro lado, a std::unique_ptr<char[]>se exclui quando sai do escopo. Um vazio std::stringocupa 32 bytes, mas um vazio std::unique_ptr<char[]>ocupa 8 bytes, ou seja, exatamente o tamanho do seu ponteiro.

A maior desvantagem é que toda vez que eu quero saber o comprimento da corda, tenho que ligar strlenpara ela.

jorgbrown
fonte
3

Para responder às pessoas que pensam que você "precisa" usar, em vectorvez de unique_ptreu ter um caso na programação CUDA na GPU ao alocar memória no dispositivo, você deve procurar uma matriz de ponteiros (com cudaMalloc). Em seguida, ao recuperar esses dados no Host, você deve procurar novamente um ponteiro e não unique_ptrhá problema em manipular o ponteiro facilmente. O custo extra da conversão double*para vector<double>é desnecessário e leva a uma perda de desempenho.

Romain Laneuville
fonte
3

Um motivo adicional para permitir e usar std::unique_ptr<T[]> , que não foi mencionado nas respostas até agora: permite que você declare o tipo de elemento da matriz.

Isso é útil quando você deseja minimizar o encadeamento #include instruções nos cabeçalhos (para otimizar o desempenho da compilação).

Por exemplo -

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

Com a estrutura de código acima, qualquer pessoa pode #include "myclass.h"e pode usar MyClass, sem precisar incluir as dependências de implementação internas exigidas por MyClass::m_InternalArray.

Se m_InternalArrayfosse declarado como a std::array<ALargeAndComplicatedClassWithLotsOfDependencies>, ou a std::vector<...>, respectivamente - o resultado seria a tentativa de uso de um tipo incompleto, que é um erro em tempo de compilação.

Boris Shpungin
fonte
Nesse caso de uso específico, eu optaria pelo padrão Pimpl para quebrar a dependência - se for usado apenas em particular, a definição poderá ser adiada até que os métodos de classe sejam implementados; se for usado publicamente, os usuários da classe já devem ter conhecimento concreto class ALargeAndComplicatedClassWithLotsOfDependencies. Então, logicamente, você não deve se deparar com esses cenários.
3

Não posso discordar com o espírito da resposta aceita com força suficiente. "Uma ferramenta de último recurso"? Longe disso!

A meu ver, um dos recursos mais fortes do C ++ em comparação com o C e com outras linguagens semelhantes é a capacidade de expressar restrições para que possam ser verificadas em tempo de compilação e que o uso indevido acidental possa ser evitado. Portanto, ao projetar uma estrutura, pergunte-se que operações ela deve permitir. Todos os outros usos devem ser proibidos, e é melhor que essas restrições possam ser implementadas estaticamente (em tempo de compilação), para que o uso indevido resulte em falha na compilação.

Portanto, quando alguém precisa de uma matriz, as respostas para as seguintes perguntas especificam seu comportamento: 1. Seu tamanho é a) dinâmico em tempo de execução ou b) estático, mas conhecido apenas em tempo de execução ou c) estático e conhecido em tempo de compilação? 2. A matriz pode ser alocada na pilha ou não?

E, com base nas respostas, é isso que considero a melhor estrutura de dados para uma matriz desse tipo:

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

Sim eu acho unique_ptr<std::array> que também deve ser considerado, e nem é uma ferramenta de último recurso. Basta pensar no que melhor se ajusta ao seu algoritmo.

Tudo isso é compatível com APIs C simples por meio do ponteiro bruto para a matriz de dados ( vector.data()/ array.data()/ uniquePtr.get()).

PS Além das considerações acima, há também uma propriedade: std::arraye std::vectorpossui semântica de valor (possui suporte nativo para copiar e transmitir por valor), enquanto unique_ptr<T[]>só pode ser movida (aplica a propriedade única). Qualquer um pode ser útil em diferentes cenários. Pelo contrário, matrizes estáticas simples ( int[N]) e dinâmicas simples ( new int[10]) não oferecem e, portanto, devem ser evitadas se possível - o que deve ser possível na grande maioria dos casos. Se isso não bastasse, matrizes dinâmicas simples também não oferecem maneira de consultar seu tamanho - oportunidade extra para corrupção de memória e falhas de segurança.

Girafa violeta
fonte
2

Eles podem ser a resposta mais correta possível quando você só consegue apontar um único ponteiro por meio de uma API existente (mensagem da janela de reflexão ou parâmetros de retorno de chamada relacionados ao encadeamento) que têm alguma medida da vida útil após serem "capturados" no outro lado da hachura, mas que não está relacionado ao código de chamada:

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

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

Todos nós queremos que as coisas sejam agradáveis ​​para nós. C ++ é para os outros momentos.

Simon Buchan
fonte
2

unique_ptr<char[]>pode ser usado onde você deseja o desempenho de C e a conveniência de C ++. Considere que você precisa operar com milhões (ok, bilhões, se você ainda não confia) de strings. Armazenar cada um deles em um objeto stringou separado vector<char>seria um desastre para as rotinas de gerenciamento de memória (heap). Especialmente se você precisar alocar e excluir diferentes strings várias vezes.

No entanto, você pode alocar um único buffer para armazenar tantas seqüências de caracteres. Você não gostaria char* buffer = (char*)malloc(total_size);por razões óbvias (se não óbvio, procure "por que usar smart ptrs"). Você prefereunique_ptr<char[]> buffer(new char[total_size]);

Por analogia, as mesmas considerações de desempenho e conveniência se aplicam a não chardados (considere milhões de vetores / matrizes / objetos).

Serge Rogatch
fonte
Um não colocá-los todos em um grande vector<char>? A resposta, suponho, é porque eles serão inicializados com zero quando você criar o buffer, enquanto não serão se você usar unique_ptr<char[]>. Mas esta pepita de chave está faltando na sua resposta.
Arthur Tacca
2
  • Você precisa que sua estrutura contenha apenas um ponteiro por motivos de compatibilidade binária.
  • Você precisa fazer interface com uma API que retorna a memória alocada com new[]
  • Sua empresa ou projeto possui uma regra geral contra o uso std::vector, por exemplo, para impedir que programadores descuidados introduzam cópias acidentalmente
  • Você deseja impedir que programadores descuidados introduzam cópias acidentalmente nesta instância.

Existe uma regra geral de que os contêineres C ++ devem ser preferidos ao invés de rolar o seu próprio com ponteiros. É uma regra geral; tem exceções. Tem mais; estes são apenas exemplos.

Jimmy Hartzell
fonte
0

Se você precisar de uma matriz dinâmica de objetos que não sejam construtíveis para cópia, o caminho a seguir é um ponteiro inteligente para uma matriz. Por exemplo, e se você precisar de uma matriz atômica.

Ilia Minkin
fonte