在C ++中传递回调函数参数的最佳方法是什么?
我想过简单地使用模板,如下:
template <typename Function>
void DoSomething(Function callback)
这是使用的方式,例如在std::sort
中用于比较函数对象。
使用 &&
传递怎么样? E.g:
template <typename Function>
void DoSomething(Function&& callback)
这两种方法的优点和缺点是什么,为什么STL使用前者,例如在std::sort
?
答案 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
函数,不需要任何额外的间接寻址。
这个例子展示了函数引用和函数指针之间一个有趣的区别。
当然,内联等编译器优化可以轻松摆脱这种额外的间接性。