如何区分std::shared_ptr通过make_shared创建与绑定已实例化原始指针的状态
你提到的问题确实很有意思——常规的if (sp)或者检查sp.get() != nullptr只能判断shared_ptr是否持有有效指针,但没法区分它是通过make_shared直接创建的对象,还是绑定了一个预先用new分配好的原始指针。毕竟这两种情况里,shared_ptr的布尔值都是true,get()也都返回非空地址。
为什么常规检测无效?
先回顾你的测试案例:
- 案例1(
make_shared):shared_ptr会分配一块连续内存,同时存放对象和引用计数控制块,最终持有指向对象的有效指针。 - 案例3(绑定外部
new的指针):shared_ptr会单独分配控制块,对象则存放在之前独立分配的堆内存中,所以get()返回的是外部指针的地址,同样有效。
这两种场景下,shared_ptr的"非空状态"表现完全一致,所以常规方法无法区分。
可行的区分方案
需要说明的是:C++标准并没有提供直接的API来区分这两种情况,因为这属于shared_ptr的内部实现细节。但我们可以通过两种思路来实现检测:
1. 利用控制块与对象的内存连续性(依赖编译器实现)
make_shared的核心优化是将控制块和对象分配在同一块连续内存中,而绑定外部指针时,控制块和对象是分开的两块内存。以GCC/Clang的实现为例,控制块位于对象内存的前方,我们可以通过计算地址来判断:
#include <iostream> #include <memory> // 仅适用于GCC/Clang的实现,不保证跨编译器兼容 template<typename T> bool is_made_via_shared(const std::shared_ptr<T>& sp) { if (!sp) return false; // 获取控制块地址(GCC中,控制块位于对象内存的前方) using ControlBlockType = std::_Sp_counted_ptr_inplace<T, std::allocator<T>, __gnu_cxx::_Lock_policy::_S_atomic>; auto* control_block = reinterpret_cast<void*>(reinterpret_cast<char*>(sp.get()) - sizeof(ControlBlockType)); // 检查shared_ptr的控制块是否指向该地址 return *reinterpret_cast<void**>(&sp) == control_block; } class Entity {}; int main() { auto es = std::make_shared<Entity>(); auto raw_ptr = new Entity; auto es3 = std::shared_ptr<Entity>(raw_ptr); std::cout << "es is made via make_shared: " << std::boolalpha << is_made_via_shared(es) << std::endl; // 输出true std::cout << "es3 is made via make_shared: " << std::boolalpha << is_made_via_shared(es3) << std::endl; // 输出false return 0; }
⚠️ 注意:这种方法依赖特定编译器的shared_ptr实现,换用MSVC等其他编译器可能完全失效,只适合调试或特定环境下使用。
2. 自定义类构造标记(可移植,但需修改类)
如果Entity是你自己维护的类,可以添加一个成员变量来标记对象的创建方式:
#include <iostream> #include <memory> class Entity { private: bool created_via_make_shared_ = false; // 让make_shared能访问私有构造逻辑 friend std::shared_ptr<Entity> std::make_shared<Entity>(); public: Entity() = default; // 用于外部new调用的构造函数,标记为非make_shared创建 explicit Entity(bool) : created_via_make_shared_(false) {} bool is_made_via_shared() const { return created_via_make_shared_; } }; // 特化make_shared的逻辑,标记为make_shared创建 template<> std::shared_ptr<Entity> std::make_shared<Entity>() { auto ptr = new Entity; ptr->created_via_make_shared_ = true; return std::shared_ptr<Entity>(ptr); } int main() { auto es = std::make_shared<Entity>(); auto raw_ptr = new Entity(true); auto es3 = std::shared_ptr<Entity>(raw_ptr); std::cout << "es is made via make_shared: " << es->is_made_via_shared() << std::endl; // true std::cout << "es3 is made via make_shared: " << es3->is_made_via_shared() << std::endl; // false return 0; }
这种方法完全符合标准,可移植性强,但需要你能修改目标类的代码,并且要注意避免外部代码绕过标记逻辑。
总结
如果不需要跨编译器,且只是用于调试或特定场景,可以用内存连续性检测的方法;如果需要可移植性,且能修改目标类,自定义构造标记是更稳妥的选择。但要注意,C++标准并不要求shared_ptr必须用连续内存实现make_shared,所以第一种方法始终存在兼容性风险。
内容的提问来源于stack exchange,提问作者didjek

