Puntero único: ¿por qué se llama al destructor 3 veces?

8

Tengo un método que devuelve un objeto por valor. El método proviene de una biblioteca sobre la que no tengo control. Para el manejo adicional del objeto, quiero seguir trabajando con un unique_ptr en este objeto. Aquí hay un ejemplo:

#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());
}

El ejemplo produce el siguiente resultado:

Constructor!
Destructor!
Destructor!
Destructor!

¿Por qué se llama al destructor de Bla 3 veces aquí? ¿Es correcta la forma en que creo el unique_prt?

Tobi S.
fuente
44
Le sugiero que use un depurador para establecer un punto de interrupción en el destructor. Entonces puede ver fácilmente desde dónde se llaman los destructores.
Algún tipo programador el
3
Sugerencia: std::moveno mueve nada. Simplemente se lanza de un tipo a otro.
abhiarora
44
Defina copiar y mover constructores para ver la imagen completa. También usar std::moveon returnes un gran error.
Marek R
66
No devuelva un objeto usando std :: move. Esto evita el RVO.
Daniel Schlößer

Respuestas:

11

De hecho, hay 3 veces que Blase construye una instancia de .

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

No regrese por movimiento. Simplemente regrese bla, en la mayoría de los casos la copia se elidirá.

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

Tenga en cuenta que make_unique<Bla>siempre construye una nueva instancia. En este caso, porque está pasando otra instancia, se convierte en una construcción de copia.

Una sugerencia de que la construcción de la copia tiene lugar es que su constructor predeterminado se invoca solo una vez, mientras que el destructor se invoca 3 veces. Esto se debe a que en los otros 2 casos se invoca el constructor de copia (o movimiento) implícito ( Bla::Bla(Bla const&)).

rustyx
fuente
Gracias por la explicación, pensé que se suponía que std :: move debía evitar la copia, pero aparentemente hace lo contrario.
Tobi S.
1
@TobiS. Sería una construcción de movimiento en lugar de una construcción de copia, pero su objeto no tiene automáticamente un constructor de movimiento, porque definió un destructor .
user253751
@ user253751: Gracias por la aclaración.
Tobi S.
¿Crees que // 2nd construction (return by copy)no es correcto? El valor devuelto se moverá construido? Vea esto (podría estar equivocado): stackoverflow.com/a/60487169/5735010 .
abhiarora
1
@abhiarora ver segundo comentario. No hay constructor de movimiento.
rustyx
3

El compilador puede incluso advertirte que

mover un objeto local en una declaración de devolución evita la elisión de copia.

No estoy 100% seguro, pero creo que recibes las tres llamadas de desctructor de:

  • La variable local bladeGetBla()
  • El valor de retorno de GetBla()después de que se usó enstd::make_unique<Bla>(GetBla());
  • Obviamente del destructor de la std::unique_ptr

La forma más fácil es dejar que se std::make_uniqeinvoque el constructor predeterminado 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>();
}

Salida

Constructor!
Destructor!
churill
fuente
2

La forma correcta de crear unique_ptr:

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

Sin embargo, su código crea 3 instancias de Bla:

  1. Objeto local blaen GetBla()función.
  2. Valor de retorno de GetBla().
  3. Finalmente, make_unique()crea una instancia más.

NOTA:

  1. En presencia de un destructor definido por el usuario, el compilador no genera move-constructor, por lo que el GetBla()valor de retorno es una copia del objeto local bla.
  2. Como GetBla()devuelve moveel objeto local ed, se suprime la elisión de copia .
Igor R.
fuente
1

Para ver realmente lo que sucede detrás de escena, puede usar un depurador o definir un constructor de copia . He agregado el constructor de copia en su código. Pruebe el código que figura a continuación:

#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::moveno mueve nada Simplemente se convierte de lvaluereferencia a rvaluereferencia y su objeto devuelto podría haberse construido a través de un constructor de movimiento (y la supresión de copia podría ser suprimida) pero el compilador no declara implícitamente el moveconstructor porque ha definido el destructor (y he agregado el constructor de copia en mi ejemplo)

Salidas:

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

Vea mis comentarios a continuación:

  1. El objeto blase construye en función a GetBla()través del constructor predeterminado.
  2. El valor de retorno de la GetBla()función se construye a partir del objeto creado en # 1.
  3. bla El objeto (construido en # 1) se destruye y se llama a su destructor.
  4. std::make_unique<Bla>llama newy luego llama al constructor apropiado y elige el copyconstructor.
  5. El objeto creado en el n. ° 2 se destruye.
  6. Finalmente, el objeto creado en el n. ° 4 se destruye.
abhiarora
fuente