【问题标题】:Best way of passing callback function parameters in C++在 C++ 中传递回调函数参数的最佳方法
【发布时间】:2017-03-28 02:09:11
【问题描述】:

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

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

template <typename Function>
void DoSomething(Function callback)

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

使用 &amp;&amp; 传递呢?例如:

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

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

【问题讨论】:

  • @VittorioRomeo:我相信这个问题根本不是重复的。
  • 这在in one wayanother 之前已经被问过......实际上并没有“绝对优越”的方式来处理这个问题,这取决于可调用对象的性质。另请参阅具有类似意图的this question
  • 我的问题与in the other thread 提出的问题非常不同。当然,我不需要编辑任何东西,只是不同而已。例如另一个线程甚至没有提到通过&amp;&amp;,我也很好奇STL的std::sort使用的模式(为什么STL使用&amp;&amp;?) .
  • 好吧,STL 按值传递回调的一个可能原因是在 C++11 之前不存在转发引用。

标签: c++ templates callback parameter-passing


【解决方案1】:

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

... 它使用forwarding reference 通过引用传递,恕我直言,在函数仅使用回调的情况下更胜一筹。因为函子对象虽然通常很小,但可以任意大。按值传递它会产生一些不必要的开销。

形式参数可以以T&amp;T const&amp;T&amp;&amp; 结尾,具体取决于实际参数。


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

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


关于

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

…值得注意的是,std::sort 自 C++98 以来就已经存在,远在 C++11 中引入转发引用之前,因此它最初不能具有该签名。

可能只是没有足够的动力来改进这一点。毕竟,一个人通常不应该修复有效的东西。尽管如此,在 C++17 中还是引入了带有 “execution policy” argument 的额外重载。

由于这涉及到可能的 C++11 更改,因此通常的基本原理来源(即 Bjarne Stroustrup 的 “The design and evolution of C++”.)并未涵盖它,而且我不知道任何确凿的答案。


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

同样,template&lt;class F&gt; void call( F&amp;&amp; ) 风格,你可以这样写:

template<class T>
std::decay_t<T> copy( T&& t ) { return std::forward<T>(t); }

明确复制某些内容,并通过以下方式强制call 使用f 的本地副本:

call( copy(f) );

所以这两种样式的主要区别在于默认情况下的行为方式。


【讨论】:

  • 我很好奇为什么 STL(应该准备好正确处理最通用的情况)使用与转发引用不同的方法。
  • @Mr.C64:我在答案中添加了一些我可以贡献的东西。
  • 我认为值得注意的是,如果您有一个有状态的 lambda,您可能会故意将副本传递给算法。让我想知道是否应该为未知类型提供类似 std::copy 的函数:godbolt.org/g/MIU1ED
  • 提及std::ref 基本上给你&amp;&amp; 是值得的。
  • @Yakk:随意修复/添加/修改。感谢您提及。
【解决方案2】:

因为这是 C++11(或更高版本):&lt;functional&gt;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; });
}

【讨论】:

  • 使用std::function 会引入可能不必要的运行时开销。如果您需要其运行时灵活性(与模板参数相比) 和/或不想要更大的二进制大小,请仅使用 std::function
  • @VittorioRomeo:我认为简单也是使用std::function 的一个很好的理由,即模板化通常可能是过早的优化。例如,范围保护类模板用std::function 表示很简单。为了用泛型函数类型来表达它,需要引入一个工厂函数。
  • 正如 Vittorio 所说,std::function 会导致潜在的运行时开销。这就是为什么我不赞成您认为这应该是您的默认选择的建议。
  • 运行时开销对于非约束环境是可以接受的。并且越来越小。
【解决方案3】:

在我看来,使用&amp;&amp; 会是更好的方法,主要是因为它避免了当参数是左值时复制构造函子。在 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&amp;&amp;,第二组打印将打印值 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(&amp;fun); 将函数的地址放在堆栈上,然后将这个临时堆栈槽的引用(即地址)发送到invoke2 函数。 invoke2 函数必须使用额外的内存查找来读取堆栈上的引用,然后才能使用它。额外的间接寻址也适用于invoke2(ptr)。在所有其他情况下,函数的地址直接发送到 invoke1/invoke2 函数,不需要任何额外的间接寻址。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-13
    相关资源
    最近更新 更多