Por que unique_ptr <Derived> convertido implicitamente para unique_ptr <Base>?

21

Eu escrevi o seguinte código que usa unique_ptr<Derived>onde unique_ptr<Base>é esperado

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;
}

e eu esperava este código não compilar, porque de acordo com o meu entendimento unique_ptr<Base>e unique_ptr<Derived>são tipos não relacionados e unique_ptr<Derived>não é na verdade derivado de unique_ptr<Base>modo a atribuição não deve funcionar.

Mas, graças a alguma mágica, funciona, e eu não entendo o porquê, ou mesmo se é seguro fazê-lo. Alguém pode explicar por favor?

Youda008
fonte
3
ponteiros inteligentes são para enriquecer o que os ponteiros podem fazer para não limitá-lo. Se isso não era possível unique_ptrseria bastante inútil na presença de herança
idclev 463035818
3
"Mas, graças a alguma mágica, funciona" . Quase, você tem UB porque Basenão possui destruidor virtual.
Jarod42

Respostas:

25

O pouco de mágica que você está procurando é o construtor de conversão # 6 aqui :

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

Permite construir std::unique_ptr<T>implicitamente a partir de um std::unique_ptr<U> if expirante (encobrir deleters para maior clareza):

unique_ptr<U, E>::pointer é implicitamente conversível em pointer

Ou seja, ele imita conversões implícitas de ponteiros brutos, incluindo conversões derivadas da base, e faz o que você espera ™ com segurança (em termos de vida útil - você ainda precisa garantir que o tipo de base possa ser excluído polimorficamente).

Quentin
fonte
2
AFAIK, o deleter de Base, não chamará o destruidor de Derived, então não tenho certeza se é realmente seguro. (Não é menos seguro do que ponteiro bruto, reconhecidamente.)
cpplearner
14

Porque std::unique_ptrtem um construtor de conversão como

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

e

Esse construtor participa apenas da resolução de sobrecarga se tudo o que se segue for verdadeiro:

a) unique_ptr<U, E>::pointeré implicitamente conversível empointer

...

A Derived*pode converter para Base*implicitamente, então o construtor de conversão pode ser aplicado para este caso. Então, um std::unique_ptr<Base>poderia ser convertido de um std::unique_ptr<Derived>implicitamente, assim como o ponteiro bruto. (Observe que o valor std::unique_ptr<Derived>deve ser um valor de construção std::unique_ptr<Base>devido à característica de std::unique_ptr.)

songyuanyao
fonte
7

Você pode construir implicitamente uma std::unique_ptr<T>instância a partir de um rvalor de whenstd::unique_ptr<S> for Sconvertível em T. Isto é devido ao construtor # 6 aqui . A propriedade é transferida neste caso.

No seu exemplo, você possui apenas rvalues ​​do tipo std::uinque_ptr<Derived>(porque o valor de retorno std::make_uniqueé um rvalue) e, quando você usa isso como a std::unique_ptr<Base>, o construtor mencionado acima é chamado. Os std::unique_ptr<Derived>objetos em questão, portanto, vivem apenas por um curto período de tempo, ou seja, são criados, e a propriedade é passada para o std::unique_ptr<Base>objeto que é usado posteriormente.

lubgr
fonte