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

static_function实现中复制消除失效的原因排查

静态函数对象static_function复制消除失效问题排查

我尝试实现一款采用预分配存储的「静态」尺寸函数static_function,区别于使用动态堆分配的std::function。但该实现未达预期,callable的复制消除并未像单独使用lambda时生效。

以下是-O3编译选项下的预期与实际行为对比代码及输出:

static_function实现代码

#include <utility>
#include <cstddef>

#include <type_traits>

template <typename T, size_t StackSize = 64>
class static_function;

// TODO: move and swap
//  - can move smaller instance to larger instance
//  - only instances of the same size are swappable
// TODO: condiotnal dynamic storage?
template <typename Ret, typename ... Args, size_t StackSize>
class static_function<Ret(Args...), StackSize>
{
public:
    constexpr static size_t static_size = StackSize;
    using return_type = Ret;

    template <typename Callable>
    constexpr explicit static_function(Callable &&callable)
        : pVTable_(std::addressof(v_table::template get<Callable>()))
    {
        static_assert(sizeof(std::decay_t<Callable>) <= static_size, "Callable type is too big!");

        new (&data_) std::decay_t<Callable>(std::forward<Callable>(callable));
    }

    constexpr return_type operator()(Args ... args) const
    {
        return (*pVTable_)(data_, std::move(args)...);
    }

    ~static_function() noexcept
    {
        pVTable_->destroy(data_);
    }

private:
    using stack_data = std::aligned_storage_t<static_size>;

    struct v_table
    {
        virtual return_type operator()(const stack_data&, Args &&...) const = 0;
        virtual void destroy(const stack_data&) const = 0;

        template <typename Callable>
        static const v_table& get()
        {
            struct : v_table {
                return_type operator()(const stack_data &data, Args &&... args) const override 
                {
                    return (*reinterpret_cast<const Callable*>(&data))(std::move(args)...);
                }

                void destroy(const stack_data &data) const override
                {
                    reinterpret_cast<const Callable*>(&data)->~Callable();
                }

            } constexpr static vTable_{};

            return vTable_;
        }  
    };    

private:
    stack_data data_;
    const v_table *pVTable_;
};

测试代码

#include <iostream>
#include <string>

int main()
{
    struct prisoner 
    {
        std::string name;

        ~prisoner()
        {
            if (!name.empty())
                std::cout << name << " has been executed\n";
        }
    };

    std::cout << "Expected:\n";
    {
        const auto &func = [captured = prisoner{"Pvt Ryan"}](int a, int b) -> std::string {
            std::cout << captured.name << " has been captured!\n";
            return std::string() + "oceanic " + std::to_string(a + b);
        };
        std::cout << func(4, 811) << '\n';
    }
    std::cout << "THE END\n\n";

    std::cout << "Actual:\n";
    {
        const auto &func = static_function<std::string(int, int)>([captured = prisoner{"Pvt Ryan"}](int a, int b) -> std::string {
            std::cout << captured.name << " has been captured!\n";
            return std::string() + "oceanic " + std::to_string(a + b);
        });

        std::cout << func(4, 811) << '\n';
    }
    std::cout << "THE END!\n";

    return 0;
}

输出结果

Expected:
Pvt Ryan has been captured!
oceanic 815
Pvt Ryan has been executed
THE END

Actual:
Pvt Ryan has been executed
Pvt Ryan has been captured!
oceanic 815
Pvt Ryan has been executed
THE END!

问题原因与修复方案

核心问题

你的实现存在两个关键问题:

  1. 临时lambda被复制而非移动:传入static_function构造函数的临时lambda被复制到预分配存储区,导致临时lambda先析构(输出第一次"执行"信息),static_function销毁时内部存储的lambda再析构(第二次输出),出现额外的析构调用。
  2. 析构调用存在未定义行为:v_table的destroy函数通过const指针调用析构函数,而存储区的对象本身是非const的,属于未定义行为。

修复步骤

1. 改用移动构造减少复制

修改static_function的构造函数,确保优先使用移动构造转移lambda资源:

template <typename Callable>
constexpr explicit static_function(Callable &&callable)
    : pVTable_(std::addressof(v_table::template get<std::decay_t<Callable>>()))
{
    static_assert(sizeof(std::decay_t<Callable>) <= static_size, "Callable type is too big!");

    // 使用std::move触发移动构造(针对右值参数)
    new (&data_) std::decay_t<Callable>(std::move(callable));
}

注:这里用std::move替代std::forward,因为我们明确要把传入的右值lambda移动到存储区,避免不必要的复制。

2. 修复析构函数的未定义行为

修改v_table的虚函数定义,移除stack_data的const限定,并调整指针类型:

struct v_table
{
    virtual return_type operator()(const stack_data&, Args &&...) const = 0;
    // 移除destroy参数的const限定
    virtual void destroy(stack_data&) const = 0;

    template <typename Callable>
    static const v_table& get()
    {
        struct : v_table {
            return_type operator()(const stack_data &data, Args &&... args) const override 
            {
                return (*reinterpret_cast<const Callable*>(&data))(std::move(args)...);
            }

            // 接收非const的stack_data,使用非const指针调用析构
            void destroy(stack_data &data) const override
            {
                reinterpret_cast<Callable*>(&data)->~Callable();
            }

        } constexpr static vTable_{};

        return vTable_;
    }  
};

同时更新static_function的析构函数,传入非const的data_:

~static_function() noexcept
{
    pVTable_->destroy(data_);
}

3. 额外优化:启用复制消除

修改后,编译器在-O3下可以优化掉移动操作(如果lambda的移动构造是 trivial 的),最终只会保留一次析构调用,和预期行为一致。

内容的提问来源于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.24 11:03:19