c++中传递回调函数参数的最佳方式

Best way of passing callback function parameters in C++

本文关键字:参数 最佳 方式 函数 回调 c++      更新时间:2023-10-16

在c++中传递回调函数参数的最佳方法是什么?

我想简单地使用模板,像这样:

template <typename Function>
void DoSomething(Function callback)

这是在std::sort中用于比较函数对象的方法。

使用 && 传递呢?例如:

template <typename Function>
void DoSomething(Function&& callback)

这两种方法的优缺点是什么?为什么STL使用前者,例如在std::sort中?

template <typename Function>
void DoSomething(Function&& callback)

…使用转发引用通过引用传递,对于函数只使用回调的情况来说是IMHO优越的。因为函子对象虽然通常很小,但可以任意大。然后按值传递它会产生一些不必要的开销。

形式参数可以以T&T const&T&&结尾,这取决于实际的参数。


另一方面,通过引用传递简单的函数指针涉及到额外的不必要的间接,这在原则上可能意味着一些轻微的开销。

如果对给定的编译器、系统和应用程序是否重要存在疑问,则度量


关于

" 为什么STL使用前者[按值传递]例如在std::sort中?

值得注意的是,std::sort自c++ 98以来就存在了,远远早于c++ 11中引入的转发引用,因此它最初不可能具有该签名。

可能只是没有足够的动机来改善这一点。毕竟,一个人通常不应该修复那些有效的东西。不过,c++ 17中引入了带有" execution policy "参数的额外重载。

由于这涉及到c++ 11可能发生的变化,因此通常的基本原理来源(即Bjarne Stroustrup的"c++的设计和演变")没有涵盖它。,我不知道有什么确凿的答案。


使用template<class F> void call( F )样式,您仍然可以返回"按引用传递"。只需执行call( std::ref( your callback ) )std::ref覆盖operator()并将其转发给所包含的对象。

同样,对于template<class F> void call( F&& )样式,您可以这样写:
template<class T>
std::decay_t<T> copy( T&& t ) { return std::forward<T>(t); }

显式复制某些内容,并强制call使用f的本地副本:

call( copy(f) );

所以这两种样式在默认情况下的行为主要不同。


因为这是c++ 11(或更高版本):<functional>std::function在这里是你最好的朋友。

#include <iostream>
#include <functional>
#include <string>

void DoSomething(std::function<void()> callback) {
    callback();
}
void PrintSomething() {
   std::cout << "Hello!" << std::endl;
}
int main()
{
    DoSomething(PrintSomething);
    DoSomething([]() { std::cout << "Hello again!" << std::endl; });
}

在我看来,使用&&将是更好的方法,主要是因为它避免了在参数为左值时复制构造函子。对于lambda,这意味着避免复制构造所有捕获的值。在std::function的情况下,您还需要额外的堆分配,除非使用了小对象优化。

但是在某些情况下,拥有函子的副本可能是一件好事。一个例子是使用如下的生成器函数对象:

#include <functional>
#include <cstdio>
template <typename F>
void func(F f) {
        for (int i = 0; i < 5; i++) {
                printf("Got %dn", (int)f());
        }
}
int main() {
        auto generator0 = [v = 0] () mutable { return v++; };
        auto generator1 = generator0; generator1();
        printf("Printing 0 to 4:n");
        func(generator0);
        printf("Printing 0 to 4 again:n");
        func(generator0);
        printf("Printing 1 to 5:n");
        func(generator1);
}

如果我们使用F&&代替,第二组打印将打印值5到9而不是0到4。我想这取决于应用程序/库的首选行为。

当使用函数指针时,我们也可能会遇到必须在invoke2函数中使用额外的间接层的情况:

template <typename F>
void invoke1(F f) {
        f();
}
template <typename F>
void invoke2(F&& f) {
        f();
}
void fun();
void run() {
        void (*ptr)() = fun;
        void (&ref)() = fun;
        invoke1(fun); // calls invoke1<void (*)()>(void (*)())
        invoke1(&fun);// calls invoke1<void (*)()>(void (*)())
        invoke1(ptr); // calls invoke1<void (*)()>(void (*)())
        invoke1(ref); // calls invoke1<void (*)()>(void (*)())
        invoke2(fun); // calls invoke2<void (&)()>(void (&)())
        invoke2(&fun);// calls invoke2<void (*)()>(void (*&&)())
        invoke2(ptr); // calls invoke2<void (*&)()>(void (*&)())
        invoke2(ref); // calls invoke2<void (&)()>(void (&)())
}

语句invoke2(&fun);将函数的地址放在堆栈上,然后将这个临时堆栈槽的引用(即地址)发送给invoke2函数。然后,invoke2函数必须使用额外的内存查找来读取堆栈上的引用,然后才能使用它。额外的间接也适用于invoke2(ptr)。在所有其他情况下,函数的地址直接发送给invoke1/invoke2函数,不需要任何额外的间接。

这个例子展示了函数引用和函数指针之间一个有趣的区别。

当然,像内联这样的编译器优化可以很容易地摆脱这种额外的间接。