Retornando unique_ptr das funções

367

unique_ptr<T>não permite a construção da cópia, mas suporta a semântica de movimentação. No entanto, posso retornar a unique_ptr<T>de uma função e atribuir o valor retornado a uma variável.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

O código acima compila e funciona como pretendido. Então, como é que essa linha 1não invoca o construtor de cópia e resulta em erros do compilador? Se eu tivesse que usar a linha 2, faria sentido (usar a linha também 2funciona, mas não somos obrigados a fazê-lo).

Eu sei que o C ++ 0x permite essa exceção, unique_ptrjá que o valor de retorno é um objeto temporário que será destruído assim que a função sair, garantindo assim a exclusividade do ponteiro retornado. Estou curioso para saber como isso é implementado, é especial no compilador ou há alguma outra cláusula na especificação de linguagem que isso explora?

Pretoriano
fonte
Hipoteticamente, se você estivesse implementando um método de fábrica , você preferiria 1 ou 2 para retornar a saída da fábrica? Presumo que esse seria o uso mais comum de 1 porque, com uma fábrica adequada, você realmente deseja que a propriedade da coisa construída passe para o chamador.
Xharlie 15/09/2015
7
@Xharlie? Ambos passam a propriedade do unique_ptr. A questão toda é sobre 1 e 2, sendo duas maneiras diferentes de conseguir a mesma coisa.
Pretoriano
Nesse caso, o RVO também ocorre em c ++ 0x, a destruição do objeto unique_ptr será uma vez executada após a mainsaída da função, mas não quando a foosaída for concluída.
ampawd

Respostas:

219

existe alguma outra cláusula na especificação da linguagem que isso explora?

Sim, veja 12.8 §34 e §35:

Quando certos critérios são atendidos, uma implementação pode omitir a construção de copiar / mover de um objeto de classe. [...] Essa elisão de operações de copiar / mover, chamada cópia elision , é permitida em uma declaração de retorno em uma função com um tipo de retorno de classe, quando a expressão é o nome de um objeto automático não volátil com o mesmo tipo não qualificado de cv que o tipo de retorno de função [...]

Quando os critérios para a elisão de uma operação de cópia são atendidos e o objeto a ser copiado é designado por um lvalue, a resolução de sobrecarga para selecionar o construtor da cópia é executada primeiro como se o objeto tivesse sido designado por um rvalue .


Só queria acrescentar mais um ponto de que o retorno por valor deve ser a opção padrão aqui, porque um valor nomeado na declaração de retorno, na pior das hipóteses, ou seja, sem elisões em C ++ 11, C ++ 14 e C ++ 17, é tratado como um rvalue. Por exemplo, a função a seguir é compilada com o -fno-elide-constructorssinalizador

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Com o sinalizador definido na compilação, há dois movimentos (1 e 2) acontecendo nessa função e depois um movimento mais tarde (3).

fredoverflow
fonte
@juanchopanza Você quer dizer essencialmente que foo()também está prestes a ser destruído (se não tiver sido atribuído a nada), assim como o valor de retorno dentro da função e, portanto, faz sentido que o C ++ use um construtor de movimento ao fazer isso unique_ptr<int> p = foo();?
7cows
11
Esta resposta diz que uma implementação tem permissão para fazer algo ... não diz que deve, portanto, se essa foi a única seção relevante, isso implicaria que confiar nesse comportamento não é portátil. Mas não acho isso certo. Estou inclinado a pensar que a resposta correta tem mais a ver com o construtor de movimentos, conforme descrito nas respostas de Nikola Smiljanic e Bartosz Milewski.
22614 Don Hatch
6
@DonHatch Diz que é "permitido" executar elisão de copiar / mover nesses casos, mas não estamos falando sobre elisão de cópia aqui. É o segundo parágrafo citado que se aplica aqui, que retrata as regras de exclusão de cópias, mas não é a exclusão de cópias em si. Não há incerteza no segundo parágrafo - é totalmente portátil.
Joseph Mansfield
@juanchopanza Sei que agora isso acontece 2 anos depois, mas você ainda acha que isso está errado? Como mencionei no comentário anterior, não se trata de elisão de cópias. Acontece que nos casos em que a elision da cópia pode ser aplicada (mesmo que não possa ser aplicada com std::unique_ptr), existe uma regra especial para primeiro tratar os objetos como rvalues. Eu acho que isso concorda inteiramente com o que Nikola respondeu.
Joseph Mansfield
11
Então, por que ainda recebo o erro "tentando fazer referência a uma função excluída" para o meu tipo somente de movimentação (construtor de cópia removido) ao devolvê-lo exatamente da mesma maneira que neste exemplo?
DrumM
104

Isso não é de forma alguma específico std::unique_ptr, mas se aplica a qualquer classe que seja móvel. É garantido pelas regras de idioma, pois você está retornando por valor. O compilador tenta excluir cópias, chama um construtor de movimentação se não pode remover cópias, chama um construtor de cópia se não pode mover e falha na compilação se não pode copiar.

Se você tivesse uma função que aceite std::unique_ptrcomo argumento, não seria capaz de passar p para ela. Você precisaria invocar explicitamente o construtor move, mas nesse caso não deve usar a variável p após a chamada para bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}
Nikola Smiljanić
fonte
3
@ Fred - bem, na verdade não. Embora pnão seja temporário, o resultado do foo()que está sendo retornado é; portanto, é um rvalue e pode ser movido, o que torna a atribuição mainpossível. Eu diria que você estava errado, exceto que Nikola parece aplicar essa regra a psi mesma, que está errada.
Edward Strange
Exatamente o que eu queria dizer, mas não consegui encontrar as palavras. Eu removi essa parte da resposta, pois não estava muito claro.
Nikola Smiljanić 30/11
Eu tenho uma pergunta: na pergunta original, existe alguma diferença substancial entre Linha 1e Linha 2? Na minha opinião é a mesma desde quando construir pem main, ele só se preocupa com o tipo de tipo de retorno foo, certo?
Hongxu Chen
11
@HongxuChen Nesse exemplo, não há absolutamente nenhuma diferença, veja a citação do padrão na resposta aceita.
Nikola Smiljanić
Na verdade, você pode usar p depois, desde que você o atribua. Até então, você não pode tentar fazer referência ao conteúdo.
Alan
38

unique_ptr não possui o construtor de cópias tradicional. Em vez disso, possui um "construtor de movimentação" que usa referências de rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Uma referência rvalue (o duplo e comercial) será vinculada apenas a um rvalue. É por isso que você recebe um erro ao tentar passar um lvalue unique_ptr para uma função. Por outro lado, um valor retornado de uma função é tratado como um rvalue, portanto, o construtor de movimentação é chamado automaticamente.

A propósito, isso funcionará corretamente:

bar(unique_ptr<int>(new int(44));

O unique_ptr temporário aqui é um rvalue.

Bartosz Milewski
fonte
8
Eu acho que o ponto é mais, por que p- obviamente - um lvalue - pode ser tratado como um rvalue na declaração de retorno return p;na definição de foo. Acho que não há nenhum problema com o fato de que o valor de retorno da própria função pode ser "movido".
CB Bailey
A quebra do valor retornado da função em std :: move significa que ele será movido duas vezes?
3
@RodrigoSalazar std :: move é apenas um elenco sofisticado de uma referência lvalue (&) para uma referência rvalue (&&). Uso estranho de std :: move em uma referência de rvalue será simplesmente um noop
TiMoch
13

Eu acho que é perfeitamente explicado no item 25 do Effective Modern C ++ de Scott Meyers . Aqui está um trecho:

A parte da bênção padrão do RVO continua dizendo que, se as condições para o RVO forem atendidas, mas os compiladores optarem por não executar a remoção da cópia, o objeto retornado deverá ser tratado como um rvalor. Com efeito, a Norma exige que, quando a RVO for permitida, a elisão da cópia ocorra ou std::moveseja aplicada implicitamente aos objetos locais que estão sendo devolvidos.

Aqui, o RVO se refere à otimização do valor de retorno e, se as condições para o RVO forem atendidas, significa retornar o objeto local declarado dentro da função que você esperaria executar o RVO , o que também é bem explicado no item 25 de seu livro, consultando o padrão (aqui o objeto local inclui os objetos temporários criados pela instrução de retorno). A maior retirada do trecho é a remoção da cópia ou std::moveé aplicada implicitamente aos objetos locais que estão sendo retornados . Scott menciona no item 25 que std::moveé aplicado implicitamente quando o compilador decide não excluir a cópia e o programador não deve fazê-lo explicitamente.

No seu caso, o código é claramente um candidato ao RVO , pois retorna o objeto local pe o tipo de pé o mesmo que o tipo de retorno, o que resulta em elisão de cópia. E se o compilador optar por não excluir a cópia, por qualquer motivo, std::moveteria entrado em ação 1.

David Lee
fonte
5

Uma coisa que eu não vi em outras respostas éPara esclarecer outras respostas, há uma diferença entre o retorno de std :: unique_ptr que foi criado dentro de uma função e um dado a essa função.

O exemplo pode ser assim:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
v010dya
fonte
É mencionado na resposta por fredoverflow - claramente destacado " objeto automático ". Uma referência (incluindo uma referência rvalue) não é um objeto automático.
21417 Toby Speight
@TobySpeight Ok, desculpe. Acho que meu código é apenas um esclarecimento.
precisa saber é o seguinte