使用 std::function 或转发引用作为高阶函数的通用可调用对象输入参数?

using a std::function or a forwarding reference as general-purpose callable object input parameter of a higher-order function?

我想知道编写高阶函数的主要区别、优点和缺点,将 std::function 转发引用 作为输入参数,例如template<typename F> void hof(F&& fun);。 显然,前者比后者更严格,因为它指定了输入可调用对象必须符合的函数类型

std::function 通常会有显着的 运行 时间开销。通过 template 参数传递通用可调用对象避免了 std::function 的间接成本,并且 允许编译器积极优化 .

我为 lambda 递归写了一些简单的基准测试 (Y-combinator vs std::functionthe end of this articlestd::function 生成的程序集总是比非多态 Y 组合器实现多至少 3.5 倍。这是一个不错的 示例,它显示了 std::function 如何比 template 参数更昂贵。

我建议尝试 gcc.godbolt.org 以了解两种技术之间的组装差异。


这是一个例子I just came up with:

#if defined(STDFN)
void pass_by_stdfn(std::function<void()> f)
{
    f();
}
#else
template <typename TF>
void pass_by_template(TF&& f)
{
    f();
}
#endif

volatile int state = 0;

int main()
{
#if defined(STDFN)
   pass_by_stdfn([i = 10]{ state = i; });
#else
   pass_by_template([i = 10]{ state = i; });  
#endif
}

With STDFN 未定义,生成的程序集为:

main:
        mov     DWORD PTR state[rip], 10
        xor     eax, eax
        ret
state:
        .zero   4

使用STDFN定义,生成的程序集长48行。

std::function有很多优点,但也有一系列必须考虑的缺点。
例如:

  • 可调用对象应该是可复制构造的。
    换句话说,这编译:

    #include <functional>
    #include <utility>
    #include <memory>
    
    template<typename F>
    void func(F &&f) { std::forward<F>(f)(); }
    
    int main() {
        func([ptr = std::make_unique<int>()](){});
    }
    

    但这不是:

    #include <functional>
    #include <utility>
    #include <memory>
    
    void func(std::function<void(void)> f) { f(); }
    
    int main() {
        func([ptr = std::make_unique<int>()](){});
    }
    
  • 尽管(强调我的):

    Implementations are encouraged to avoid the use of dynamically allocated memory for small callable objects, for example, where f's target is an object holding only a pointer or reference to an object and a member function pointer.

    您不能保证在可以避免的情况下不会在动态存储上进行分配,并且在所有其他情况下您肯定会进行分配。

  • 当你构造一个std::function时,构造函数可能会抛出一个bad_alloc.

  • ...也许我们可以继续,但这不值得,你明白了。

std::function 是非常有用的工具,但是当您需要 时,您应该使用它们。
例如,如果您计划将 函数 存储在某处,您最终可能会使用 std::function。另一方面,如果您打算接受一个可调用对象并即时调用它,您可能不会使用 std::function.
这些只是几个例子,不幸的是,黄金法则并不存在。