现代C++中替代函数指针的方法

15

到目前为止,我一直在使用函数指针,就像在C++中这样的格式。现在有时候我确实需要它们,想知道C++11/14中是否引入了其他替代方案。

#include <iostream>
using namespace std;

void sayHello();
void someFunction(void f());

int main() {

    someFunction(sayHello);
    return 0;
}
void sayHello(){
    std::cout<<"\n Hello World";
}
void someFunction(void f()){
    f();
} 

我确实看过这个问题,但是无法理解使用函数指针的任何优点,而且我想问一下,使用函数指针是否存在任何错误(不推荐)的事情,因为我从来没有见过有人使用它们。或者是否有其他替代方案。


尝试使用lambda表达式 :) - Andrey Lyubimov
@AndrewLavq,这里有一些重型打印函数(虽然不是一个应用程序,只是为了演示目的),如果我使用lambda表达式,将会影响可读性。 - hg_git
4个回答

12

你提到的问题建议使用std::function,但没有强调(或根本没有提到)与std::bind组合时的价值。

你的例子是最简单的,但假设你有一个

std::function<void (int, int)> f ;

函数指针可以做类似的事情。但是假设你需要一个带有参数int的函数g(int),其第二个参数绑定为0,而该函数是函数f。使用函数指针您无法做太多事情,但使用std::function,您可以执行以下操作:

std::function<void(int)> g = std::bind(f, _1, 0) ;

这看起来很有前途,但我困惑的是为什么我要使用一个比 f 少一个参数的另一个函数 g?换句话说,std::bind 与 std::function 结合使用的可能实际用例是什么? - hg_git
3
假设你有一个函数 compare(string, string, bool caseSensitive)。当你需要进行不区分大小写的比较时,可以传递 std::bind(compare, _1, _2, false) - MSalters
1
@hg_git 如果你想要带有延迟参数的函数,可以简单地想象一个持有“待办事项”列表的工作线程。这是非常常见的:一个带有“撤销列表”的应用程序。每个用户步骤都将被记录下来,并且函数调用将包含在一个带有参数的列表中(绑定)。如果你想要撤销,只需反向调用列表并执行函数的相反操作即可。 - Klaus
1
@Klaus:与快照状态相比,那种撤销列表作为一种策略已经过时了。为每个可能的用户操作编写“撤销”是很糟糕的,而采用COW状态,您可以轻松地将状态还原回任何操作之前的状态。 - Puppy
2
@Puppy 这只是一个例子!如果数据非常复杂且庞大,那么存储长时间撤消列表的每个状态几乎是不可能的。因此,我无法相信“撤消函数”已经死亡。但总的来说:这只是一个例子 :-) - Klaus

5
作为传统函数指针的替代,C++11引入了模板别名,与可变参数模板结合使用可以简化函数指针语法。以下是创建“模板”函数指针的示例:
template <typename R, typename ...ARGS> using function = R(*)(ARGS...);

它可以这样使用:

void foo()                { ... }
int bar(int)              { ... }
double baz(double, float) { ... }

int main()
{
    function<void>                  f1 = foo;
    function<int, int>              f2 = bar;
    function<double, double, float> f3 = baz;

    f1(); f2({}); f3({}, {});
    return 0;
}

此外,它可以很好地处理函数重载:
void overloaded(int)      { std::cout << "int version\n"; }
void overloaded(double)   { std::cout << "double version\n"; }

int main()
{
    function<void, int>    f4 = overloaded;
    function<void, double> f5 = overloaded;

    f4({}); // int version
    f5({}); // double version
    return 0;
}

它可以作为声明函数指针参数的一种非常棒的方式:

void callCallback(function<int, int> callback, int value)
{
    std::cout << "Calling\n";
    std::cout << "v: " << callback(value) << '\n';
    std::cout << "Called\n";
}

int main()
{
    function<int, int> f2 = bar;
    callCallback(f2, {});
    return 0;
}

这个模板别名可以作为std::function的替代,它既没有缺点也没有优点(这里有一个很好的解释)。

演示实例

简而言之,我认为结合可变参数模板的模板别名是C++中一个不错、漂亮、整洁和现代的选择,可以替代"裸"函数指针 (毕竟,这个别名本质上仍然是函数指针)。但是,std::function同样是一个好、漂亮、整洁和现代的C++组件,具有优秀的特性值得考虑。选择使用函数指针(或别名)还是选择std::function取决于您的实现需求。


2
不,绝对不是这样的。如果你不想使用std::function的优势,那就使用一个真正的模板,而不仅仅是一个语法简化的别名。 - Puppy

4
我想说,使用函数指针是有问题的(不推荐)。首先,它们不支持泛型 - 因此您无法使用接受任何T类型的std :: vector 的函数指针。其次,它们不支持绑定状态,因此如果将来有任何人希望引用其他状态,他们将完全失败。这特别糟糕,因为这包括成员函数的this。
在C++11中,有两种方法可以使用函数。第一种是使用模板。第二种是使用std :: function。
模板大致如下:
template<typename T> void func(F f) {
    f();
}

这里的主要优点是它接受任何类型的函数对象,包括函数指针、lambda表达式、functor、bind-result等,而且F可以具有任何数量的函数调用重载和任何签名,包括模板,同时它可以具有任何绑定状态和大小。因此,它非常灵活。由于编译器可以内联操作符并直接将状态传递到对象中,所以它也是最大效率的。

int main() {
    int x = 5;
    func([=] { std::cout << x; });
}

这里的主要缺点是模板的通常缺点 - 它不适用于虚函数,并且必须在头文件中定义。
另一种方法是使用std :: function。 std :: function具有许多相同的优点 - 它可以是任何大小,绑定到任何状态,并且可以是任何可调用的内容,但会有一些折衷。 主要是,签名在类型定义时被固定,因此您不能为一些尚未知的T拥有std :: function<void(std :: vector<T>)>,并且可能还涉及一些动态间接/分配(如果您无法SBO)。 其优点在于,由于std :: function是一个真正的具体类型,因此您可以像使用任何其他对象一样传递它,因此它可以用作虚函数参数等。
基本上,函数指针非常有限,不能真正做任何有趣的事情,并使API非常不灵活。它们令人发指的语法只是小菜一碟,使用模板别名来减少它的可读性是可笑而无意义的。

3
“template<typename T> void func(F f)” - 这个代码中哪一个是错别字 - 是 T 类型还是 F 类型? - bloody

3
我看了一下这个问题,但是没看出来与传统使用函数指针相比有任何优势。另外我想问一下,使用函数指针有什么不好的地方(不推荐),因为我从来没有见过有人使用它们。
常规“全局”函数通常不能有状态。虽然在函数式编程范式中遍历时不一定要有状态,但有时状态可能会有用,特别是当它与所更改的内容正交相关时(例如启发式)。这时候,函数对象有优势。
常规函数组合起来不太容易(创建较低级别函数的高级函数)。
普通函数不允许在运行时绑定额外的参数。
有时普通函数可以替代lambda,反之亦然,具体取决于上下文。通常不希望仅因为在“容器遍历”期间有某些非常本地/特定的需求而编写特殊函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接