std::function 作为参数的函数重载问题
今天写 C++ 遇到一个关于 std::function
的有趣问题,经过一些研究之后搞清楚了原因,记录如下。
问题
为了避免其它代码的干扰,把问题简化描述如下:
using namespace std;
void foo(const function<bool()> &f) {
cout << "bool\n";
}
void foo(const function<void()> &f) {
cout << "void\n";
// 编译报错: 有多个重载函数 "foo" 实例与参数列表匹配
foo([&]() -> bool { return true; });
}
void foo_user() {
// 编译报错: 同上
foo([&]() -> bool { return true; });
// 不报错
foo([&]() {});
}
为什么?
在某个 C++ 群里询问之后,有一个群友提醒说可以把编译报错的那个 lambda 表达式手动构造成 std::function<bool()>
来解决,于是修改成下面这样:
using namespace std;
void foo(const function<bool()> &f) {
cout << "bool\n";
}
void foo(const function<void()> &f) {
cout << "void\n";
// 不报错
foo(std::function<bool()>([&]() -> bool { return true; }));
}
void foo_user() {
// 不报错
foo(std::function<bool()>([&]() -> bool { return true; }));
// 不报错
foo([&]() {});
}
确实解决了无法找到合适的重载函数的问题。进而意识到,用 lambda 的时候会报错是因为 lambda 到 std::function
有一次类型转换,而 [&]() -> bool { return true; }
可能既可以转换为 std::function<bool()>
也可以转换为 std::function<void()>
,从而产生了二义性。
群友后来又发现 std::function<bool()>
可以赋值给 std::function<void()>
,于是猜测 std::function
可能不关心它所表示的函数的返回类型,但这仍然无法解释为什么 foo([&]() {})
不报错。后来因为忙其它事情,群里也没有继续再讨论了。
空闲下来之后,继续研究了这个问题,把前面觉得可能的猜测都找到了定论。
为了方便解释,下面用 Cling 解释器来求值一些 type trait,以观察不同 std::function
实例之间的关系。
首先导入需要的头文件:
$ cling -std=c++17
[cling]$ #include <type_traits>
[cling]$ using namespace std;
然后用 std::is_convertible_v
来检查不同 std::function
之间是否能相互转换:
[cling]$ is_convertible_v<function<bool()>, function<void()>>
(const bool) true
[cling]$ is_convertible_v<function<void()>, function<bool()>>
(const bool) false
[cling]$ is_convertible_v<function<bool(int)>, function<bool()>>
(const bool) false
[cling]$ is_convertible_v<function<bool(int)>, function<void()>>
(const bool) false
[cling]$ is_convertible_v<function<bool(int)>, function<void(int)>>
(const bool) true
[cling]$ is_convertible_v<function<void(int)>, function<bool()>>
(const bool) false
[cling]$ is_convertible_v<function<void(int)>, function<void()>>
(const bool) false
[cling]$ is_convertible_v<function<void(int)>, function<bool(int)>>
(const bool) false
观察上面的结果可以发现,在参数类型相同的情况下,有返回类型的 std::function
可以转换为无返回类型的 std::function
,反过来则不可以。
从而一开始的问题便可以解释通了:
[&]() -> bool { return true; }
既可以转换为std::function<bool()>
也可以转换为std::function<void()>
,于是产生二义性;[&]() {}
只能转换为std::function<void()>
,于是没有二义性;- 手动构造出的
std::function<bool()>
虽然也可以转换为std::function<void()>
,但由于有一个不需要类型转换的重载,于是也没有二义性。
为什么 std::function<bool()>
可以转换为 std::function<void()>
?
虽然一开始问题解决了,但是还是不明白为什么 std::function<bool()>
可以转换为 std::function<void()>
,于是去找了 std::function
的实现(以 LLVM 的 libcxx 为例,代码相比 GNU libstdc++ 更清晰一些),节选如下:
template<class _Rp, class ..._ArgTypes>
class /* ... */ function<_Rp(_ArgTypes...)> /* : ... */ {
// ...
template <class _Fp, bool = _And<
_IsNotSame<__uncvref_t<_Fp>, function>,
__invokable<_Fp&, _ArgTypes...>
>::value>
struct __callable;
template <class _Fp>
struct __callable<_Fp, true> {
// MARK 1
static const bool value = is_same<void, _Rp>::value ||
is_convertible<typename __invoke_of<_Fp&, _ArgTypes...>::type, _Rp>::value;
};
template <class _Fp>
struct __callable<_Fp, false> {
static const bool value = false;
};
template <class _Fp>
using _EnableIfCallable = typename enable_if<__callable<_Fp>::value>::type;
// ...
// MARK 2
template<class _Fp, class = _EnableIfCallable<_Fp>>
function(_Fp);
// ...
};
可以看到 MARK 2
处为满足 _EnableIfCallable<_Fp>
的 _Fp
实现了一个构造函数,而满足 _EnableIfCallable<_Fp>
意味着 __callable<_Fp>::value
是 true
。根据 MARK 1
处的偏特化,发现当 _Rp
(也就是 std::function
的返回类型)为 void
、或调用 _Fp
的返回值类型可以转换为 _Rp
时,__callable<_Fp>::value
是 true
。
也就是说,除了前面观察发现的结论——有返回类型的 std::function
可以转换为无返回类型的 std::function
,标准库还允许有返回类型的 std::function
转换为返回类型可由前者的返回类型构造的 std::function
。用上一节的方式验证如下:
[cling]$ is_convertible_v<function<int(int)>, function<long(int)>>
(const bool) true
[cling]$ struct A {};
[cling]$ struct B : A {};
[cling]$ is_convertible_v<function<B(int)>, function<A(int)>>
(const bool) true
[cling]$ is_convertible_v<function<A(int)>, function<B(int)>>
(const bool) false
看到这其实已经豁然开朗了,从逻辑上来说,一个有返回值的函数确实可以当作没有返回值的函数来调用,返回 T
的函数也可以当作返回 T
能转换到的类型的函数来调用,只需进行一次类型转换,非常合理。
为了确定这是 C++ 标准定义的行为而不是标准库实现的私货,去翻了 C++17 标准(因为前面的讨论都是在 C++17 标准上进行的,虽然新版本并没有变化),在 23.14.3 和 23.14.13.2 确实有相关表述,这里就不贴出了。