在C ++中传递回调函数参数的最佳方法

时间:2016-11-14 12:25:21

标签: c++ templates callback parameter-passing

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

我想过简单地使用模板,如下:

template <typename Function>
void DoSomething(Function callback)

这是使用的方式,例如在std::sort中用于比较函数对象。

使用 && 传递怎么样? E.g:

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

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

3 个答案:

答案 0 :(得分:15)

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

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

正式参数可以最终为T&T const&T&&,具体取决于实际参数。

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

如果怀疑这对于给定的编译器,系统和应用程序是否重要,那么测量

关于

  

为什么STL使用前者[通过值],例如在std::sort

...值得注意的是,自C ++ 98以来std::sort一直存在,早在C ++ 11中引入转发引用之前,所以它最初不能拥有该签名。

可能没有足够的动力来改善这一点。毕竟,人们通常不应该解决哪个有效。尽管如此,C ++ 17中还是引入了“execution policy” argument的额外过载。

由于这涉及到可能的C ++ 11更改,因此通常的理由来源(即Bjarne Stroustrup的“The design and evolution of C++”.未涵盖,并且我不知道任何结论性答案

使用template<class F> void call( F )样式,您仍然可以通过引用&#34;传递#34;。只需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) );

所以这两种风格在默认情况下的表现主要不同。

答案 1 :(得分:1)

由于这是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; });
}

答案 2 :(得分:0)

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

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

#include <functional>
#include <cstdio>

template <typename F>
void func(F f) {
        for (int i = 0; i < 5; i++) {
                printf("Got %d\n", (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 函数,不需要任何额外的间接寻址。

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

当然,内联等编译器优化可以轻松摆脱这种额外的间接性。