Deixe-me tentar indicar os diferentes modos viáveis de passar ponteiros para objetos cuja memória é gerenciada por uma instância do std::unique_ptr
modelo de classe; isso também se aplica ao std::auto_ptr
modelo de classe mais antiga (que acredito permitir todos os usos que o ponteiro exclusivo faz, mas para os quais, além disso, lvalues modificáveis serão aceitos onde rvalues são esperados, sem a necessidade de chamar std::move
), e até certo ponto também std::shared_ptr
.
Como exemplo concreto para a discussão, considerarei o seguinte tipo de lista simples
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
As instâncias dessa lista (que não podem compartilhar partes com outras instâncias ou serem circulares) pertencem inteiramente a quem detém o list
ponteiro inicial . Se o código do cliente souber que a lista que ele armazena nunca estará vazia, ele também poderá optar por armazenar a primeira node
diretamente, em vez de a list
. Nenhum destruidor node
precisa ser definido: como os destruidores de seus campos são chamados automaticamente, a lista inteira será excluída recursivamente pelo destruidor de ponteiro inteligente assim que o tempo de vida do ponteiro ou nó inicial terminar.
Esse tipo recursivo oferece a oportunidade de discutir alguns casos que são menos visíveis no caso de um ponteiro inteligente para dados simples. Além disso, as próprias funções ocasionalmente fornecem (recursivamente) um exemplo de código do cliente. list
É claro que o typedef for é tendencioso unique_ptr
, mas a definição pode ser alterada para uso auto_ptr
ou, em shared_ptr
vez disso, sem muita necessidade de mudar para o que é dito abaixo (principalmente com relação à segurança de exceções sendo garantida sem a necessidade de escrever destruidores).
Modos de passar ponteiros inteligentes
Modo 0: passa um ponteiro ou argumento de referência em vez de um ponteiro inteligente
Se sua função não estiver relacionada à propriedade, este é o método preferido: não faça com que seja necessário um ponteiro inteligente. Nesse caso, sua função não precisa se preocupar com quem é o proprietário do objeto apontado ou por que meio a propriedade é gerenciada; portanto, passar um ponteiro bruto é perfeitamente seguro e a forma mais flexível, pois, independentemente da propriedade, o cliente sempre pode produza um ponteiro bruto (chamando o get
método ou a partir do endereço do operador &
).
Por exemplo, a função para calcular o comprimento dessa lista não deve ser um list
argumento, mas um ponteiro bruto:
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
Um cliente que possui uma variável list head
pode chamar essa função como length(head.get())
, enquanto um cliente que optou por armazenar uma node n
lista que não esteja vazia pode chamar length(&n)
.
Se for garantido que o ponteiro não é nulo (o que não é o caso aqui, pois as listas podem estar vazias), pode-se preferir passar uma referência ao invés de um ponteiro. Pode ser um ponteiro / referência para non- const
se a função precisar atualizar o conteúdo do (s) nó (s), sem adicionar ou remover nenhum deles (o último envolveria propriedade).
Um caso interessante que se enquadra na categoria modo 0 é fazer uma cópia (profunda) da lista; embora uma função que faça isso deva obviamente transferir a propriedade da cópia criada, ela não se preocupa com a propriedade da lista que está copiando. Portanto, pode ser definido da seguinte forma:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Esse código merece uma análise mais detalhada, tanto para a questão de por que ele é compilado (o resultado da chamada recursiva para copy
na lista do inicializador se liga ao argumento de referência rvalue no construtor move de unique_ptr<node>
, aka list
, ao inicializar o next
campo do gerado node
), e para a pergunta sobre por que ela é protegida por exceção (se durante o processo de alocação recursiva a memória acabar e alguma chamada de new
arremessos std::bad_alloc
, nesse momento, um ponteiro para a lista parcialmente construída é mantido anonimamente em um tipo temporário list
criado para a lista de inicializadores e seu destruidor limpará essa lista parcial). A propósito, alguém deve resistir à tentação de substituir (como eu fiz inicialmente) o segundo nullptr
porp
, que afinal é nulo nesse ponto: não é possível construir um ponteiro inteligente de um ponteiro (bruto) para constante , mesmo quando se sabe que ele é nulo.
Modo 1: passe um ponteiro inteligente por valor
Uma função que assume um valor de ponteiro inteligente como argumento toma posse do objeto apontado imediatamente: o ponteiro inteligente que o chamador reteve (seja em uma variável nomeada ou temporária anônima) é copiado no valor do argumento na entrada da função e no chamador O ponteiro tornou-se nulo (no caso de um temporário, a cópia pode ter sido eliminada, mas, em qualquer caso, o chamador perdeu o acesso ao objeto apontado). Eu gostaria de chamar esse modo de chamada em dinheiro : o chamador paga antecipadamente pelo serviço chamado e não pode ter ilusões sobre a propriedade após a chamada. Para deixar isso claro, as regras de idioma exigem que o chamador envolva o argumento emstd::move
se o ponteiro inteligente for mantido em uma variável (tecnicamente, se o argumento for um valor l); nesse caso (mas não no modo 3 abaixo), essa função faz o que o nome sugere, movendo o valor da variável para uma temporária, deixando a variável nula.
Nos casos em que a função chamada assume incondicionalmente a propriedade de (pilfers) o objeto apontado, esse modo é usado com std::unique_ptr
ou std::auto_ptr
é uma boa maneira de passar um ponteiro junto com sua propriedade, o que evita qualquer risco de vazamento de memória. No entanto, acho que há poucas situações em que o modo 3 abaixo não deve ser preferido (nem um pouco) sobre o modo 1. Por esse motivo, não fornecerei exemplos de uso desse modo. (Mas veja o reversed
exemplo do modo 3 abaixo, onde é observado que o modo 1 faria pelo menos também.) Se a função usar mais argumentos do que apenas esse ponteiro, pode acontecer que haja, além disso, uma razão técnica para evitar o modo 1 (com std::unique_ptr
ou std::auto_ptr
): como uma operação de movimento real ocorre ao passar uma variável de ponteirop
pela expressão std::move(p)
, não se pode presumir que p
possui um valor útil ao avaliar os outros argumentos (a ordem da avaliação não é especificada), o que poderia levar a erros sutis; por outro lado, o uso do modo 3 garante que nenhuma mudança p
ocorra antes da chamada da função, para que outros argumentos possam acessar com segurança um valor p
.
Quando usado com std::shared_ptr
, esse modo é interessante porque, com uma única definição de função, permite ao chamador escolher se deseja manter uma cópia de compartilhamento do ponteiro enquanto cria uma nova cópia de compartilhamento a ser usada pela função (isso acontece quando um valor lvalue o argumento é fornecido; o construtor de cópia para ponteiros compartilhados usado na chamada aumenta a contagem de referência) ou apenas fornece à função uma cópia do ponteiro sem reter um ou tocar na contagem de referência (isso acontece quando um argumento rvalue é fornecido, possivelmente um lvalue envolvido em uma chamada de std::move
). Por exemplo
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
O mesmo poderia ser alcançado definindo separadamente void f(const std::shared_ptr<X>& x)
(para o caso lvalue) e void f(std::shared_ptr<X>&& x)
(para o caso rvalue), com os corpos de função diferindo apenas no fato de a primeira versão chamar semântica de cópia (usando construção / atribuição de cópia ao usar x
), mas a segunda versão mover semântica (escrevendo em std::move(x)
vez disso, como no código de exemplo). Portanto, para ponteiros compartilhados, o modo 1 pode ser útil para evitar duplicação de código.
Modo 2: passe um ponteiro inteligente por referência de valor (modificável)
Aqui, a função requer apenas uma referência modificável ao ponteiro inteligente, mas não fornece indicação do que fará com ele. Eu gostaria de chamar esse método de chamada com cartão : o chamador garante o pagamento fornecendo um número de cartão de crédito. A referência pode ser usada para se apropriar do objeto apontado, mas não é necessário. Esse modo requer o fornecimento de um argumento lvalue modificável, correspondendo ao fato de que o efeito desejado da função pode incluir deixar um valor útil na variável de argumento. Um chamador com uma expressão rvalue que deseja passar para essa função seria forçado a armazená-la em uma variável nomeada para poder fazer a chamada, pois o idioma apenas fornece conversão implícita em uma constantelvalue reference (referente a um temporário) de um rvalue. (Diferentemente da situação oposta tratada por std::move
, uma conversão de Y&&
para Y&
, com Y
o tipo de ponteiro inteligente, não é possível; no entanto, essa conversão pode ser obtida por uma função de modelo simples, se realmente desejado; consulte https://stackoverflow.com/a/24868376 / 1436796 ). No caso em que a função chamada pretende tomar posse incondicionalmente do objeto, roubando o argumento, a obrigação de fornecer um argumento lvalue está dando o sinal errado: a variável não terá valor útil após a chamada. Portanto, o modo 3, que oferece possibilidades idênticas em nossa função, mas solicita que os chamadores forneçam um rvalor, deve ser preferido para esse uso.
No entanto, há um caso de uso válido para o modo 2, ou seja, funções que podem modificar o ponteiro ou o objeto apontado de uma maneira que envolva propriedade . Por exemplo, uma função que prefixa um nó a list
fornece um exemplo desse uso:
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
Claramente, seria indesejável aqui forçar o uso de chamadores std::move
, já que o ponteiro inteligente ainda possui uma lista bem definida e não vazia após a chamada, embora diferente do que antes.
Novamente, é interessante observar o que acontece se a prepend
chamada falhar por falta de memória livre. Então a new
chamada será lançada std::bad_alloc
; neste momento, como não foi node
possível alocar, é certo que a referência de rvalor passado (modo 3) de std::move(l)
ainda não pode ter sido roubada, pois isso seria feito para construir o next
campo do node
que não pôde ser alocado. Portanto, o ponteiro inteligente original l
ainda mantém a lista original quando o erro é gerado; essa lista será destruída adequadamente pelo destruidor de ponteiro inteligente ou, caso l
sobreviva graças a uma catch
cláusula suficientemente cedo , ainda manterá a lista original.
Esse foi um exemplo construtivo; com uma piscadela para esta pergunta, também é possível dar o exemplo mais destrutivo de remover o primeiro nó que contém um determinado valor, se houver:
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
Novamente, a correção é bastante sutil aqui. Notavelmente, na declaração final, o ponteiro (*p)->next
mantido dentro do nó a ser removido é desvinculado (por release
, que retorna o ponteiro, mas torna o original nulo) antes reset
(implicitamente) destrói esse nó (quando destrói o valor antigo mantido por p
), garantindo que um e apenas um nó é destruído naquele momento. (Na forma alternativa mencionada no comentário, esse momento seria deixado para os internos da implementação do operador de atribuição de movimento da std::unique_ptr
instância list
; a norma diz 20.7.1.2.3; 2 que esse operador deve agir "como se chamando reset(u.release())
", de onde o timing deve ser seguro aqui também.)
Observe que prepend
e remove_first
não pode ser chamado pelos clientes que armazenam uma node
variável local para uma lista sempre não-vazia, e com razão, pois as implementações fornecidas não podem funcionar nesses casos.
Modo 3: passar um ponteiro inteligente por referência de valor (modificável)
Este é o modo preferido para usar quando simplesmente se apropriar do ponteiro. Gostaria de chamar esse método de chamada por cheque : o chamador deve aceitar renunciar à propriedade, como se estivesse fornecendo dinheiro, assinando o cheque, mas a retirada real é adiada até que a função chamada realmente ofereça o ponteiro (exatamente como usaria o modo 2 ) A "assinatura do cheque" significa que os chamadores precisam envolver um argumento std::move
(como no modo 1) se for um lvalue (se for um rvalue, a parte "desistir da propriedade" é óbvia e não requer código separado).
Observe que tecnicamente o modo 3 se comporta exatamente como o modo 2, portanto a função chamada não precisa assumir a propriedade; no entanto, eu insistiria que, se houver alguma incerteza sobre a transferência de propriedade (em uso normal), o modo 2 deve ser preferido ao modo 3, de modo que o uso do modo 3 seja implicitamente um sinal para os chamadores de que estão desistindo da propriedade. Pode-se replicar que apenas a passagem do argumento do modo 1 realmente indica perda forçada de propriedade para os chamadores. Porém, se um cliente tiver alguma dúvida sobre as intenções da função chamada, ele deve conhecer as especificações da função que está sendo chamada, o que deve remover qualquer dúvida.
É surpreendentemente difícil encontrar um exemplo típico envolvendo nosso list
tipo que usa a passagem de argumentos do modo 3. Mover uma lista b
para o final de outra lista a
é um exemplo típico; no entanto a
(que sobrevive e mantém o resultado da operação) é melhor passado usando o modo 2:
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
Um exemplo puro de passagem de argumento do modo 3 é o seguinte que pega uma lista (e sua propriedade) e retorna uma lista que contém os nós idênticos na ordem inversa.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
Essa função pode ser chamada l = reversed(std::move(l));
para inverter a lista em si mesma, mas a lista invertida também pode ser usada de maneira diferente.
Aqui, o argumento é imediatamente movido para uma variável local para eficiência (pode-se usar o parâmetro l
diretamente no lugar de p
, mas acessá-lo sempre que envolver um nível extra de indireção); portanto, a diferença na passagem de argumentos do modo 1 é mínima. De fato, usando esse modo, o argumento poderia ter servido diretamente como variável local, evitando assim o movimento inicial; essa é apenas uma instância do princípio geral de que, se um argumento passado por referência serve apenas para inicializar uma variável local, é melhor passá-lo por valor e usar o parâmetro como variável local.
O uso do modo 3 parece preconizado pelo padrão, como testemunha o fato de que todas as funções de biblioteca fornecidas transferem a propriedade de ponteiros inteligentes usando o modo 3. Um caso convincente em particular é o construtor std::shared_ptr<T>(auto_ptr<T>&& p)
. Esse construtor usou (in std::tr1
) para obter uma referência lvalue modificável (assim como o auto_ptr<T>&
construtor de cópia) e, portanto, poderia ser chamado com um auto_ptr<T>
lvalue p
como em std::shared_ptr<T> q(p)
, após o qual p
foi redefinido como nulo. Devido à alteração do modo 2 para o 3 na passagem de argumentos, esse código antigo deve agora ser reescrito std::shared_ptr<T> q(std::move(p))
e continuará a funcionar. Entendo que o comitê não gostou do modo 2 aqui, mas eles tiveram a opção de mudar para o modo 1, definindostd::shared_ptr<T>(auto_ptr<T> p)
em vez disso, eles poderiam garantir que o código antigo funcionasse sem modificação, porque (diferentemente dos ponteiros exclusivos) os ponteiros automáticos podem ser silenciosamente desreferenciados para um valor (o próprio objeto ponteiro sendo redefinido como nulo no processo). Aparentemente, o comitê preferiu o modo de defesa 3 ao invés do modo 1, que optou por quebrar ativamente o código existente, em vez de usar o modo 1 mesmo para um uso já reprovado.
Quando preferir o modo 3 ao invés do modo 1
O modo 1 é perfeitamente utilizável em muitos casos e pode ser preferido em relação ao modo 3 nos casos em que assumir a propriedade assumiria a forma de mover o ponteiro inteligente para uma variável local, como no reversed
exemplo acima. No entanto, vejo duas razões para preferir o modo 3 no caso mais geral:
É um pouco mais eficiente passar uma referência do que criar um ponteiro temporário e nix o antigo (lidar com dinheiro é um tanto trabalhoso); em alguns cenários, o ponteiro pode ser passado várias vezes inalterado para outra função antes de ser realmente furtado. Essa passagem geralmente exige gravação std::move
(a menos que o modo 2 seja usado), mas observe que este é apenas um elenco que na verdade não faz nada (em particular, sem referência), portanto, tem custo zero associado.
Deveria ser concebível que alguma coisa gere uma exceção entre o início da chamada de função e o ponto em que ela (ou alguma chamada contida) realmente move o objeto apontado para outra estrutura de dados (e essa exceção ainda não está capturada dentro da própria função ), ao usar o modo 1, o objeto referido pelo ponteiro inteligente será destruído antes que uma catch
cláusula possa manipular a exceção (porque o parâmetro de função foi destruído durante o desenrolamento da pilha), mas não ao usar o modo 3. O último fornece o o chamador tem a opção de recuperar os dados do objeto nesses casos (capturando a exceção). Observe que o modo 1 aqui não causa vazamento de memória , mas pode levar a uma perda irrecuperável de dados para o programa, o que também pode ser indesejável.
Retornando um ponteiro inteligente: sempre por valor
Para concluir uma palavra sobre o retorno de um ponteiro inteligente, provavelmente aponte para um objeto criado para uso pelo chamador. Este não é realmente um caso comparável ao passar ponteiros para funções, mas, para ser completo, eu gostaria de insistir que, nesses casos, sempre retorne por valor (e não use std::move
na return
declaração). Ninguém quer obter uma referência a um ponteiro que provavelmente acabou de ser nixado.