1

From cppreference,

If T is a derived class of some base B, then std::unique_ptr<T> is implicitly convertible to std::unique_ptr<B>

which it obviously must be for polymorphism to work as it does with raw pointers. My question is, if a smart pointer is not generally convertible to a pointer as we can see here, then what is the mechanism used by the smart pointer to allow for runtime polymorphism? My thinking is that either in a constructor or std::make_unique<>()/std::make_shared<>() the internal pointer in the object is used for this conversion. But if these implicit conversions aren't allowed anywhere else, why don't we have to call get() when constructing our smart pointers?

As a very simple example I came up with the following test:

#include <iostream>
#include <memory>

class Base
{
public:
    virtual ~Base() = default;

    virtual void foo() const { std::cout << "Base foo() called." << std::endl; }
};

class Derived : public Base
{
public:
    virtual void foo() const override { std::cout << "Derived foo() called." << std::endl; }
};

void bar(Base* pBase) 
{
    std::cout << "bar() called." << std::endl;
    pBase->foo();
}

int main()
{
    std::unique_ptr<Base> pObject { std::make_unique<Derived>() };      // Implicit conversion here, why no call to get()?

    // bar(pObject);                                                    // Can't be converted, so we have to call get()
    bar(pObject.get());
}
JaMiT
  • 14,422
  • 4
  • 15
  • 31
crdrisko
  • 454
  • 2
  • 5
  • 14
  • _Implicit conversion here_ ... correct. _why no call to get()?_ ...because that wouldn't work, you'd end up with two smart pointers managing the lifespan of the same object. – Eljay Aug 07 '20 at 13:47
  • @Eljay would that also be the case if the two were `shared_ptr<>`’s instead of `unique_ptr<>`’s? – crdrisko Aug 07 '20 at 13:51
  • Think more abstractly. You start with a statement that some class is implicitly convertible to another class. Why do you expect that this implicit conversion first requires an implicit conversion to `T*`? Why should internal mechanisms of the implicit conversion be exposed to the end user? – JaMiT Aug 07 '20 at 14:01
  • 1
    Yes, if you get the raw pointer out of a smart pointer without making the smart pointer relinquish ownership of the raw pointer, and then hand it to a different smart pointer to also manage, both smart pointers will unwittingly manage the lifespan of the object. After one smart pointer destroys the object, any use of the now deleted object by the other smart pointer including deleting it will result in undefined behavior. And that's a Bad Thing™. – Eljay Aug 07 '20 at 14:10
  • @JaMiT I suppose I’m getting tripped up because if there weren’t a conversion to T* wouldn’t more classes be able to be instantiated by derived types without pointers? Perhaps this actually is allowed and I’m just making it more complicated than it needs to be – crdrisko Aug 07 '20 at 14:12
  • @crdrisko Re-read your comment. There **is** a conversion to `T*` (via member functions). There is not an **implicit** conversion to `T*`. The implicit conversion to `std::unique_ptr` is allowed to invoke non-implicit conversions as part of its implementation. – JaMiT Aug 07 '20 at 14:21
  • @JaMiT yes that makes more sense thank you for pointing that out – crdrisko Aug 07 '20 at 14:29

2 Answers2

4

My question is, if a smart pointer is not generally convertible to a pointer as we can see here, then what is the mechanism used by the smart pointer to allow for runtime polymorphism?

Smart pointers are explicitly designed to make such conversion possible. As you can see in std::unique_ptr constructors documentation:

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

this overload is created for this purpose.

This constructor only participates in overload resolution if all of the following is true:

a) unique_ptr<U, E>::pointer is implicitly convertible to pointer

b) U is not an array type

c) Either Deleter is a reference type and E is the same type as D, or Deleter is not a reference type and E is implicitly convertible to D

emphasis is mine. So as pointer to derived class is implicitly convertible to base this constructor overload makes such conversion possible.

Slava
  • 43,454
  • 1
  • 47
  • 90
  • Ah, so it’s the pointer stored internally that is convertible and not the smart pointer itself? If so, is that conversion checked at run-time or is the compiler able to deduce that the two types are convertible at compile-time? – crdrisko Aug 07 '20 at 14:02
  • @crdrisko it's only implicitly convertible if it can be determined at compile-time. You can't downcast by `BasePtr* p; DerivedPtr* d = p` either. – PeterT Aug 07 '20 at 14:08
  • @crdrisko template instantiation happens at compile time so all checks as well. Your statment also not quite correct, if internal pointer is implicitly convertable then such smart pointers are, as single parameter constructor provides type conversion. – Slava Aug 07 '20 at 14:15
1

The returned pointer from .get() does not transfer ownership to the caller (that would be .release()).

If you use the pointer from .get() to construct another smart-pointer you will get a double-free.

So this is an error:

std::unique_ptr<Base> pObject { std::make_unique<Derived>().get() };

this works

std::unique_ptr<Base> pObject { std::make_unique<Derived>().release() };

And for shared_ptr constructing from both .get() and .release() is wrong because you could have other instances having the same shared-state that you're not tranferring. So you would potentially end up with two smart-pointers to the same pointer but with a different shared-state.

PeterT
  • 7,981
  • 1
  • 26
  • 34
  • Can you elaborate a little more on how the shared_ptr would handle either get() or release()? Does it have to do with how the two would then have different counts as opposed to issues with the internal pointer itself? – crdrisko Aug 07 '20 at 14:23
  • @crdrisko yes, the use-count is part of the shared state – PeterT Aug 07 '20 at 14:33