Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::unique_ptr prevent access to the object being destroyed? [duplicate]

Tags:

c++

As far as I see the code below is correct:

#include <iostream>
#include <functional>

struct A
{
    std::function<void ()> func;
    
    int value = 5;
    
    ~A() { func(); }
};

int main()
{
    A* p_a = new A();
    p_a->func = [&p_a]() { std::cout << p_a->value; };
    delete p_a;
    return 0;
}

but the following code is not and results in a segmentation fault:

#include <memory>

int main()
{
    std::unique_ptr<A> p_a = std::make_unique<A>();
    p_a->func = [&p_a]() { std::cout << p_a->value; };
    p_a = {};
    return 0;
}

std::unique_ptr clears its internal pointer first and then deletes the object and thus prevents access to the object being destroyed.

What is it for? What is the logic behind this?

EDIT1:

Marked the question is a duplicate. I would prefer std::uniqu_ptr to keep old object if the deleter throws an exception (if I did not misunderstand something).

like image 583
Dmitriano Avatar asked Oct 14 '22 20:10

Dmitriano


2 Answers

This implementation is done for preventing recursion in std::unique_ptr::reset().

When you take std::unique_ptr by reference, you don't own std::unique_ptr and don't control the life-time of the referenced object. The correct code must always check for nullptr the raw pointer.

int main()
{
    std::unique_ptr<A> p_a = std::make_unique<A>();
    p_a->func = [&p_a]() { if (p_a) std::cout << p_a->value; };
    p_a = {};
    return 0;
}
like image 170
273K Avatar answered Oct 24 '22 00:10

273K


This is just a comment, but too long to fit into the comment section. So I post it as an answer.

First lets look at the standard, it says

u can, upon request, transfer ownership to another unique pointer u2. Upon completion of such a transfer, the following postconditions hold:
(4.1) — u2.p is equal to the pre-transfer u.p,
(4.2) — u.p is equal to nullptr, and
(4.3) — if the pre-transfer u.d maintained state, such state has been transferred to u2.d.
As in the case of a reset, u2 must properly dispose of its pre-transfer owned object via the pre-transfer associated deleter before the ownership transfer is considered complete.

The standard only specified the behavior after the reset, the exact behavior during reset is implementation-dependent.

Then let's look at the GNU implementation:

void reset(pointer __p) noexcept
{
  const pointer __old_p = _M_ptr();
  _M_ptr() = __p;
  if (__old_p)
    _M_deleter()(__old_p);
}
~unique_ptr() noexcept
{
  static_assert(...);
  auto& __ptr = _M_t._M_ptr();
  if (__ptr != nullptr)
    get_deleter()(std::move(__ptr));
  __ptr = pointer();
}

Which looks quite odd because the exact order are different in reset and dtor. It doesn't make sense to me, until I read the MSVC implementation:

void reset(pointer _Ptr = nullptr) noexcept {
    pointer _Old = _STD exchange(_Mypair._Myval2, _Ptr);
    if (_Old) {
        _Mypair._Get_first()(_Old);
    }
}

When reset, of course we need to swap current pointer with the argument. Also, don't forget to call the deleter. Simple job.

~unique_ptr() noexcept {
    if (_Mypair._Myval2) {
        _Mypair._Get_first()(_Mypair._Myval2);
    }
}

In destructor, Microsoft doesn't bother set the pointer to nullptr, they just call the deleter. Simple.

If you really want to set the pointer to nullptr, I bet you will set it after you call the deleter.


Conclusion:

I don't think the STL authors really want to "prevent" you doing something. They just simply write the most straight-forward code to implement the requirement.

like image 1
yyyy Avatar answered Oct 24 '22 00:10

yyyy