继承场景下,map插入unique_ptr失败时如何保留所有权?
问题解析:try_emplace插入unique_ptr到map,抽象类模板场景下插入失败无法保留所有权
问题背景
此前问题《Insert unique_ptr in a map and keep ownership of unique_ptr on failed insertion》已解决,现扩展场景:实现addToMap方法,接收string类型键和unique_ptr右值引用,通过try_emplace插入到map,要求插入失败时调用者保留所有权。该方案在unique_ptr持有普通类型(如int)时正常,但以抽象类作为模板参数时失效。
示例代码:
#include <map> #include <string> #include <memory> #include <iostream> template<typename T> struct Foo { void addToMap(const std::string& key, std::unique_ptr<T>&& value) { _map.try_emplace(key, std::move(value)); std::cout << "Inside " << value << std::endl; // 预期插入失败时value非空且可用 } std::map<std::string, std::unique_ptr<T>> _map; }; class IBar { public: virtual ~IBar() = default; virtual void Zoo() = 0; }; class BarImp : public IBar { public: ~BarImp() override = default; void Zoo() override { } }; int main() { { auto a = std::make_unique<int>(0); auto b = std::make_unique<int>(1); Foo<int> foo; foo.addToMap("0", std::move(a)); foo.addToMap("0", std::move(b)); std::cout << "Outside " << a << std::endl; std::cout << "Outside " << b << std::endl; } std::cout << std::endl; { auto a = std::make_unique<BarImp>(); auto b = std::make_unique<BarImp>(); Foo<IBar> foo; foo.addToMap("0", std::move(a)); foo.addToMap("0", std::move(b)); std::cout << "Outside " << a << std::endl; std::cout << "Outside " << b << std::endl; } }
示例输出:
Inside 00000000 Inside 010F3F88 Outside 00000000 Outside 010F3F88 Inside 00000000 Inside 010F26D0 Outside 00000000 Outside 00000000
可见,继承场景下最后一行Outside输出为空,不符合预期。
原因分析
问题核心在于函数调用时的隐式类型转换:
- 当模板参数
T为抽象类IBar时,调用addToMap传入的是std::unique_ptr<BarImp>右值,而函数参数要求std::unique_ptr<IBar>&&。此时会触发隐式转换,将std::unique_ptr<BarImp>转换为std::unique_ptr<IBar>的临时对象,这个转换会立即把原unique_ptr(b)的所有权转移到临时对象中。 - 无论后续
try_emplace插入是否成功,原b已经为空;插入失败时,临时对象会被销毁,导致所有权彻底丢失。 - 而
int等非继承类场景下,无需类型转换,std::move(b)直接作为右值引用传给参数value,try_emplace仅在插入成功时才会转移所有权,失败时所有权保留在原对象中。
修正方案
方案1:模板化addToMap参数,避免提前类型转换
修改Foo的addToMap方法,使其接受任意类型U的unique_ptr,让try_emplace仅在插入成功时才执行类型转换和所有权转移:
template<typename T> struct Foo { template<typename U> void addToMap(const std::string& key, std::unique_ptr<U>&& value) { auto [it, inserted] = _map.try_emplace(key, std::move(value)); if (inserted) { std::cout << "Inside (success) " << value << std::endl; } else { std::cout << "Inside (failed) " << value << std::endl; } } std::map<std::string, std::unique_ptr<T>> _map; };
此方案中,std::move(value)仅在插入成功时才会被用来构造std::unique_ptr<T>,插入失败时value仍持有所有权,原调用者的b不会被提前清空。
修正后输出:
Inside (success) 00000000 Inside (failed) 010F3F88 Outside 00000000 Outside 010F3F88 Inside (success) 00000000 Inside (failed) 010F26D0 Outside 00000000 Outside 010F26D0
方案2:显式转换+提前检查(单线程场景)
调用者先显式转换unique_ptr类型,再检查map中是否存在目标键,仅在不存在时执行插入:
// main函数中继承场景的代码修改 { auto a = std::make_unique<BarImp>(); auto b = std::make_unique<BarImp>(); Foo<IBar> foo; foo.addToMap("0", std::unique_ptr<IBar>(std::move(a))); auto b_ptr = std::unique_ptr<IBar>(std::move(b)); if (foo._map.find("0") == foo._map.end()) { foo._map.emplace("0", std::move(b_ptr)); } // 若插入失败,可将b_ptr转回BarImp类型 if (b_ptr) { b = std::unique_ptr<BarImp>(static_cast<BarImp*>(b_ptr.release())); } std::cout << "Outside " << a << std::endl; std::cout << "Outside " << b << std::endl; }
此方案需调用者手动处理类型转换和插入逻辑,适合单线程场景,多线程下存在竞态风险。
内容的提问来源于stack exchange,提问作者RoQuOTriX
相关产品推荐
相关产品推荐

