基于std::variant的variadic_callable类析构函数被调用两次问题排查
问题:variadic_callable对象析构函数被调用两次的原因及解决方法
我尝试用可变参数的variadic_callable类结合std::variant与访问者模式实现运行时多态,但遇到了异常情况:对象的析构函数被调用了两次。更奇怪的是,第一次析构调用后std::string的资源并未被销毁。
测试代码
#include <utility> #include <variant> #include <tuple> namespace detail { template<typename... Impl> class variadic_callable { public: template<typename T> constexpr explicit variadic_callable(T &&t) : varImpl_(std::forward<T>(t)) {} variadic_callable(const variadic_callable &) = delete; variadic_callable(variadic_callable &&) = delete; template<typename... Args> constexpr decltype(auto) operator()(Args &&...args) const { return std::visit( [argsTuple = std::forward_as_tuple(args...)](const auto &visitor) { return std::apply( [&visitor](auto &&...args) { return visitor(std::forward<decltype(args)>(args)...); }, argsTuple); }, varImpl_); } private: std::variant<Impl...> varImpl_; }; } // namespace detail #include <string> #include <iostream> int main(int, char **) { struct callable { std::string str = "Long enough string to be allocated. Oceanic"; callable() { std::cout << "callable()" << std::endl; } void operator()(int i) const { std::cout << str << " " << i << '\n'; } ~callable() { std::cout << "~callable()" << std::endl; } }; { std::cout << "expected:\n"; const auto &c = callable(); c(815); std::cout << "finished\n"; } std::cout << '\n'; { std::cout << "actual\n"; const auto &w = detail::variadic_callable<callable>{callable()}; w(815); std::cout << "finished\n"; } }
程序输出
Program returned: 0 expected: callable() Long enough string to be allocated. Oceanic 815 finished ~callable() actual callable() ~callable() Long enough string to be allocated. Oceanic 815 finished ~callable()
原因分析
两次析构的来源:
- 第一次析构:
detail::variadic_callable<callable>{callable()}中的callable()是临时对象,它作为右值传入variadic_callable的构造函数。由于该临时对象未被绑定到能延长其生命周期的引用(仅临时对象直接绑定到const左值引用时才会延长生命周期),variadic_callable构造完成后,这个临时对象会立即被析构。 - 第二次析构:
variadic_callable内部的std::variant<callable>成员varImpl_中存储了一个通过移动构造得到的callable实例(std::forward<T>(t)将临时对象作为右值转发,触发callable的默认移动构造函数),当w的生命周期结束时,这个实例会被析构。
- 第一次析构:
std::string资源未销毁的原因:
callable的默认移动构造函数会将临时对象中std::string的资源转移到varImpl_内的实例中,临时对象的std::string会变为空状态。因此第一次析构临时对象时,空的std::string不会释放任何资源;只有第二次析构varImpl_内的实例时,真正持有资源的std::string才会释放内存。
解决方法
要避免临时对象的额外析构,可直接在variadic_callable的std::variant成员中就地构造callable实例,而非传入临时对象。通过为variadic_callable添加支持就地构造的构造函数实现:
namespace detail { template<typename... Impl> class variadic_callable { public: // 原有的转发构造函数 template<typename T> constexpr explicit variadic_callable(T &&t) : varImpl_(std::forward<T>(t)) {} // 添加就地构造的构造函数 template<typename ImplType, typename... Args> constexpr explicit variadic_callable(std::in_place_type_t<ImplType>, Args&&... args) : varImpl_(std::in_place_type<ImplType>, std::forward<Args>(args)...) {} variadic_callable(const variadic_callable &) = delete; variadic_callable(variadic_callable &&) = delete; template<typename... Args> constexpr decltype(auto) operator()(Args &&...args) const { return std::visit( [argsTuple = std::forward_as_tuple(args...)](const auto &visitor) { return std::apply( [&visitor](auto &&...args) { return visitor(std::forward<decltype(args)>(args)...); }, argsTuple); }, varImpl_); } private: std::variant<Impl...> varImpl_; }; } // namespace detail
然后在main中使用就地构造的方式创建对象:
{ std::cout << "fixed actual\n"; const auto &w = detail::variadic_callable<callable>{std::in_place_type<callable>}; w(815); std::cout << "finished\n"; }
修改后的输出会与预期一致:
fixed actual callable() Long enough string to be allocated. Oceanic 815 finished ~callable()
内容的提问来源于stack exchange,提问作者Sergey Kolesnik
相关产品推荐
相关产品推荐

