Dlaczego Unique_ptr <Derived> niejawnie rzutuje na Unique_ptr <Base>?

21

Napisałem następujący kod, który używa unique_ptr<Derived>tam, gdzie unique_ptr<Base>jest oczekiwane

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

i spodziewałem się tego kodu nie skompilować, bo według mojego rozumienia unique_ptr<Base>i unique_ptr<Derived>są związane typy i unique_ptr<Derived>nie jest w rzeczywistości pochodzi od unique_ptr<Base>tak przyporządkowanie nie powinna działać.

Ale dzięki pewnej magii działa i nie rozumiem dlaczego, a nawet jeśli jest to bezpieczne. Czy ktoś może wyjaśnić, proszę?

Youda008
źródło
3
inteligentne wskaźniki mają wzbogacić to, co wskaźniki mogą zrobić, aby go nie ograniczyć. Gdyby to nie było możliwe, unique_ptrbyłoby raczej bezużyteczne w przypadku dziedziczenia
idclev 463035818,
3
„Ale dzięki pewnej magii działa” . Prawie masz UB, ponieważ Basenie ma wirtualnego destruktora.
Jarod42

Odpowiedzi:

25

Trochę magii, której szukasz, to konstruktor konwertujący # 6 tutaj :

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

Umożliwia konstruowanie std::unique_ptr<T>niejawnie z wygasającego, std::unique_ptr<U> jeśli (glosowanie przez usuwanie dla jasności):

unique_ptr<U, E>::pointer jest domyślnie konwertowalny na pointer

Innymi słowy, naśladuje niejawne konwersje surowego wskaźnika, w tym konwersje pochodne do bazy, i robi to, czego oczekujesz ™ bezpiecznie (pod względem żywotności - nadal musisz upewnić się, że typ podstawowy można usunąć polimorficznie).

Quentin
źródło
2
AFAIK deleter Basenie wywoła destruktora Derived, więc nie jestem pewien, czy to naprawdę bezpieczne. (Trzeba przyznać, że jest nie mniej bezpieczny niż surowy wskaźnik).
cpplearner,
14

Ponieważ std::unique_ptrma konstruktor konwertujący jako

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

i

Ten konstruktor uczestniczy w rozwiązywaniu problemów z przeciążeniem tylko wtedy, gdy spełnione są wszystkie poniższe warunki:

a) unique_ptr<U, E>::pointerjest domyślnie konwertowalny napointer

...

A Derived*może przekształcić się Base*domyślnie, w tym przypadku można zastosować konstruktor konwertujący. Wtedy a std::unique_ptr<Base>może zostać przekonwertowane z std::unique_ptr<Derived>niejawnie, tak jak robi to wskaźnik raw. (Uwaga: wartość std::unique_ptr<Derived>musi być wartością konstrukcyjną std::unique_ptr<Base>ze względu na charakterystykę std::unique_ptr.)

songyuanyao
źródło
7

Można niejawnie skonstruować std::unique_ptr<T>wystąpienie z rvalue od std::unique_ptr<S>kiedy Smożna przekształcić T. Wynika to z konstruktora # 6 tutaj . W takim przypadku własność zostaje przeniesiona.

W twoim przykładzie masz tylko wartości typu std::uinque_ptr<Derived>(ponieważ zwracana wartość std::make_uniquejest wartością), a gdy używasz tego jako a std::unique_ptr<Base>, wywoływany jest wspomniany wyżej konstruktor. Przedmiotowe std::unique_ptr<Derived>obiekty żyją więc tylko przez krótki czas, tzn. Są tworzone, a następnie własność jest przekazywana do std::unique_ptr<Base>obiektu, który jest dalej używany.

lubgr
źródło