使用 raw_ptr 表示 unique_ptr 是指向 类 的 nullptr

signaling that unique_ptr is nullptr to classes using raw_ptr

我有底层代码。有没有办法向原始指针发出信号,表明它对应的 unique_ptr 已被重置?

std::unique_ptr<int> pi = std::make_unique<int>(3);

int* rpi = pi.get();
std::cout << "rpi = " << *rpi << std::endl;

pi.reset();

if( rpi != nullptr) // this is always the case
{
    std::cout << "rpi = " << *rpi << std::endl;
}
else
{
    std::cout << "nullptr" << std::endl;
}

假设您的原始指针值可能在程序的任何地方结束,不,您不能。想象一下,您需要某种结构,其中包含指向为您的智能指针创建的唯一全局变量的引用或指针。该变量存储状态,而不是您传递的“结构指针”。它会起作用。

那么这个结构在 C++ 中叫什么?它是 weak_ptr,它的状态仅来自 shared_ptr。为什么?因为对于该指针的任何使用,您必须安全地声明其所有权,这意味着共享所有权。

简而言之:不,你不能。

但更重要的是:您不能在重置后使用原始指针!

当您调用pi.reset()时,将调用底层删除器。在您的示例中,此调用将等效于 delete rpi。众所周知,这样删除内存地址后,就不能再解引用了。

换句话说:这一行

std::cout << "rpi = " << *rpi << std::endl;

调用未定义的行为。如果它确实像您期望的那样打印出数字 3,那纯属偶然。您的程序可能会在此行崩溃或执行其他任何操作...

好吧,你可以通过一些假设来解决这个任务,使用自定义删除仿函数。代码见下,代码后有解释:

Try it online!

#include <memory>
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <iostream>

template <typename T>
class RefDeleter {
public:
    RefDeleter() {
        std::unique_lock lock(mux_);
        id_ = ++gid_;
    }
    ~RefDeleter() {
        std::unique_lock lock(mux_);
        refs_.erase(id_);
    }
    RefDeleter & operator ()(T * ptr) {
        {
            std::unique_lock lock(mux_);
            auto it = refs_.find(id_);
            if (it != refs_.end())
                for (auto e: it->second)
                    *e = nullptr;
        }
        delete ptr;
        return *this;
    }
    RefDeleter & AddRef(T ** ptr) {
        std::unique_lock lock(mux_);
        refs_[id_].insert(ptr);
        return *this;
    }
    RefDeleter & DelRef(T ** ptr) {
        std::unique_lock lock(mux_);
        refs_[id_].erase(ptr);
        return *this;
    }
private:
    uint64_t id_ = 0;
    static inline std::mutex mux_;
    static inline uint64_t gid_ = 0;
    static inline std::unordered_map<uint64_t,
        std::unordered_set<T **>> refs_;
};

static auto l = [](int* p) { delete p; };

template <typename T>
using ref_unique_ptr = std::unique_ptr<T, RefDeleter<T>>;

template <typename T>
class RefDeleterScope {
public:
    RefDeleterScope(ref_unique_ptr<T> & rup, T * & raw)
        : rup_(rup), raw_(raw) {
        rup_.get_deleter().AddRef(&raw_);
    }
    ~RefDeleterScope() {
        rup_.get_deleter().DelRef(&raw_);
    }
private:
    ref_unique_ptr<T> & rup_;
    T * & raw_;
};

#define GETRUP(raw, unique) \
    raw = unique.get(); RefDeleterScope ref_del_scope_##__LINE__(unique, raw);

int main() {
    {
        ref_unique_ptr<int> up;
        up.reset(new int(123));
        int * rp; GETRUP(rp, up);
        std::cout << rp << " " << (rp ? *rp : 0) << std::endl;
        up.reset();
        std::cout << rp << " " << (rp ? *rp : 0) << std::endl;
    }
}

输出:

0x1182eb0 123
0 0

如您所见,使用下一个假设可以解决任务:

  1. 现在你应该到处使用 std::unique_ptr<int, RefDeleter<int>> 而不仅仅是 std::unique_ptr<int>。我将第一个类型命名为 ref_uniqe_ptr<int> 为了更短的方便。

  2. 您不能使用 int * rp = up.get();,只能使用 int * rp; GETRUP(rp, up);,并且您应该在任何地方都这样使用,第一种用法会破坏解决方案。 GETRUP(rp, up) 将唯一指针 up 的指针分配给原始指针 rp。如您所见,GETRUP() 宏分配了一个单独的清理器 class 对象,该对象使用 RAII 在作用域结束后进行清理。

  3. 这个原始指针rp现在有一个块生命周期,这意味着你不能在它的范围之外复制rp,这意味着你不能做int * rp2 = rp;,你应该只做 int * rp2; GETRUP(rp2, up);。这意味着只要你需要原始指针的副本,就必须使用 GETRUP()。因为每个新的原始指针都被分配了一个单独的引用,这是可更新的。

  4. 重置后所有原始指针都将变为 nullptr。即使您将唯一指针重置为某个非空值,所有引用原始指针的内容仍然会变为 nullptr。只有在重置后创建的新原始指针才会保存非空值。所以我的解决方案只在重置时清除指向 null 的原始指针,它不会更新为新的非空值。如果你愿意,你总是可以通过使用 GETRUP().

    重新分配原始指针的值(如果它变为空)