Ponteiro exclusivo - Por que o destruidor é chamado 3 vezes

8

Eu tenho um método que retorna um objeto por valor. O método vem de uma biblioteca que eu não tenho controle. Para o manuseio adicional do objeto, quero continuar trabalhando com um unique_ptr nesse objeto. Aqui está um exemplo:

#include <iostream>
#include <memory>

class Bla {
  public:
      Bla() { std::cout << "Constructor!\n"; }
      ~Bla() { std::cout << "Destructor!\n"; }
};

Bla GetBla() {
  Bla bla;
  return std::move(bla);
}

int main() {
  auto bla = std::make_unique<Bla>(GetBla());
}

O exemplo produz a seguinte saída:

Constructor!
Destructor!
Destructor!
Destructor!

Por que o destruidor de Bla é chamado 3 vezes aqui? A maneira como eu crio o unique_prt está correta?

Tobi S.
fonte
4
Eu sugiro que você use um depurador para definir um ponto de interrupção no destruidor. Então você pode ver facilmente de onde os destruidores são chamados.
Algum programador,
3
Dica: std::movenão move nada. Apenas transmite de um tipo para outro.
abhiarora
4
Defina copiar e mover construtores para ver a imagem completa. Também usar std::moveon returné um grande erro.
Marek R
6
Não retorne um objeto usando std :: move. Isso impede o RVO.
Daniel Schlößer 02/03

Respostas:

11

De fato, há três vezes que uma instância de Blaé construída.

Bla GetBla() {
  Bla bla;    // 1st construction
  return std::move(bla); // 2nd construction (return by copy)
}

Não volte por movimento. Basta retornar bla, na maioria dos casos a cópia será elidida.

  auto bla = std::make_unique<Bla>(GetBla());  // 3rd construction - Bla copy construction

Observe que make_unique<Bla>sempre constrói uma nova instância. Nesse caso, porque você está passando outra instância, ela se torna cópia-construção.

Uma dica de que a construção da cópia ocorre é que o construtor padrão é chamado apenas uma vez, enquanto o destruidor é chamado 3 vezes. Isso ocorre porque nos outros 2 casos o construtor implícito de cópia (ou movimentação) é invocado ( Bla::Bla(Bla const&)).

rustyx
fonte
Obrigado pela explicação, pensei que std :: move deveria impedir a cópia, mas aparentemente faz o contrário.
Tobi S.
1
@TobiS. Seria uma construção de movimentação em vez de uma construção de cópia, mas seu objeto não possui automaticamente um construtor de movimentação, porque você definiu um destruidor .
user253751 2/03
@ user253751: Obrigado pelo esclarecimento.
Tobi S.
Você acha que // 2nd construction (return by copy)não está correto? O valor retornado será movido construído? Veja isto (eu posso estar errado): stackoverflow.com/a/60487169/5735010 .
abhiarora 5/03
1
@abhiarora ver segundo comentário. Não há construtor de movimento.
rustyx 5/03
3

O compilador pode até avisar que

mover um objeto local em uma instrução de retorno evita a exclusão da cópia.

Não tenho 100% de certeza, mas acho que você recebe as três chamadas de desctrutor de:

  • A variável local bladeGetBla()
  • O valor de retorno GetBla()depois de ter sido usado emstd::make_unique<Bla>(GetBla());
  • Obviamente do destruidor do std::unique_ptr

A maneira mais fácil é std::make_uniqechamar o construtor padrão de Bla:

auto bla = std::make_unique<Bla>(); // Calls Bla::Bla() to initalize the owned object
#include <iostream>
#include <memory>

class Bla {
  public:
      Bla() { std::cout << "Constructor!\n"; }
      ~Bla() { std::cout << "Destructor!\n"; }
};

int main() {
  auto bla = std::make_unique<Bla>();
}

Resultado

Constructor!
Destructor!
churill
fonte
2

A maneira correta de criar unique_ptr:

auto bla = std::make_unique<Bla>();

No entanto, seu código cria três instâncias de Bla:

  1. Objeto local blaem GetBla()função.
  2. Valor de retorno de GetBla().
  3. Por fim, make_unique()cria mais uma instância.

NOTA:

  1. Na presença de destruidor definido pelo usuário, o compilador não gera move-constructor, portanto, o GetBla()valor de retorno é uma cópia do objeto local bla.
  2. Como o objeto local GetBla()retornado move, a eliminação de cópias é suprimida.
Igor R.
fonte
1

Para realmente ver o que está acontecendo nos bastidores, você pode usar um depurador ou definir o construtor de cópias . Eu adicionei o construtor de cópia no seu código. Experimente o código fornecido abaixo:

#include <iostream>
#include <memory>

class Bla {
public:
    Bla(void) 
    {
        std::cout << "Constructor!" << std::endl;
    }
    //Bla(Bla &&)
    //{
    //    std::cout << "Move Constructors" << std::endl;
    //}
    Bla(const Bla &)
    {
        std::cout << "Copy Constructors" << std::endl;
    }
    ~Bla(void)
    {
        std::cout << "Destructor!" << std::endl;
    }
private:
    int a = 2;
};

Bla GetBla(void) 
{
    Bla bla; // Default Constructor Called here
    return std::move(bla); // Second Construction over here
}

int main(void)
{
    auto bla = std::make_unique<Bla>(GetBla()); // Third Construction
    return 0;
} 

NOTA:

std::movenão move nada. Ele só lança de lvaluereferência para rvaluereferência e seu objeto retornado poderia ter sido construída através de um construtor de movimento (e copiar elisão poderiam ser suprimidos), mas o compilador não implicitamente declarado moveconstrutor porque você definiu o destruidor (e eu adicionei construtor de cópia no meu exemplo)

Saídas:

Constructor! # 1
Copy Constructors # 2
Destructor! # 3
Copy Constructors # 4
Destructor! # 5
Destructor! # 6

Veja meus comentários abaixo:

  1. O objeto blaé construído em função GetBla()via construtor padrão.
  2. O valor de retorno da GetBla()função é do objeto criado na construído-cópia # 1.
  3. bla O objeto (construído em # 1) é destruído e seu destruidor é chamado.
  4. std::make_unique<Bla>chama newe chama o construtor apropriado e escolhe o copyconstrutor.
  5. O objeto criado em # 2 é destruído.
  6. Finalmente, o objeto criado em # 4 é destruído.
abhiarora
fonte