1. 项目概述为什么需要管理C对象在Lua中的生命周期如果你正在用C写游戏引擎、高性能服务器或者任何需要脚本扩展能力的应用那么Lua大概率是你的老朋友了。而Sol2作为当下最流行、最现代的C - Lua绑定库它让“把C对象暴露给Lua”这件事变得前所未有的简单。但简单背后往往藏着最深的坑。我见过太多项目初期用Sol2绑几个类跑得飞快结果到了中后期内存泄漏、野指针、访问违例等问题层出不穷调试起来让人头皮发麻。问题的核心几乎都指向同一个地方对象生命周期管理的混乱。Lua有自己的垃圾回收GC机制而C对象的内存管理方式五花八门——可能是new/delete可能是std::shared_ptr也可能是栈上对象或者某个单例。当这两种截然不同的世界发生碰撞时谁来负责“生”构造/创建谁来负责“死”析构/销毁如果Lua还在引用一个已经被C删除的对象或者反过来C试图使用一个已被Lua回收的对象灾难就发生了。所以今天我们不谈简单的绑定而是深入Sol2的腹地系统性地拆解它提供的几种核心用户类型User Types。每一种类型都代表了一种不同的生命周期管理模式和所有权哲学。理解它们你就能像搭积木一样为你的C对象选择最合适的那一款“Lua外衣”从而构建出稳定、高效且内存安全的C/Lua混合编程环境。这不仅仅是调用几个API而是关乎你项目底层架构的健壮性。2. Sol2用户类型全景解析六种武器与它们的战场Sol2提供了丰富的用户类型包装器每种都对应着不同的语义和生命周期。盲目选择只会带来麻烦我们必须先看清它们的全貌。下面这个表格是我根据多年实战经验总结的“武器库清单”用户类型核心所有权Lua中表现生命周期关键点典型应用场景sol::usertypeTLua拥有值语义用户数据UserdataLua GC负责析构。C端必须提供可访问的拷贝/移动构造。轻量、自包含的值对象如Vector3、Color、Rect。sol::simple_usertypeTLua拥有值语义用户数据Userdatausertype的轻量版牺牲灵活性换取编译速度和代码体积优化。对编译时长敏感的项目中的简单值对象。sol::unique_usertypeTLua独占所有权用户数据Userdata移动语义对象在Lua间转移所有权不可复制。Lua GC负责析构。唯一、不可共享的资源如文件句柄、网络连接。sol::shared_ptr_usertypeTC/Lua共享所有权用户数据Userdata内部封装std::shared_ptrT。引用计数为0时析构。需要跨C/Lua多上下文共享的对象如角色、资源管理器。sol::pointer_usertypeTC拥有所有权轻量用户数据Light Userdata仅传递指针无生命周期管理Lua不负责析构。已知生命周期远超Lua引用的对象如全局单例、栈对象引用。sol::table_asTLua拥有表模拟Lua表Table用Lua表模拟对象GC管理表本身。C端需定义转换规则。需要高度动态性、或与现有Lua代码无缝交互的配置/数据对象。注意sol::usertype和sol::simple_usertype虽然都是值语义但simple_usertype在注册时方法更集中生成的模板代码更少适合大量简单类型的绑定能显著改善编译时间。但它不支持一些高级特性如从Lua元表继承。理解这张表是第一步。接下来我们将深入每一种类型通过代码和场景让你彻底掌握它们的使用心法和避坑指南。2.1sol::usertypeT经典的值对象容器这是Sol2中最常用、最直观的类型。它的设计哲学是值语义。当你把一个C对象推入Lua时Sol2会在Lua一侧分配一块新的用户数据userdata并将你的C对象拷贝或移动到这块内存中。从此这个对象的生杀大权就交给了Lua的垃圾回收器。// 示例一个简单的二维向量类 class Vec2 { public: float x, y; Vec2(float x, float y) : x(x), y(y) {} Vec2(const Vec2) default; // 必须可拷贝 ~Vec2() { std::cout Vec2 destroyed: ( x , y )\n; } float length() const { return std::sqrt(x*x y*y); } Vec2 normalized() const { float l length(); return l 0 ? Vec2(x/l, y/l) : Vec2(0,0); } }; // 在Lua中注册为 usertype sol::state lua; lua.open_libraries(sol::lib::base); lua.new_usertypeVec2(Vec2, sol::call_constructor, sol::constructorsVec2(float, float)(), x, Vec2::x, y, Vec2::y, length, Vec2::length, normalized, Vec2::normalized ); // Lua脚本 lua.script(R( local v Vec2(3, 4) print(v.x .. v.x) -- 输出v.x 3 print(v:length() .. v:length()) -- 输出v:length() 5 v nil -- 移除引用 collectgarbage(collect) -- 强制GC触发析构函数打印信息 ));核心要点与避坑必须可拷贝/移动这是最大的前提。如果你的类禁用了拷贝构造 delete或移动构造sol::usertype将无法工作。因为Sol2需要在Lua中构造一个副本。析构函数安全确保你的析构函数不会抛出异常并且执行迅速。它会在Lua GC的某个不确定时刻被调用长时间阻塞或崩溃会影响整个Lua状态。性能考量对于大型对象例如包含巨大std::vector的类每次在C和Lua间传递都发生拷贝开销巨大。这种情况下usertype不是好选择。引用与修改在Lua中修改v.x修改的是Lua userdata里的副本不会影响C端可能存在的原始对象。它们是两个独立的实例。实操心得usertype最适合那些“小而美”的纯数据或工具类比如数学向量、矩阵、颜色、矩形等。这些对象本身就应该按值传递和存储。在游戏开发中Vec3、Quaternion用usertype绑定是黄金标准。2.2sol::simple_usertypeT追求编译速度的轻量之选你可以把它理解为usertype的“青春版”或“特化版”。它的API更集中注册成员时不需要为每个成员单独指定类型从而减少了大量的模板实例化最终目的是加快编译速度尤其是在绑定大量简单类型时效果显著。// 使用 simple_usertype 绑定同样的 Vec2 类 lua.new_simple_usertypeVec2(Vec2, sol::call_constructor, sol::constructorsVec2(float, float)(), x, Vec2::x, y, Vec2::y, length, Vec2::length, normalized, Vec2::normalized // 注意所有属性/方法都在一个参数列表中声明 );从Lua脚本的使用角度来看simple_usertype和usertype完全一样。它们的区别在于C的编译期和二进制大小。核心要点与避坑功能限制simple_usertype不支持sol::base_classes继承、sol::factories复杂工厂函数等一些高级特性。如果你的类型需要从Lua中继承其他绑定类型请用回usertype。注册方式所有成员变量、函数必须在同一个new_simple_usertype调用中完成注册不像usertype可以后期用set或set_function添加。何时使用当你的项目有成千上万个简单的PODPlain Old Data或小型类需要绑定并且你被漫长的编译时间折磨时simple_usertype是你的救星。对于复杂类型还是优先考虑usertype。2.3sol::unique_usertypeT移动语义与独占所有权这个名字就揭示了它的本质唯一和独占。它模拟了C11的std::unique_ptr语义。绑定为unique_usertype的对象不能被复制只能在Lua内部或C与Lua之间移动。这完美契合了那些“唯一性”资源的管理。// 模拟一个唯一的文件句柄资源 class UniqueFile { std::ofstream file_; public: UniqueFile(const std::string path) : file_(path) { if (!file_) throw std::runtime_error(Cannot open file); std::cout UniqueFile opened: path std::endl; } ~UniqueFile() { std::cout UniqueFile closed.\n; } // 禁止拷贝 UniqueFile(const UniqueFile) delete; UniqueFile operator(const UniqueFile) delete; // 允许移动 UniqueFile(UniqueFile) default; UniqueFile operator(UniqueFile) default; void write(const std::string text) { file_ text; } }; sol::state lua; lua.open_libraries(sol::lib::base); // 注册为 unique_usertype注意构造函数可能抛异常需要安全调用 lua.new_usertypeUniqueFile(UniqueFile, sol::call_constructor, sol::factories([](const std::string path) { return std::make_uniqueUniqueFile(path); // 返回 unique_ptr }), write, UniqueFile::write ); lua.script(R( -- 正确创建唯一对象 local f1 UniqueFile(test.txt) f1:write(Hello ) -- 错误尝试赋值复制会导致编译/运行时错误 -- local f2 f1 -- 正确移动语义在Lua中赋值给新变量并置空原变量常伴随移动 -- 但更常见的场景是在函数间传递所有权随之转移 function getFile() local f UniqueFile(log.txt) f:write(Start Log) return f -- 所有权移出函数 end local myLog getFile() -- 所有权转移到 myLog -- 此时函数内的 f 已无效 myLog:write( End Log) myLog nil -- 引用消失GC会触发文件关闭 ));核心要点与避坑移动而非拷贝你的类必须支持移动语义或者本身就是std::unique_ptrT。拷贝构造函数必须被禁用。工厂函数由于std::make_unique返回的就是unique_ptr我们通常使用sol::factories来包装创建过程确保对象从一开始就被unique_ptr管理。Lua中的“移动”在Lua层面当你将一个unique_usertype变量赋值给另一个或者从函数返回时Sol2在底层执行的是移动操作。原变量会变成nil或空状态。你需要让Lua脚本编写者了解这一点。适用场景网络连接TcpConnection、GPU资源句柄、物理引擎中的刚体如果引擎不允许复制等。任何在逻辑上具有唯一性、不可共享的资源都是unique_usertype的用武之地。2.4sol::shared_ptr_usertypeT共享所有权的桥梁这是处理C/Lua混合编程中共享对象最强大、最安全的工具。它内部封装了一个std::shared_ptrT。当你在C端将一个shared_ptr推入Lua时Lua会持有这个shared_ptr的一份拷贝增加引用计数。反之亦然。只有当C端和Lua端的所有shared_ptr引用都消失时对象才会被销毁。class GameObject { std::string name_; public: GameObject(std::string name) : name_(std::move(name)) { std::cout GameObject name_ created.\n; } ~GameObject() { std::cout GameObject name_ destroyed.\n; } std::string getName() const { return name_; } void setName(const std::string name) { name_ name; } }; sol::state lua; lua.open_libraries(sol::lib::base); // 注册为 shared_ptr_usertype // 注意注册的是 GameObject但绑定后Lua操作的是 shared_ptrGameObject lua.new_usertypeGameObject(GameObject, sol::call_constructor, sol::constructorsstd::shared_ptrGameObject(std::string)(), getName, GameObject::getName, setName, GameObject::setName ); // C端创建共享对象 auto player std::make_sharedGameObject(Hero); lua[globalPlayer] player; // 传入Lua引用计数1 std::cout Ref count after pushing to Lua: player.use_count() std::endl; // 可能是2 lua.script(R( print(Lua sees: .. globalPlayer:getName()) -- 输出Lua sees: Hero globalPlayer:setName(SuperHero) -- Lua可以持有多个引用 local anotherRef globalPlayer anotherRef:setName(UltraHero) )); // C端依然可以访问并看到修改 std::cout C sees: player-getName() std::endl; // 输出C sees: UltraHero std::cout Ref count before clearing Lua: player.use_count() std::endl; // 清除Lua中的引用 lua[globalPlayer] sol::nil; lua[anotherRef] sol::nil; // 假设我们也能访问到另一个引用 lua.collect_garbage(); // 建议执行一次GC // 此时如果C的 player 也超出作用域对象就会被销毁核心要点与避坑循环引用这是shared_ptr的老问题在混合环境中更易发生。例如一个CGameObject的shared_ptr被Lua引用同时这个对象内部又有一个Lua回调函数sol::function而这个回调函数捕获了Lua中对这个对象的引用。这就构成了C-Lua-C的循环引用导致内存泄漏。解决方法是使用std::weak_ptr来打破循环。构造器注意注册构造器时类型是std::shared_ptrGameObject(...)而不是GameObject(...)。这告诉Sol2我们创建和传递的都是共享指针。性能开销shared_ptr的原子引用计数操作有轻微开销。对于生命周期极短或极其频繁创建销毁的小对象这可能成为瓶颈。但对于游戏中的角色、物品、管理器等主要实体这点开销通常是值得的。线程安全std::shared_ptr的引用计数操作是线程安全的但这不意味着对象本身的读写是安全的。在多线程环境下访问被Lua和C共享的对象仍需额外的同步机制。实操心得在大型游戏中shared_ptr_usertype是我的默认选择用于管理核心的游戏实体。它清晰地将所有权模型定义为“共享”避免了“谁该删除”的哲学争论。配合std::weak_ptr在需要的地方观察对象可以构建出非常健壮的对象关系网。2.5sol::pointer_usertypeT或轻量用户数据危险的匕首这是一把没有刀鞘的匕首极其锋利也极易伤己。pointer_usertype或直接传递原始指针在Lua中表现为lightuserdata不管理任何生命周期。它只是简单地将一个C指针的值传递给Lua。Lua将其视为一个不透明的“数字”GC不会对它进行任何析构操作。class SingletonManager { public: static SingletonManager getInstance() { static SingletonManager instance; return instance; } void doSomething() { std::cout Singleton working...\n; } private: SingletonManager() default; }; sol::state lua; lua.open_libraries(sol::lib::base); // 方法1直接传递指针成为 lightuserdata lua[singletonPtr] SingletonManager::getInstance(); // 需要额外绑定方法到一个通用的“函数”或通过元表操作比较麻烦 // 方法2注册为 usertype但以指针形式传递更常见 lua.new_usertypeSingletonManager(SingletonManager, doSomething, SingletonManager::doSomething // 不定义构造器防止Lua创建实例 ); // 将单例的指针赋值给一个全局名称 lua[singleton] SingletonManager::getInstance(); // 这里传递的是指针 lua.script(R( singleton:doSomething() -- 通过指针安全地调用方法 -- 但绝对不能做下面的事 -- singleton nil -- collectgarbage() -- 这不会删除C单例但之后如果Lua代码误以为它被管理了就会有问题。 ));核心要点与避坑绝对的生命周期保证你必须100%确定这个指针所指向的C对象的生命周期长于任何可能访问它的Lua代码的生命周期。全局单例、静态存储期对象、或者由更高层次系统如游戏世界明确管理的对象才适合。禁止删除永远不要在Lua侧尝试“释放”这个指针。Sol2没有提供默认的析构器如果你错误地将其当作普通usertype对待可能会导致未定义行为。使用场景极窄除了全局单例另一种场景是性能极度敏感的底层循环你明确知道对象在栈帧存活期间被使用。即便如此也需要万分小心。标识问题lightuserdata没有元表要让它能调用方法obj:method()你需要更复杂的设置通常不如直接包装成usertype方便。警告新手最大的坑就是图省事把所有对象都用指针形式传给Lua。项目初期跑得快等到对象被意外销毁Lua侧抛出“访问无效内存”的错误时调试将如同大海捞针。除非你有绝对把握否则不要使用轻量用户数据或原始指针传递。2.6sol::table_asT用Lua表模拟对象这是一种非常灵活的方式。它不直接绑定C对象而是定义一套规则在C的struct/class和Lua的table之间进行转换。对象以Lua表的形式存在由Lua GC管理。当你从Lua获取这个“对象”时Sol2会按规则将表的内容填充到一个C对象中可能是临时的。// 一个配置结构体 struct Config { int width 800; int height 600; std::string title My Game; bool fullscreen false; }; // 告诉Sol2如何与Lua表互转 namespace sol { template struct lua_type_ofConfig : std::integral_constantsol::type, sol::type::table {}; template struct lua_asConfig { static std::optionalConfig get(lua_State* L, int index, sol::stack::record tracking) { sol::table tbl sol::stack::getsol::table(L, index, tracking); Config cfg; cfg.width tbl.get_orint(width, cfg.width); cfg.height tbl.get_orint(height, cfg.height); cfg.title tbl.get_orstd::string(title, cfg.title); cfg.fullscreen tbl.get_orbool(fullscreen, cfg.fullscreen); return cfg; } }; template struct lua_pushConfig { static int push(lua_State* L, const Config cfg) { sol::table tbl sol::stack::push_sol_table(L); tbl[width] cfg.width; tbl[height] cfg.height; tbl[title] cfg.title; tbl[fullscreen] cfg.fullscreen; return 1; } }; } sol::state lua; lua.open_libraries(sol::lib::base); // 无需特殊注册Config 现在可以像基本类型一样传递 lua.script(R( -- 在Lua中直接创建“Config对象” local config { width 1920, height 1080, title Awesome Game, fullscreen true } -- 传递给C函数 someCFunction(config) )); // C端接收 lua.set_function(someCFunction, [](const Config cfg) { std::cout Config received: cfg.title ( cfg.width x cfg.height )\n; });核心要点与避坑非侵入式最大的优点是你不需要修改原有的C类也不需要它有什么特殊构造。你只是定义了转换规则。值语义每次都是拷贝每次在C/Lua边界传递都会发生一次拷贝构造。Lua中表的修改不会自动同步到C端除非你再次获取。这适用于配置、参数、消息等一次性或只读数据。灵活性高Lua表可以动态添加字段而你的C结构体在转换时可以只读取它关心的部分使用get_or提供默认值忽略其他字段兼容性很好。性能对于复杂的嵌套结构序列化/反序列化表会有开销。不适合在每帧调用的高性能循环中使用。适用场景游戏配置、UI样式定义、网络协议包、脚本函数的参数选项等。任何需要与Lua进行灵活、松散耦合数据交换的场景。3. 生命周期管理的实战策略与设计模式理解了六种武器我们进入更高阶的实战如何为你的项目设计一套合理的生命周期管理策略这不仅仅是选择类型更是关于架构的思考。3.1 策略一基于所有权的清晰划分这是最根本的原则。在项目设计初期就要明确每一类对象的“主人”是谁。Lua拥有的对象值语义使用sol::usertype或sol::simple_usertype。适用于自包含的、无外部依赖的工具类。例如MathUtility,Color,Rectangle。它们的生命周期完全由Lua脚本控制脚本用完即弃简单明了。C拥有的对象引用语义使用sol::shared_ptr_usertype。适用于游戏的核心实体如Player,Enemy,Item。这些对象的创建、逻辑更新、最终销毁都由C主导Lua只是持有引用并调用其方法或响应其事件。这是最常用、最安全的模式。唯一性资源使用sol::unique_usertype。适用于FileStream,DatabaseConnection,RenderTexture。确保资源不被意外共享所有权转移清晰。全局服务使用指针或引用包装谨慎使用pointer_usertype。适用于LogManager,AssetLoader,InputSystem。这些对象在程序启动时创建结束时销毁生命周期覆盖整个应用。3.2 策略二工厂模式与安全创建永远不要让Lua直接new一个由C复杂管理的对象。应该使用工厂函数。class Character; // 假设是一个复杂的类由C世界管理 using CharacterPtr std::shared_ptrCharacter; class CharacterManager { std::unordered_mapint, CharacterPtr characters_; public: CharacterPtr createCharacter(const std::string name) { auto id generateId(); auto char std::make_sharedCharacter(id, name); characters_[id] char; return char; } CharacterPtr getCharacter(int id) { /* ... */ } void destroyCharacter(int id) { /* ... */ } }; // 绑定 lua.new_usertypeCharacter(Character, ...); // 绑定方法 lua.new_usertypeCharacterManager(CharacterManager, createCharacter, CharacterManager::createCharacter, getCharacter, CharacterManager::getCharacter ); // 将全局管理器实例暴露给Lua lua[gCharacterMgr] CharacterManager::getInstance(); // Lua脚本 local hero gCharacterMgr:createCharacter(Arthur) hero:walkTo(100, 200) -- Lua不需要关心hero何时销毁由CharacterManager统一管理这样做的好处是创建逻辑集中化便于注入依赖、进行池化处理、分配ID、加入全局列表等。销毁也可以由管理器统一处理避免Lua忘记置空导致的内存泄漏对于shared_ptr即使Lua泄漏只要C管理器还持有引用对象就不会被过早销毁但管理器本身应有清理机制。3.3 策略三使用std::weak_ptr打破循环引用这是使用shared_ptr_usertype时必须掌握的技巧。当C对象持有Lua函数sol::function而该函数又捕获了对此C对象的引用时就形成了循环。class EventEmitter { public: std::functionvoid() onEvent; ~EventEmitter() { std::cout Emitter dead\n; } void trigger() { if(onEvent) onEvent(); } }; sol::state lua; lua.new_usertypeEventEmitter(EventEmitter, sol::call_constructor, sol::constructorsstd::shared_ptrEventEmitter()(), onEvent, EventEmitter::onEvent, // 暴露std::function危险 trigger, EventEmitter::trigger ); lua.script(R( local emitter EventEmitter() -- 循环引用形成 emitter.onEvent function() print(Event from: ) -- 这里隐式捕获了 emitter (一个 shared_ptr) end emitter nil -- 即使Lua引用消失因为C的function捕获了shared_ptr引用计数不为0内存泄漏 collectgarbage() -- EventEmitter 的析构函数永远不会被调用 ));解决方案让C端持有std::weak_ptr并在调用前尝试提升lock。class SafeEventEmitter { std::weak_ptrSafeEventEmitter self_; // 弱引用自身 sol::function luaCallback_; public: static std::shared_ptrSafeEventEmitter create() { auto ptr std::make_sharedSafeEventEmitter(); ptr-self_ ptr; // 设置弱引用 return ptr; } void setCallback(sol::function func) { luaCallback_ std::move(func); } void trigger() { auto shared_self self_.lock(); if (shared_self luaCallback_.valid()) { // 安全地调用Lua可以传递 shared_self 或 weak_self luaCallback_(shared_self); } } ~SafeEventEmitter() { std::cout SafeEmitter dead\n; } }; // 绑定 setCallback 方法而不是内部的 std::function在Lua侧回调函数可以接收这个weak_ptr或shared_ptr。关键是C对象不再直接持有指向自己的shared_ptr打破了循环。这是一个进阶话题但对于构建复杂的双向回调系统至关重要。4. 高级话题自定义析构器与内存追踪有时Sol2的默认行为可能不满足你的需求。例如你的对象需要特殊的清理逻辑如通知某个管理器或者你使用了自定义的内存分配器。4.1 自定义析构器你可以在注册usertype时指定一个自定义的析构函数。class ManagedObject { int id_; static inline std::setint aliveIds_; public: ManagedObject(int id) : id_(id) { aliveIds_.insert(id); std::cout Object id_ created.\n; } static void customDeleter(ManagedObject* ptr) { std::cout Custom deleter for object ptr-id_ std::endl; aliveIds_.erase(ptr-id_); delete ptr; // 最后还是要手动删除 } ~ManagedObject() { std::cout Destructor for object id_ std::endl; } // 这个也会被调用 }; lua.new_usertypeManagedObject(ManagedObject, sol::call_constructor, sol::constructorsManagedObject(int)(), sol::meta_function::garbage_collect, ManagedObject::customDeleter // 指定自定义析构器 );当Lua GC回收该userdata时会调用你提供的customDeleter而不是直接调用delete。你可以在其中执行额外的日志记录、资源返还等操作。注意你仍然需要负责最终的内存释放delete ptr。4.2 内存泄漏排查技巧混合环境下的内存泄漏更难排查。以下是一些实用技巧重载new/delete并记录在调试版本中为你需要追踪的类重载全局的operator new和operator delete并记录分配/释放的堆栈信息可用boost::stacktrace或平台相关API。使用Sol2的lua_gc定期在C端调用lua.collect_garbage()并观察内存变化。如果Lua内存只增不减很可能有对象被意外持有。弱表Weak Table跟踪在Lua中创建一个弱表setmetatable({}, {__mode v})每当创建一个重要的shared_ptr_usertype对象时就把它作为值放入弱表。弱表不会阻止GC。你可以定期遍历这个弱表如果发现某个对象还在弱表中说明它没有被GC但在你的C管理器中已经不存在了那就可能是C端泄漏。反之如果对象不在弱表中了已被GC但C管理器认为它还活着那就是Lua端引用已丢失但C端还持有shared_ptr需要检查逻辑。工具辅助使用Valgrind、Dr. Memory等内存检测工具它们对C内存管理非常有效。对于Lua部分可以使用Lua的collectgarbage(count)查看内存使用或使用像LuaJIT的-jv和-jp标志进行性能分析。5. 跨版本与兼容性考量Sol2是一个活跃的库不同版本间API可能有变化。生命周期管理相关的接口相对稳定但仍需注意API差异例如早期版本可能对unique_usertype的支持方式不同。始终查阅你所使用的Sol2版本的官方文档。Lua版本Sol2支持Lua 5.1和LuaJIT。确保你的生命周期管理策略与你使用的Lua版本兼容。例如LuaJIT的GC行为可能与标准Lua略有不同。C标准std::shared_ptr和std::unique_ptr的行为是标准化的但确保你的编译器对C11/14/17的支持是完整的。对于移动语义的支持是unique_usertype工作的基础。最后再分享一个我自己的体会生命周期管理没有银弹。在项目初期不妨保守一点对大多数动态创建的对象都使用shared_ptr_usertype虽然有一点开销但能避免绝大多数内存问题。当性能 profiling 指出这里是瓶颈时再考虑针对特定类型进行优化比如将频繁创建的、小的、短暂使用的对象改为usertype值语义或者使用对象池技术来管理shared_ptr的分配。清晰的架构和正确的模式选择远比后期艰难的调试和重构要划算得多。