最近在忙自己的研究生科研工作和尽量在不看源码的情况下写一个玩具版的muduo(我已经看过陈硕的《Linux多线程服务端编程:使用muduo C++网络库》,相当于按自己的理解再写一遍),没太有时间写C++对象模型的后面部分,等组会开完后再继续写。
今天就写一下几天前看到的一个小技巧,也即标题:std::weak_ptr<void>绑定到std::shared_ptr<T>

std::weak_ptr

我们知道weak_ptr目的是防止只使用std::shared_ptr导致的循环引用,从而导致内存泄漏。一个经典的例子如下:

#include <iostream>
#include <vector>
#include <memory>
#include <string>
class Child;
class Parent {
public:
    Parent(const std::string& name)
        : m_name(name),
          m_children()
    {}
    ~Parent();
    void addChild(std::shared_ptr<Child>& child) {
        m_children.push_back(child);
    }
    const std::string&
    getName() const {
        return m_name;
    }
    std::vector<std::shared_ptr<Child>>&
    getChildren() {
        return m_children;
    }
private:
    std::string m_name;
    std::vector<std::shared_ptr<Child>> m_children; // Parent对象使用shared_ptr来持有Child对象
};
class Child {
public:
    Child(const std::string& name, std::shared_ptr<Parent>& parent)
        : m_name(name),
          m_parent(parent)
    {}
    ~Child() {
        std::cout << m_name << "'s destruction" << std::endl;
    }
    void showParentName() const {
        std::shared_ptr<Parent> parent = m_parent.lock();
        if (parent) {
            std::cout << m_name << "'s parent: " << parent->getName() << std::endl;
        } else {
            std::cout << m_name << "'s parent has destructed" << std::endl;
        }
    }
private:
    std::string m_name;
    std::weak_ptr<Parent> m_parent; // Child对象使用weak_ptr来引用Parent对象
};
Parent::~Parent() {
    std::cout << m_name << "'s destruction" << std::endl;
}
void func() {
    std::shared_ptr<Parent> parent = std::make_shared<Parent>("Parent01");
    std::shared_ptr<Child> child = std::make_shared<Child>("Child01", parent);
    parent->addChild(child);
    child->showParentName();
}
int main() {
    func();
}
// Output:
//  Child01's parent: Parent01
//  Parent01's destruction
//  Child01's destruction

我们可以看到ParentChild对象均正常析构了。

std::weak_ptr与其绑定的std::shared_ptr

在上面的代码中,如果有其他地方持有std::shared_ptr<Child>,那么在Parent析构时,被该std::share_ptr<Child>持有的Child对象不会析构,而且Child::showParentName会正常识别出其Parent对象已经被析构。这就是std::weak_ptr能判断其绑定的std::shared_ptr管理的对象是否已经析构。
但有一个问题,如果我只是用std::weak_ptr来判断其绑定的std::shared_ptr管理的对象是否已经析构,但其绑定的std::shared_ptr管理的对象类型不一定怎么办?正如标题所言,std::weak_ptr<void>可以绑定到所有类型的std::shared_ptr,所以只要使用一个std::weak_ptr即可。
我知道这个用法的来源是陈硕的muduo网络库。
muduo中,类Channel用于管理一个socket描述符的读、写、出错事件,并调用相应的回调。
但有一个问题是Channel类并不持有该socket描述符(只存有该socket描述符,但其生命期并不归Channel管理),那如何判断Channel对应的管理socket描述符的类是否已经析构呢(因为Channel的读写出错回调往往是通过std::bind或者lambda包裹的socket描述符的持有者的private方法,如果持有者已经析构,再调用回调会导致段错误从而core dump)?
muduo就是在Channel中使用std::weak_ptr<void>。其有一个方法Channel::tie,接受const std::shared_ptr<void>&类型的参数,此参数要求传入持有socket描述符管理者对象的std::shared_ptrmuduo将此参数赋值给给std::weak_ptr<void>对象,使其可以监控socket描述符管理者对象是否已经析构。部分代码如下:

// muduo/net/Channel.cc
void Channel::tie(const std::shared_ptr<void>& obj)
{
  tie_ = obj;   // std::weak_ptr<void> tie_
  tied_ = true; // bool tied_
}
void Channel::handleEvent(Timestamp receiveTime)
{
  std::shared_ptr<void> guard;
  if (tied_)
  {
    guard = tie_.lock();
    if (guard)
    {
      handleEventWithGuard(receiveTime);
    }
  }
  else
  {
    handleEventWithGuard(receiveTime);
  }
}

这样的用法是合法的吗?我们可以在cppreference上查看一下std::shared_ptr和std::weak_ptr的相关信息。
可以看到std::shared_ptr有如下的构造函数:

// https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr
template<typename T>    // 这两行是我自己加的,
class std::shared_ptr { // 说明里面是该类的成员函数
// ...  (1) - (2)
template< class Y >
explicit shared_ptr( Y* ptr ); // (3)
// ... (4)-(13)
};

可以看到可以由std::shared_ptr<Y>构造std::shared_ptr<T>,要求是:

For (3-4,6), Y* must be convertible to T*. // until C++17

也就是只要T*能转化为Y*即可,而一般的指针类型(除了成员指针和成员函数指针)都可以转化为void*,所以std::shared_ptr<T>构造std::shared_ptr<void>是可以的,而且他们管理着相同的对象。测试如下:

#include <iostream>
#include <memory>
class Test {
public:
    ~Test() {
        std::cout << "Test::~Test()" << std::endl;
    }
};
int main() {
    std::shared_ptr<void> pvoid;
    {
        std::shared_ptr<Test> pTest = std::make_shared<Test>();
        pvoid = pTest;
    }
    std::cout << "pTest has destructed" << std::endl;
}
// Output:
//  pTest has destructed
//  Test::~Test()

然后std::shared_ptr<void>构造std::weak_ptr<void>就是理所当然的了。
那能不能由std::shared_ptr<T>直接构造std::weak_ptr<void>呢?按理来说是可以的,我们在cppreference里面找一下可以发现:

// https://en.cppreference.com/w/cpp/memory/weak_ptr/weak_ptr
template<T>      // 这两行是我自己加的,
std::weak_ptr {  // 说明里面是该类的成员函数
    template< class Y >
    weak_ptr( const std::shared_ptr<Y>& r ) noexcept;
};

The templated overloads don't participate in the overload resolution unless Y* is implicitly convertible to T*

即如果Y*能隐式转化为T*的话是可以的,而我们知道一般的指针类型(除了成员指针和成员函数指针)都可以隐式转化为void*类型,所以由std::share_ptr<T>构造std::weak_ptr<void>是可行的。验证如下:

#include <iostream>
#include <memory>
class Test {
public:
    ~Test() {
        std::cout << "Test::~Test()" << std::endl;
    }
};
std::weak_ptr<void> func() {
    std::shared_ptr<Test> pTest = std::make_shared<Test>();
    std::weak_ptr<void> pVoidWeak = pTest;
    std::shared_ptr<void> pVoid = pVoidWeak.lock();
    if (pVoid) {
        std::cout << "Test object exists" << std::endl;
    } else {
        std::cout << "Test object has been destructed" << std::endl;
    }
    return pVoidWeak;
}
int main() {
    auto pVoidWeak = func();
    std::shared_ptr<void> pVoid = pVoidWeak.lock();
    if (pVoid) {
        std::cout << "Test object exists" << std::endl;
    } else {
        std::cout << "Test object has been destructed" << std::endl;
    }
}
// Output:
//  Test object exists
//  Test::~Test()
//  Test object has been destructed

发表回复