如何向 STL 算法中传入重载函数
本文分享如何向 STL 算法中传入重载函数,来自 Jonathan 2017 年在 Fluent C++ 上发起的一个挑战
1 | 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. |
大概意思是说 STL 本身非常好用,但是如果传递一个具有重载的函数就会使得 STL 失效。
举个例子,如果我们定义了一个函数 func
以及一个 vector
数组如下:
1 | void func (int &i) { |
那么我们可以通过 for_each
使 numbers
中的每个数字加一:
1 | std::for_each(begin(numbers), end(numbers), func); |
但是这时如果我们还有一个同名的重载函数:
1 | void func (std::string &s); |
就会导致刚刚的代码编译失败, 因为编译器无法推断我们要使用哪一个版本:
1 | main.cpp: In function 'int main()': |
这时我们只能通过指定 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 | // C++ requires you to type out the same function body three times to obtain SFINAE-friendliness and |
怎么样,你的脑袋有没有被‘轻轻敲醒’的感觉,反正与我而言乍一看这段代码直接就是一个当头棒喝。不过当你沉下心一点点分析这段代码,你就会越来越被这代码里精妙的设计吸引。
为了理解这部分代码,我们可以先尝试对 LIFT(func)
进行展开,结果如下:
1 | [](auto&&... xs) noexcept(noexcept(func(::std::forward<decltype(xs)>(xs)...))) -> decltype(func(::std::forward<decltype(xs)>(xs)...)) |
怎么样,是不是突然就有了一种熟悉的感觉。没错,其实这也是一个 lambda 函数,只是相比于我们的简略版本考虑的更多的情况,接下来让我们一点一点分析。
1 | [] (auto&&... xs) /*省略*/ |
除开省略部分我们可以看到就是一个完美转发并支持变长参数,这使得 LIFT
可以适用于多个参数的函数并且不改变其传入参数的类型。
接着看省略部分,也就是 RETURNS
部分的内容:
1 | /*省略*/ noexcept(noexcept(func(::std::forward<decltype(xs)>(xs)...))) -> decltype(func(::std::forward<decltype(xs)>(xs)...)) |
包含两部分内容 noexcept(...)
和 -> decltype(...)
:
后者指定了 LIFT
的返回类型,在我们的简单方法中没有指定,因此默认会使用 -> auto
,这可能会使得由 func
返回的值类型被改变,考虑如下示例:
1 | int& func_return_value (int& i) { |
对示例来说我们希望拿到并传入由 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: