¿Por qué unique_ptr <Derived> se convierte implícitamente en unique_ptr <Base>?

21

Escribí el siguiente código que usa unique_ptr<Derived>donde unique_ptr<Base>se espera a

class Base {
    int i;
 public:
    Base( int i ) : i(i) {}
    int getI() const { return i; }
};

class Derived : public Base {
    float f;
 public:
    Derived( int i, float f ) : Base(i), f(f) {}
    float getF() const { return f; }
};

void printBase( unique_ptr<Base> base )
{
    cout << "f: " << base->getI() << endl;
}

unique_ptr<Base> makeBase()
{
    return make_unique<Derived>( 2, 3.0f );
}

unique_ptr<Derived> makeDerived()
{
    return make_unique<Derived>( 2, 3.0f );
}

int main( int argc, char * argv [] )
{
    unique_ptr<Base> base1 = makeBase();
    unique_ptr<Base> base2 = makeDerived();
    printBase( make_unique<Derived>( 2, 3.0f ) );

    return 0;
}

y que esperaba que este código no compila, porque según mi entendimiento unique_ptr<Base>y unique_ptr<Derived>son tipos no relacionados y unique_ptr<Derived>no es, de hecho, derivado de unique_ptr<Base>lo que la asignación no debería funcionar.

Pero gracias a algo de magia funciona, y no entiendo por qué, o incluso si es seguro hacerlo. ¿Alguien puede explicar por favor?

Youda008
fuente
3
Los punteros inteligentes deben enriquecer lo que los punteros pueden hacer para no limitarlo. Si esto no fuera posible unique_ptr, sería bastante inútil en presencia de herencia
idclev 463035818
3
"Pero gracias a algo de magia funciona" . Casi tienes UB ya Baseque no tiene destructor virtual.
Jarod42

Respuestas:

25

El poco de magia que estás buscando es el constructor de conversión # 6 aquí :

template<class U, class E>
unique_ptr(unique_ptr<U, E> &&u) noexcept;

Permite construir un std::unique_ptr<T>implícitamente a partir de un std::unique_ptr<U> if expirado (pasar por alto los eliminadores para mayor claridad):

unique_ptr<U, E>::pointer es implícitamente convertible a pointer

Es decir, imita las conversiones implícitas de puntero sin formato, incluidas las conversiones derivadas a base, y hace lo que espera ™ de forma segura (en términos de vida útil: aún debe asegurarse de que el tipo base se pueda eliminar polimórficamente).

Quentin
fuente
2
AFAIK el eliminador de Baseno llamará al destructor de Derived, por lo que no estoy seguro de si es realmente seguro. (Es cierto que no es menos seguro que un puntero en bruto).
cpplearner
14

Porque std::unique_ptrtiene un constructor de conversión como

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept;

y

Este constructor solo participa en la resolución de sobrecarga si se cumple todo lo siguiente:

a) unique_ptr<U, E>::pointeres implícitamente convertible apointer

...

A Derived*podría convertir Base*implícitamente, entonces el constructor de conversión podría aplicarse para este caso. Entonces a std::unique_ptr<Base>podría convertirse de a std::unique_ptr<Derived>implícitamente tal como lo hace el puntero sin formato. (Tenga en cuenta que std::unique_ptr<Derived>tiene que ser un valor para construir std::unique_ptr<Base>debido a la característica de std::unique_ptr).

songyuanyao
fuente
7

Puede construir implícitamente una std::unique_ptr<T>instancia a partir de un valor de std::unique_ptr<S>cuando Ssea ​​convertible a T. Esto se debe al constructor # 6 aquí . La propiedad se transfiere en este caso.

En su ejemplo, solo tiene valores de tipo std::uinque_ptr<Derived>(porque el valor de retorno de std::make_uniquees un valor r), y cuando lo usa como a std::unique_ptr<Base>, se invoca el constructor mencionado anteriormente. Por lo tanto, los std::unique_ptr<Derived>objetos en cuestión solo viven durante un corto período de tiempo, es decir, se crean, luego la propiedad se pasa al std::unique_ptr<Base>objeto que se usa más adelante.

lubgr
fuente