发布于 

如何向 STL 算法中传入重载函数

本文分享如何向 STL 算法中传入重载函数,来自 Jonathan 2017 年在 Fluent C++ 上发起的一个挑战

1
2
3
The STL is a fantastic tool to make your code more expressive and more robust. If you’re a C++ developer and want to become proficient, it is essential that you learn the STL.

But there is one case where we can’t apply STL algorithms right out of the box: when the function passed has overloads.

大概意思是说 STL 本身非常好用,但是如果传递一个具有重载的函数就会使得 STL 失效。

举个例子,如果我们定义了一个函数 func 以及一个 vector 数组如下:

1
2
3
4
5
void func (int &i) {
++i;
}

std::vector<int> numbers = {1, 2, 3, 4, 5};

那么我们可以通过 for_each 使 numbers 中的每个数字加一:

1
std::for_each(begin(numbers), end(numbers), func);

但是这时如果我们还有一个同名的重载函数:

1
void func (std::string &s);

就会导致刚刚的代码编译失败, 因为编译器无法推断我们要使用哪一个版本:

1
2
3
4
5
6
7
main.cpp: In function 'int main()':
main.cpp:20:50: error: no matching function for call to 'for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>)'
std::for_each(begin(numbers), end(numbers), func);
^
/usr/local/include/c++/7.1.0/bits/stl_algo.h:3878:5: note: template argument deduction/substitution failed:
main.cpp:20:50: note: couldn't deduce template parameter '_Funct'
std::for_each(begin(numbers), end(numbers), func);

这时我们只能通过指定 func 的类型来避免这个错误:

1
std::for_each(begin(numbers), end(numbers), static_cast<void(*)(int&)>(func));

这显然并不美观,也会使得代码的可读性下降,于是 Jonathan 希望大家能够用更好的方式来解决这个问题
不知道大家现在是否有自己的想法呢,如果你也想挑战一下这个问题,不妨暂停一下,来一个简单的头脑风暴,说不定你的方案更加优秀。

倒计时

3

2

1

答案揭晓!

首先说一下我个人的想法,我的想法非常简单,首先这里编译报错是因为 for_each 函数无法判断这里所用到的 func 应该使用哪一个版本,即使这对我们来说非常直观 (容器内是 int 那么就应该调用 int 版本的重载,string 亦然)。

那么为了帮助确定重载版本,我们可以使用一个 lambda 函数代替 func 作为 for_each 的处理函数,而 lambda 的实现方式允许我们拿到 处理的参数类型并进一步确定所调用的 func 版本

1
std::for_each(begin(numbers), end(numbers), [](auto i){ return func(i); });

如果你的想法也和我一样,恭喜你也同时了 Jonathan 的称赞,他评价这种解决方案

It’s not as generic as the the previous solution, but it does all that is necessary for simple cases
这并非通用的解决方案,但它能很好的胜任大多数不复杂的场景

接下来让我们看一下本次挑战的最终获胜答案,来自 Vittorio Romeo:

1
2
3
4
5
6
7
// C++ requires you to type out the same function body three times to obtain SFINAE-friendliness and
// noexcept-correctness. That's unacceptable.
#define RETURNS(...) noexcept(noexcept(__VA_ARGS__)) -> decltype(__VA_ARGS__){ return __VA_ARGS__; }

// The name of overload sets can be legally used as part of a function call - we can use a macro to
// create a lambda for us that "lifts" the overload set into a function object.
#define LIFT(f) [](auto&&... xs) RETURNS(f(::std::forward<decltype(xs)>(xs)...))

怎么样,你的脑袋有没有被‘轻轻敲醒’的感觉,反正与我而言乍一看这段代码直接就是一个当头棒喝。不过当你沉下心一点点分析这段代码,你就会越来越被这代码里精妙的设计吸引。

为了理解这部分代码,我们可以先尝试对 LIFT(func) 进行展开,结果如下:

1
2
3
4
[](auto&&... xs) noexcept(noexcept(func(::std::forward<decltype(xs)>(xs)...))) -> decltype(func(::std::forward<decltype(xs)>(xs)...))
{
return func(::std::forward<decltype(xs)>(xs)...);
}

怎么样,是不是突然就有了一种熟悉的感觉。没错,其实这也是一个 lambda 函数,只是相比于我们的简略版本考虑的更多的情况,接下来让我们一点一点分析。

1
2
3
4
[] (auto&&... xs) /*省略*/
{
return func(::std::forward<decltype(xs)>(xs)...);
}

除开省略部分我们可以看到就是一个完美转发并支持变长参数,这使得 LIFT 可以适用于多个参数的函数并且不改变其传入参数的类型。

接着看省略部分,也就是 RETURNS 部分的内容:

1
2
/*省略*/ noexcept(noexcept(func(::std::forward<decltype(xs)>(xs)...))) -> decltype(func(::std::forward<decltype(xs)>(xs)...))
/*省略*/

包含两部分内容 noexcept(...)-> decltype(...):

后者指定了 LIFT 的返回类型,在我们的简单方法中没有指定,因此默认会使用 -> auto,这可能会使得由 func 返回的值类型被改变,考虑如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int& func_return_value (int& i) {
return i;
}

void use_value(int& i) {
std::cout << "I got a l value i:" << i << &i << std::endl;
}

void use_value(int&& i) {
std::cout << "I got a r value i:" << i << &i << std::endl;
}

auto x = [](auto&&... xs) {
return func_return_value(::std::forward<decltype(xs)>(xs)...);
};

auto x1 = [](auto&&... xs) -> decltype(func_return_value(::std::forward<decltype(xs)>(xs)...)){
return func_return_value(::std::forward<decltype(xs)>(xs)...);
};

auto main(void) -> int {

int a = 3;
// I have a value a :30x7ffc9ecd766c
std::cout << "I have a value a :" << a << &a << std::endl;
// I got a r value i:30x7ffc9ecd76ac
use_value(x(a));
// I got a l value i:30x7ffc9ecd766c
use_value(x1(a));
}

对示例来说我们希望拿到并传入由 func_return_value 返回的 int& 类型变量,但是由于 x 中的默认返回类型为 auto,因此导致其示例化了一个新的临时变量并作为右值返回。

接着思考另外一个问题,如果我们使用 ->decltype(auto) 代替 ->auto 呢,是否可以达到我们想要的目标, 又会有什么问题呢。

Jonathan 也帮助我们分析了这一问题,并指出 decltype(expr) 的版本具有更好的 SFINAE 亲和性, Guillaume Racicot 在 stackoverflow 上也讨论了这个问题。

实际上结合 Vittorio 在 C++Now 2017 的演讲,我更偏向于这里使用 ->decltype(auto) 也能同样出色的完成任务,但是 RETURNS 中的用法可以带给我们再其他更多场景下使用的启发。

接着 LIFT 的最后一部分 noexcept 则是根据传入的 func 设置是否抛出异常

综上,希望大家能够收获一点有用的东西!


References: