The congnitive load of C++

The congnitive load of C++

std::shared_ptr #

Consider the following code below:

struct Base {
    virtual ~Base() {}
};

struct Derived : public Base {
    virtual ~Derived() {}
};

struct Wrapper {
    Wrapper(const std::shared_ptr<Base>&a) : a(a) {}
    const std::shared_ptr<Base>& a;
};

Wrapper do_something(const std::shared_ptr<Base>& a [[clang::lifetimebound]]) {
    Wrapper w(a);
    return w;
}

int main() {
    std::shared_ptr<Derived> t = std::make_shared<Derived>();
    auto i = do_something(t);
    std::printf("%p\n", &t);
    std::printf("%p\n", &(i.a));
}

Everythings seems OK but if we use the annotation [[clang::lifetimebound]] from clang, we could get the following warning:

warning: temporary whose address is used as value of local variable ‘i’ will be destroyed at the end of the full-expression [-Wdangling]

But if we just ignore the warning and run the program, we may get the output like this:

0x7ffff09985d0
0x7ffff09985b8

So here comes some questions: What happened here? Where is the “temporary” exactly as? And why are the addresses of t and i.a different?

I believe we all know that if we use a temporary reference as the parameter, when the object is destroyed, the reference would turn into a dangling pointer. However, t wasn’t destoryed. Nonetheless, compiler gives us a warning that we take a temporary variable (which was t) which was destoryed at the end of the full-expression. That’s weird, because t was exactly there and wasn’t destoryed! Some people may say that’s a compiler bug. Well, maybe the warning was strange, but it did generate a correct warning.

So, let’s take a see at constructor of std::shared_ptr from cppreference.

shared_ptr( const shared_ptr& r ) noexcept;
template< class Y >
shared_ptr( const shared_ptr<Y>& r ) noexcept;

Constructs a shared_ptr which shares ownership of the object managed by r. If r manages no object, *this manages no object either. The template overload doesn’t participate in overload resolution if Y* is not implicitly convertible to(until C++17)compatible with(since C++17) T*.

As we know, Base and Derived are covariant and raw pointers to them will act accordingly. But shared_ptr and shared_ptr are not covariant even if Y is derived from T. So as the cppreference says, if Y* is implicitly convertible to T*, then it will construct a shared_ptr if you pass a shared_ptr to function do_something. At the end of the expression auto i = do_something(t);, the shared_ptr was destoryed. What you assign to Wrapper in do_something will become a dangling pointer. And that is where the temporary variable comes from.

However, the above scenario does not occur with unique_ptr. (What can I say)

std::unique_ptr<Derived> is implicitly convertible to std::unique_ptr<Base> through the overload (6) (because both the managed pointer and std::default_delete are implicitly convertible).

Hmm. Keep updating…