Como passo um argumento unique_ptr para um construtor ou função?


400

Eu sou novo em mover semântica no C ++ 11 e não sei muito bem como lidar com unique_ptrparâmetros em construtores ou funções. Considere esta classe fazendo referência própria:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

É assim que eu devo escrever funções usando unique_ptrargumentos?

E preciso usar std::moveo código de chamada?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


11
Não é uma falha de segmentação quando você está chamando b1-> setNext em um ponteiro vazio?
balki

Respostas:


836

Aqui estão as maneiras possíveis de usar um ponteiro exclusivo como argumento, bem como seu significado associado.

(A) Por valor

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Para que o usuário chame isso, ele deve executar um dos seguintes procedimentos:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Tomar um ponteiro exclusivo por valor significa que você está transferindo a propriedade do ponteiro para a função / objeto / etc em questão. Depois de newBaseconstruído, nextBaseé garantido que ele esteja vazio . Você não possui o objeto e nem sequer tem um ponteiro para ele. Foi-se.

Isso é garantido porque tomamos o parâmetro por valor. std::movena verdade, não move nada; é apenas um elenco chique. std::move(nextBase)retorna a Base&&que é uma referência de valor r para nextBase. É tudo o que faz.

Como Base::Base(std::unique_ptr<Base> n)leva seu argumento por valor e não por referência de valor-r, o C ++ automaticamente criará um temporário para nós. Ele cria um a std::unique_ptr<Base>partir do Base&&qual fornecemos a função via std::move(nextBase). É a construção desse temporário que realmente move o valor de nextBasepara o argumento da função n.

(B) Por referência não constante do valor l

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Isso deve ser chamado em um valor l real (uma variável nomeada). Não pode ser chamado com um temporário como este:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

O significado disso é o mesmo de qualquer outro uso de referências que não sejam const: a função pode ou não reivindicar a propriedade do ponteiro. Dado este código:

Base newBase(nextBase);

Não há garantia de que nextBaseestá vazio. Ele pode estar vazia; não pode. Realmente depende do que Base::Base(std::unique_ptr<Base> &n)quer fazer. Por isso, não é muito evidente apenas a partir da assinatura da função o que vai acontecer; você precisa ler a implementação (ou a documentação associada).

Por causa disso, eu não sugeriria isso como uma interface.

(C) Por referência de valor constante l

Base(std::unique_ptr<Base> const &n);

Eu não mostro uma implementação, porque você não pode passar de a const&. Ao passar a const&, você está dizendo que a função pode acessar o Basevia ponteiro, mas não pode armazená- lo em nenhum lugar. Não pode reivindicar a propriedade dele.

Isso pode ser útil. Não necessariamente para o seu caso específico, mas sempre é bom poder entregar um ponteiro a alguém e saber que ele não pode (sem violar as regras do C ++, como não deixar escapar const) reivindicar a propriedade dele. Eles não podem guardar. Eles podem transmiti-lo a outros, mas esses outros devem cumprir as mesmas regras.

(D) Por referência de valor r

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Isso é mais ou menos idêntico ao caso "por referência não constante do valor l". As diferenças são duas coisas.

  1. Você pode passar um temporário:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Você deve usar std::moveao transmitir argumentos não temporários.

O último é realmente o problema. Se você vir esta linha:

Base newBase(std::move(nextBase));

Você tem uma expectativa razoável de que, após a conclusão dessa linha, nextBaseesteja vazia. Deveria ter sido movido de. Afinal, você tem aquele std::movesentado ali, dizendo que o movimento ocorreu.

O problema é que não tem. Não é garantido que tenha sido movido de. Ele pode ter sido movido a partir, mas você só vai saber de olhar para o código-fonte. Você não pode distinguir apenas da assinatura da função.

Recomendações

  • (A) Por valor: se você deseja que uma função reivindique a propriedade de a unique_ptr, aceite-a por valor.
  • (C) Por referência de valor constante: se você deseja que uma função simplesmente use o unique_ptrdurante a execução dessa função, aceite-a const&. Como alternativa, passe a &ou const&para o tipo real apontado, em vez de usar a unique_ptr.
  • (D) Por referência de valor-r: Se uma função pode ou não reivindicar propriedade (dependendo dos caminhos internos do código), aceite-a &&. Mas eu recomendo fortemente que não faça isso sempre que possível.

Como manipular unique_ptr

Você não pode copiar a unique_ptr. Você só pode movê-lo. A maneira correta de fazer isso é com a std::movefunção de biblioteca padrão.

Se você escolher um unique_ptrvalor, poderá movê-lo livremente. Mas o movimento realmente não acontece por causa de std::move. Tome a seguinte declaração:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Estas são realmente duas afirmações:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(nota: o código acima não é tecnicamente compilado, pois as referências não temporárias ao valor r não são na verdade valores r. Ele está aqui apenas para fins de demonstração).

O temporaryé apenas uma referência de valor r para oldPtr. É no construtor de newPtronde o movimento acontece. unique_ptrO construtor de movimentos (um construtor que leva um &&para si) é o que faz o movimento real.

Se você tem um unique_ptrvalor e deseja armazená-lo em algum lugar, deve usá std::move-lo para fazer o armazenamento.


5
@ Nicol: mas std::movenão nomeia seu valor de retorno. Lembre-se de que as referências nomeadas rvalue são lvalues. ideone.com/VlEM3
R. Martinho Fernandes

31
Concordo basicamente com esta resposta, mas tenho algumas observações. (1) Eu não acho que exista um caso de uso válido para passar referência ao const lvalue: tudo o que o receptor poderia fazer com isso, ele também pode fazer referência ao ponteiro const (bare) ou, melhor ainda, o próprio ponteiro [e não é da sua conta saber que a propriedade é mantida por meio de a unique_ptr; talvez outros chamadores precisem da mesma funcionalidade, mas estejam retendo uma shared_ptrchamada] (2) chamada por referência de valor poderia ser útil se a função chamada modificar o ponteiro, por exemplo, adicionar ou remover nós (pertencentes à lista) de uma lista vinculada.
Marc van Leeuwen

8
... (3) Embora seu argumento a favor da passagem por valor e não pela referência rvalue faça sentido, acho que o próprio padrão sempre passa unique_ptrvalores por referência rvalue (por exemplo, ao transformá-los em shared_ptr). A justificativa para isso pode ser que ela é um pouco mais eficiente (nenhuma mudança para ponteiros temporários é feita), ao mesmo tempo em que concede exatamente os mesmos direitos para o chamador (pode passar rvalores ou lvalores envolvidos std::move, mas não nus).
Marc van Leeuwen

19
Apenas para repetir o que Marc disse e citando Sutter : "Não use uma const unique_ptr & como parâmetro; use widget * em vez disso"
Jon

17
Descobrimos um problema com valor - a mudança ocorre durante a inicialização do argumento, que não é ordenada em relação a outras avaliações de argumentos (exceto em uma lista de inicializadores, é claro). Enquanto a aceitação de uma referência rvalue ordena fortemente que a mudança ocorra após a chamada da função e, portanto, após a avaliação de outros argumentos. Portanto, aceitar a referência rvalue deve ser preferido sempre que a propriedade for tomada.
Ben Voigt

57

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_ptrmodelo de classe; isso também se aplica ao std::auto_ptrmodelo 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 listponteiro 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 nodediretamente, em vez de a list. Nenhum destruidor nodeprecisa 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_ptrou, em shared_ptrvez 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 getmé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 listargumento, 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 headpode chamar essa função como length(head.get()), enquanto um cliente que optou por armazenar uma node nlista 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- constse 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 copyna lista do inicializador se liga ao argumento de referência rvalue no construtor move de unique_ptr<node>, aka list, ao inicializar o nextcampo 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 newarremessos std::bad_alloc, nesse momento, um ponteiro para a lista parcialmente construída é mantido anonimamente em um tipo temporário listcriado 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 nullptrporp, 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::movese 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_ptrou 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 reversedexemplo 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_ptrou std::auto_ptr): como uma operação de movimento real ocorre ao passar uma variável de ponteiroppela expressão std::move(p), não se pode presumir que ppossui 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 pocorra 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 Yo 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 listfornece 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 prependchamada falhar por falta de memória livre. Então a newchamada será lançada std::bad_alloc; neste momento, como não foi nodepossí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 nextcampo do nodeque não pôde ser alocado. Portanto, o ponteiro inteligente original lainda mantém a lista original quando o erro é gerado; essa lista será destruída adequadamente pelo destruidor de ponteiro inteligente ou, caso lsobreviva graças a uma catchclá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)->nextmantido 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_ptrinstâ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 prepende remove_firstnão pode ser chamado pelos clientes que armazenam uma nodevariá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 listtipo que usa a passagem de argumentos do modo 3. Mover uma lista bpara 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 ldiretamente 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 pcomo em std::shared_ptr<T> q(p), após o qual pfoi 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 reversedexemplo 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 catchclá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 returndeclaração). Ninguém quer obter uma referência a um ponteiro que provavelmente acabou de ser nixado.


11
+1 no modo 0 - passando o ponteiro subjacente em vez do unique_ptr. Um pouco fora do tópico (já que a pergunta é sobre passar um unique_ptr), mas é simples e evita problemas.
Machta

"o modo 1 aqui não causa vazamento de memória " - implica que o modo 3 causa vazamento de memória, o que não é verdade. Independentemente de unique_ptrter sido movido ou não, ele ainda excluirá bem o valor se ainda o mantiver sempre que destruído ou reutilizado.
Rustyx

@RustyX: Não consigo ver como você interpreta essa implicação, e nunca pretendi dizer o que você acha que isso implicava. Tudo o que eu quis dizer é que, como em outros lugares, o uso de unique_ptrimpede um vazamento de memória (e, portanto, em certo sentido, cumpre seu contrato), mas aqui (ou seja, usando o modo 1), pode causar (em circunstâncias específicas) algo que pode ser considerado ainda mais prejudicial , ou seja, uma perda de dados (destruição do valor apontado) que poderia ter sido evitada utilizando modo 3.
Marc van Leeuwen

4

Sim, é necessário se você pegar o unique_ptrvalor by no construtor. Explicidade é uma coisa agradável. Como unique_ptré impossível de copiar (cópia privada), o que você escreveu deve gerar um erro do compilador.


3

Edit: Esta resposta está errada, mesmo que, estritamente falando, o código funcione. Só vou deixar aqui porque a discussão é muito útil. Esta outra resposta é a melhor resposta dada na última vez que editei esta: Como passo um argumento unique_ptr para um construtor ou uma função?

A idéia básica ::std::moveé que as pessoas que estão passando por você unique_ptrdevem usá-lo para expressar o conhecimento de que sabem unique_ptrque estão passando perderão a propriedade.

Isso significa que você deve usar uma referência rvalue para a unique_ptrem seus métodos, não para unique_ptrsi mesma. De qualquer forma, isso não funcionará, porque a passagem de unique_ptruma cópia antiga exigiria uma cópia, e isso é explicitamente proibido na interface unique_ptr. Curiosamente, usando uma referência rvalue chamado transforma-lo de volta em um lvalue de novo, então você precisa usar ::std::move dentro de seus métodos também.

Isso significa que seus dois métodos devem ficar assim:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Então as pessoas que usam os métodos fariam o seguinte:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Como você vê, ele ::std::moveexpressa que o ponteiro perderá a propriedade no ponto em que é mais relevante e útil saber. Se isso acontecesse de forma invisível, seria muito confuso para as pessoas que usam sua classe objptrperderem de repente a propriedade sem motivo aparente.


2
As referências de rvalue nomeadas são lvalues.
R. Martinho Fernandes

você tem certeza que é Base fred(::std::move(objptr));e não Base::UPtr fred(::std::move(objptr));?
codablank1

11
Para adicionar ao meu comentário anterior: este código não será compilado. Você ainda precisa usar std::movena implementação do construtor e do método. E mesmo quando você passa por valor, o chamador ainda deve usar std::movepara passar lvalues. A principal diferença é que, com a passagem por valor, essa interface torna a propriedade clara perdida. Veja Nicol Bolas comentar sobre outra resposta.
R. Martinho Fernandes

@ codablank1: Sim. Estou demonstrando como usar o construtor e os métodos na base que usam referências rvalue.
omniforme

@ R.MartinhoFernandes: Ah, interessante. Suponho que isso faça sentido. Eu esperava que você estivesse errado, mas os testes reais provaram que você estava correto. Corrigido agora.
omniforme

0
Base(Base::UPtr n):next(std::move(n)) {}

deve ser muito melhor como

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

e

void setNext(Base::UPtr n)

deveria estar

void setNext(Base::UPtr&& n)

com o mesmo corpo.

E ... o que é evtno handle()??


3
Não há nenhum ganho em usar std::forwardaqui: Base::UPtr&&é sempre um tipo de referência rvalue e o std::movepassa como um rvalue. Já foi encaminhado corretamente.
R. Martinho Fernandes

7
Eu discordo fortemente. Se uma função assume um unique_ptrvalor por, você tem a garantia de que um construtor de movimentação foi chamado no novo valor (ou simplesmente que você recebeu um temporário). Isso garante que a unique_ptrvariável que o usuário tenha esteja agora vazia . Se você o aceitar &&, ele será esvaziado apenas se o seu código chamar uma operação de movimentação. Do seu jeito, é possível que a variável da qual o usuário não tenha sido movido. O que torna o usuário std::movesuspeito e confuso. O uso std::movedeve sempre garantir que algo foi movido .
Nicol Bolas

@ NicolBolas: Você está certo. Excluirei minha resposta porque, enquanto ela funciona, sua observação está absolutamente correta.
omniforme

0

Para o topo votou a resposta. Eu prefiro passar por referência rvalue.

Entendo qual é o problema de passar por referência rvalue pode causar. Mas vamos dividir esse problema em dois lados:

  • para chamador:

Eu devo escrever código Base newBase(std::move(<lvalue>))ou Base newBase(<rvalue>).

  • para chamada:

O autor da biblioteca deve garantir que realmente moverá o unique_ptr para inicializar o membro, se desejar possuir a propriedade.

Isso é tudo.

Se você passar por referência de rvalue, ele chamará apenas uma instrução "mover", mas se passar por valor, serão dois.

Sim, se o autor da biblioteca não for especialista nisso, ele não poderá mover unique_ptr para inicializar o membro, mas é o problema do autor, não você. Seja o que for que passe por referência de valor ou valor, seu código é o mesmo!

Se você está escrevendo uma biblioteca, agora sabe que deve garantir, basta fazê-lo, passar por referência de rvalue é uma escolha melhor que valor. O cliente que usar sua biblioteca apenas escreverá o mesmo código.

Agora, para sua pergunta. Como passo um argumento unique_ptr para um construtor ou função?

Você sabe qual é a melhor escolha.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.