shared_ptr源码解读

本文对shared_ptr源码进行解读。auto_ptr无法满足标准容器要求,而shared_ptr解决了多指针间共享对象所有权问题,可安全放入容器。文中分析了其实现过程,包括构造、拷贝、析构等,还探讨了线程安全、效率及对特定资源的管理,最后得出shared_ptr实用且安全的结论。

shared_ptr源码解读

rayleahaan@hotmail.com

1  介绍

众所周知,auto_ptr由于它的破坏性复制语义,无法满足标准容器对元素的要求,因而不能放在标准容器中;如果我们希望当容器析构时能自动把它容纳的指针元素所指的对象删除时,通常采用一些间接的方式来实现,显得比较繁琐。boost库中提供了一种新型的智能指针 shared_ptr,它解决了在多个指针间共享对象所有权的问题,同时也满足容器对元素的要求,因而可以安全地放入容器中。现在我们可以这样写:

vector< shared_ptr<Book> > books;
books.push_back( shared_ptr<Book>(new Book()) );

2  实现

shared_ptr是怎么实现的?让我们分析一下它的源码吧:
按照上面代码,在创建了Book对象之后,会创建一个临时的shared_ptr<Book>对象,shared_ptr的构造函数被调用:

template<class Y>
explicit shared_ptr(Y * p)
  : px(p), pn(p, checked_deleter<Y>())
{
    detail::sp_enable_shared_from_this(p, p, pn);
}

显然这是个模板函数,推导出的Y应该为Book类型,px和pn为shared_ptr的成员变量,其声明如下:

template<class T> class shared_ptr
{
private:
    T * px;                     // contained pointer
    detail::shared_count pn;        // reference counter
}

由于我们是用Book为模板参数实例化shared_ptr(shared_ptr<Book>),px应该为 Book* 类型,px(p)把px初始化为指向创建的Book 对象。类型为shared_count的pn成员变量以两个参数初始化,一个是传入的Book对象指针,另一个是一个临时的  check_deleter<Book> 对象,shared_count的构造函数被调用:

template<class P, class D>
shared_count(P p, D d)
: pi_(0)
{
    try
    {
        pi_ = new sp_counted_base_impl<P, D>(p, d);
    }
    catch(...)
    {
        d(p); // delete p
        throw;
    }
}

应该推导出P为Book*类型,D为checked_deleter<Book>类型。在shared_count的构造函数体,创建了一个  sp_counted_base_impl<P,D> 的对象,以传入的 Book 对象指针和
 check_deleter<Book> 对象为构造函数参数。看起来 sp_counted_base_impl 像是一个实现类:

template<class P, class D>
class sp_counted_base_impl: public sp_counted_base
{
private:
    P ptr; // copy constructor must not throw
    D del; // copy constructor must not throw
public:
    sp_counted_base_impl(P p, D d): ptr(p), del(d)
    {
    }
};
sp_counted_base的构造函数应该先被调用:
class sp_counted_base {
public:
    sp_counted_base(): use_count_(1), weak_count_(1)
    {
    }
private:
    long use_count_;        // #shared
    long weak_count_;       // #weak + (#shared != 0)
};

看来sp_counted_base是保存引用计数的地方,这里use_count_和weak_count_被初始化为1。接下来  sp_counted_base_impl 的构造函数保存了 Book 对象的指针和 check_deleter<Book> 对象。用类图把几个主要类之间的关系表示出来:

当books.push_back被调用时,应该发生一次拷贝构造:

template<class Y>
shared_ptr(shared_ptr<Y> const & r)
    : px(r.px),pn(r.pn) // never throws
{ }
pn为shared_count类型,它的拷贝构造函数又被调用:
shared_count(shared_count const & r)
    : pi_(r.pi_) // nothrow
{
    if(pi_!= 0) pi_->add_ref_copy();
}

可见新创建的shared_ptr对象和被拷贝的shared_ptr对象拥有了指向同一个 sp_counted_base 对象的指针,并通过调用  sp_count_base 的add_ref_copy函数把引用计数加一:

void add_ref_copy() { ++use_count_; }

用对象图把此时刻的对象关系表示出来:

当临时share_ptr对象被复制到容器中后,它就应该被销毁了,由于没有显式定义析构函数,编译器生成的析构函数会析构pn成员:

~shared_count() // nothrow
{
    if(pi_ != 0) pi_->release();
}
shared_counted_base的release函数:
void release() // nothrow
{
    {
        long new_use_count = --use_count_;
       if(new_use_count != 0) return;
    }
    dispose();
    weak_release();
}

这里把sp_counted_base保存的引用计数减一,引用计数减为1而返回。于是临时创建的shared_ptr对象被销毁了,但是Book对象并没有被销毁。当vector中的shared_ptr对象也被析构时,引用计数会减为0,于是执行dispose函数:

virtual void dispose() = 0; // nothrow

纯虚函数?别忘了pi_实际指向的对象类型为 sp_counted_base_impl ,所以 sp_counted_base_impl 的  dispose 函数被调用:

virtual void dispose() // nothrow
{
    del(ptr);
}

根据前面分析,del是一个check_deleter<Book>对象:

template<class T>
struct checked_deleter {
    typedef void result_type;
    typedef T * argument_type;
    void operator()(T * x) const
    {
        // boost:: disables ADL
        boost::checked_delete(x);
    }
};

好像是个function object,传入了Book对象的指针,通过调用 check_delete 函数从而删除了 shared_ptr 管理的Book对象。可以看到check_deleter没有任何成员变量,因而上面发生的几次拷贝构造应该是没有开销的。sp_counted_basesp_counted_base_impl好像用了什么模式?对了,就是template method,它使引用计数的算法独立于对象删除的实现细节而被重用。
shared_ptr是线程安全的吗?是的,它使用了一种轻量级的mutex做同步,为简单起见我没有列出相关的代码,实际是比较简单的。至于效率,好像多了很多操作,会不会比较慢?经验告诉我们:不要做事先优化。当真正发现性能瓶颈时再去做优化会更有的放矢。对于一些资源,以指针方式表示,但需要以特定的方式释放,比如文件句柄,shared_ptr有没有办法管理?答案是肯定的,你只需要提供一个函数或function object来实现那个特定的释放逻辑,然后在构造shared_ptr对象时传进去就可以了:

void file_closer(FILE* f) { fclose(f); }
shared_ptr<FILE> file(fopen("/a.txt", "r"), file_closer);

这里,shared_ptr的第二个模板构造函数就起作用了:

template<class Y,
class D> shared_ptr(Y * p, D d)
    : px(p), pn(p, d)
{
    detail::sp_enable_shared_from_this(p, p, pn);
}

推导出D应该为void(*)(FILE*)函数指针类型,当在该文件句柄上的引用计数为0时,file_closer就会被调用,从而关闭文件。

3  结论

shared_ptr解决了在多个指针间共享对象所有权的问题,它可以安全的放入标准容器,并且是线程安全的。

Last modified: Saturday, March 26th, 2005
<think> 根据用户的问题,他是在添加libtorch1.8.0时遇到了一个无法解析的外部符号错误,具体是c10::Error::Error的构造函数。这个问题通常与链接库的配置有关。下面将详细分析并提供解决方案。 ### 问题分析 1. **错误信息解读**: ``` 无法解析的外部符号 '__declspec(dllimport) public: __cdecl c10::Error::Error(struct c10::SourceLocation,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >)' ``` 这个错误表明在链接阶段,编译器找不到`c10::Error::Error`这个函数的实现。这个函数是libtorch中用于抛出异常的一个类。 2. **可能的原因**: - **库文件未正确链接**:在链接器配置中,可能没有添加包含该符号的库文件(如`c10.lib`或`torch.lib`)。 - **库路径未设置**:虽然添加了库名,但链接器找不到库文件的位置。 - **运行时库冲突**:引用的库可能是用不同版本的运行时库(如MT/MD)编译的,导致不兼容。 - **libtorch版本不匹配**:使用的libtorch可能是debug/release版本与当前项目配置不一致。 ### 解决方案 #### 1. 确保链接正确的库 - 在链接器配置中,添加以下库(以Release版本为例): ``` c10.lib torch.lib torch_cpu.lib ``` - 如果是Debug版本,则使用带有`d`后缀的库,如: ``` c10d.lib torchd.lib torch_cpud.lib ``` #### 2. 设置库路径 - 在VS的项目属性中,确保库路径(`Library Directories`)已经正确设置,指向libtorch的`lib`文件夹。例如: ``` C:\libtorch\lib ``` #### 3. 检查运行库配置 - 在项目属性 → C/C++ → 代码生成 → 运行库,确保与libtorch编译时使用的运行库一致。通常,如果使用预编译的libtorch,它默认使用MD(动态链接)或MDd(Debug动态链接)。因此,项目配置应为: - Release: `MD` - Debug: `MDd` #### 4. 检查libtorch版本 - 确保下载的libtorch版本与项目配置(Release/Debug)一致。如果项目是Debug模式,必须使用Debug版本的libtorch。 #### 5. 添加预处理器定义 - 在项目属性 → C/C++ → 预处理器 → 预处理器定义中,添加: ``` TORCH_API_MODE_LEGACY ``` 这个定义有时可以解决符号导入问题。 #### 6. 检查头文件包含 - 确保已经包含了必要的头文件,并且路径正确。在C/C++ → 附加包含目录中添加libtorch的include目录,例如: ``` C:\libtorch\include C:\libtorch\include\torch\csrc\api\include ``` #### 7. 配置示例(以VS2019为例) - **包含目录**: ``` C:\libtorch\include C:\libtorch\include\torch\csrc\api\include ``` - **库目录**: ``` C:\libtorch\lib ``` - **附加依赖项**(Debug配置): ``` c10d.lib;torchd.lib;torch_cpud.lib; ``` - **运行库**:`MDd`(Debug)或`MD`(Release) ### 验证步骤 1. 创建一个简单的测试程序: ```cpp #include <torch/torch.h> int main() { torch::Tensor tensor = torch::rand({2, 3}); std::cout << tensor << std::endl; return 0; } ``` 2. 按照上述配置设置项目属性。 3. 编译并运行,确保没有链接错误。 ### 常见问题 - **问题:** 如果出现其他链接错误(如`c10::detail::torchCheckFail`),同样需要检查上述配置。 - **注意:** 在Debug模式下,必须使用带`d`后缀的库,否则会出现不匹配。 ### 引用说明 在配置libtorch时,链接器错误通常是由于库配置不正确或运行库不匹配引起[^1]。确保所有配置一致,并正确指向libtorch的安装路径。 §§相关问题§§ 1. 如何验证libtorch是否正确安装? 2. 在CMake项目中如何配置libtorch? 3. 如何解决libtorch中的其他常见链接错误? 4. 如何从源码编译libtorch? </think>### 解决 libtorch 1.8.0 链接错误:`无法解析的外部符号 c10::Error::Error` #### 📌 错误原因分析 此链接错误通常由以下原因导致: 1. **库文件链接顺序不正确** libtorch 库需要按特定顺序链接,特别是 `c10.lib` 必须放在其他 Torch 库之前[^1] 2. **Debug/Release 版本不匹配** 使用 Debug 配置时未链接带 `d` 后缀的库(如 `c10d.lib`),或 Release 配置错误链接了 Debug 库 3. **运行时库冲突** 项目设置的运行时库(`/MT` vs `/MD`)与 libtorch 编译选项不兼容 4. **ABI 兼容性问题** 使用 `_GLIBCXX_USE_CXX11_ABI=0` 编译的库与使用新 ABI 的项目链接 --- ### 🛠️ 解决方案步骤 #### 1. 调整库链接顺序(关键步骤) 在链接器输入中**确保以下顺序**: ```ini c10.lib;torch.lib;torch_cpu.lib; # Release 配置 ``` 或 Debug 配置: ```ini c10d.lib;torchd.lib;torch_cpud.lib; # Debug 配置 ``` #### 2. 配置项目属性(Visual Studio) | 设置项 | Release 配置 | Debug 配置 | |--------|--------------|-----------| | **附加依赖项** | `c10.lib;torch.lib;torch_cpu.lib;` | `c10d.lib;torchd.lib;torch_cpud.lib;` | | **C++ → 代码生成 → 运行时库** | `/MD` 或 `/MDd` | `/MDd`(必须匹配) | | **C++ → 语言 → C++语言标准** | ≥ C++14 | 同上 | | **链接器 → 输入 → 忽略特定库** | `msvcrt.lib;libcmt.lib` | `msvcrtd.lib;libcmtd.lib` | #### 3. 添加预处理器定义 在 **C/C++ → 预处理器 → 预处理器定义** 中添加: ``` _CRT_SECURE_NO_WARNINGS ``` #### 4. 完整配置示例(CMake) ```cmake cmake_minimum_required(VERSION 3.18) project(libtorch_demo) # 设置 libtorch 路径 set(CMAKE_PREFIX_PATH "C:/libtorch") find_package(Torch REQUIRED) # 关键:设置 C++14 标准 set(CMAKE_CXX_STANDARD 14) # 添加可执行文件 add_executable(main main.cpp) # 链接库(注意顺序) target_link_libraries(main ${TORCH_LIBRARIES} # 自动包含正确顺序 ) # 解决 ABI 兼容性问题 if(MSVC) target_compile_definitions(main PRIVATE "_CRT_SECURE_NO_WARNINGS") else() target_compile_definitions(main PRIVATE "_GLIBCXX_USE_CXX11_ABI=1") endif() ``` #### 5. 环境变量配置 在系统环境变量中添加: ``` PATH=C:\libtorch\lib;%PATH% ``` --- ### ✅ 验证测试 创建测试文件 `main.cpp`: ```cpp #include <torch/torch.h> int main() { try { auto tensor = torch::randn({2, 3}); TORCH_CHECK(false, "Test error message"); } catch (const c10::Error& e) { std::cout << "Caught exception: " << e.what() << std::endl; } return 0; } ``` 编译并运行,应输出类似: ``` Caught exception: Test error message ``` --- ### ⚠️ 常见错误处理 1. **多个运行时库冲突** 在链接器设置中显式忽略冲突库: ``` msvcrt.lib;libcmt.lib;libcpmt.lib ``` 2. **Debug/Release 混合错误** 确保所有配置一致: ```cmake set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL") ``` 3. **CUDA 相关错误** 添加 CUDA 库路径: ```ini C:\libtorch\lib\c10_cuda.lib C:\libtorch\lib\torch_cuda.lib ``` > **关键点**:90% 的类似错误通过调整库链接顺序(`c10` 优先)和匹配 Debug/Release 配置解决[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值