You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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()

原因分析

  1. 两次析构的来源:

    • 第一次析构:detail::variadic_callable<callable>{callable()}中的callable()是临时对象,它作为右值传入variadic_callable的构造函数。由于该临时对象未被绑定到能延长其生命周期的引用(仅临时对象直接绑定到const左值引用时才会延长生命周期),variadic_callable构造完成后,这个临时对象会立即被析构。
    • 第二次析构:variadic_callable内部的std::variant<callable>成员varImpl_中存储了一个通过移动构造得到的callable实例(std::forward<T>(t)将临时对象作为右值转发,触发callable的默认移动构造函数),当w的生命周期结束时,这个实例会被析构。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 11:01:02