记一次使用C++ std::function制作包装器模式遇到的问题
起因
我是一名Java后端开发工程师,对于C++并不是很熟悉。故事是某天,我在公司为老板增强C++调用接口的安全性,要将所有的接口都使用某个包装器封装起来,在包装器中使用try catch拦截被包装函数可能抛出的所有异常,避免异常无人处理,最终导致进程终止。 包装器代码起初设计为:
template<typename InputProtoT, typename OutputProtoT>
void Wrapper(InputProtoT& input, OutputProtoT& output, std::function<void(const InputProtoT&, OutputProtoT&)> func) {
try {
func(input, output);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
刚开始时,这段代码没有任何问题,直到我适配到某个有返回值的函数时,故事发生了转折。
折磨
我遇到了这样一类函数:
ErrorCode SomeFuncReturnEC(const InputProto& input, OutputProto& output) {
if (input.value < 0) {
return ErrorCode::INVALID_INPUT;
}
output.result = input.value * 2;
return ErrorCode::SUCCESS;
}
我需要Wrapper可以将返回值返回给调用方,而不是直接忽略,令人意外的是原有实现居然依然可用:
int main() {
InputProto input{5};
OutputProto output;
Wrapper<InputProto, OutputProto>(input, output, SomeFuncReturnEC);
std::cout << "Output result: " << output.result << std::endl;
return 0;
}
输出:
Output result: 10
但是我并不能获取到返回值:
a value of type "void" cannot be used to initialize an entity of type "ErrorCode"
怎么做到从Wrapper返回其内层函数的返回值呢?如果我添加一个模板方法,使用std::function<ErrorCode(const InputProtoT&, OutputProtoT&)>会发生什么?
实验
template<typename InputProtoT, typename OutputProtoT>
ErrorCode Wrapper(InputProtoT& input, OutputProtoT& output, std::function<ErrorCode(const InputProtoT&, OutputProtoT&)> func) {
try {
return func(input, output);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return ErrorCode::PROCESSING_ERROR;
}
}
但是报错了:
more than one instance of overloaded function "Wrapper" matches the argument list
提示我有多个重载的方法符合参数列表。
为什么?我问自己,在查阅各种资料无果后,我决定休息一下,在上厕所的路上我还在思考:记得大学时候学过,C++的函数在重载的时候并不会考虑方法的返回值,也就是说如果函数仅仅是返回值不同,那么这两个函数依然是同一个函数,那么有没有可能返回ErrorCode也可以被识别为是返回void的函数呢?我觉得非常有可能是这个原因。但是我该怎么办?我怎么才能在今天下班前解决这个工作啊,马上就要发版了,如果没有保护,出了异常直接终止进程我就要去投简历了。
我继续思考:那么为什么编译器提示我是有多个满足条件的函数,也就是说它识别出了std::function<ErrorCode(const InputProtoT&, OutputProtoT&)>类型,那么我如何指示编译器选择ErrorCode返回值的这个function呢?如果我用一个确切是std::function<ErrorCode(const InputProtoT&, OutputProtoT&)>类型的变量接我的被包装方法会发生什么,我回到座位继续写代码:
int main() {
InputProto input{5};
OutputProto output;
std::function<ErrorCode(const InputProto&, OutputProto&)> func = SomeFuncReturnEC;
ErrorCode ec = Wrapper<InputProto, OutputProto>(input, output, func);
std::cout << "Output result: " << output.result << std::endl;
return 0;
}
奇迹发生了,编译顺利通过!运行输出:
Output result: 10
恍然大悟
但我上面所说有误,实际上并不是由于C++的重载不在乎返回值,而是std::function过于大方的帮我们做了隐式转换:
在 C++ 标准中,std::function的设计遵循一个原则:只要目标函数可以被调用,并且其返回值可以被丢弃(discarded),它就可以被封装进 std::function<void(...)>。
为什么重载会报错more than one instance of overloaded function "Wrapper":
当定义了两个重载:
- Wrapper(..., std::function<void(...)>)
- Wrapper(..., std::function<ErrorCode(...)>)
并尝试调用 Wrapper(input, output, SomeFuncReturnEC) 时,编译器陷入了沉思。
对于编译器来说,它需要把 SomeFuncReturnEC 转换成一个 std::function对象。
- 选项 A:转换成 std::function<void(...)>。根据标准,这是合法的隐式转换。
- 选项 B:转换成 std::function<ErrorCode(...)>。这显然也是完全匹配的隐式转换。
在 C++ 的重载解析(Overload Resolution)规则中,这两种通过构造函数生成的隐式转换具有相同的优先级。编译器没有理由认为其中一个比另一个“更优”,于是它只能两手一摊:more than one instance of overloaded function "Wrapper" matches the argument
为什么手动声明变量就通过了?
此时:
-
func 的类型已经是确定的 std::function<ErrorCode(...)>。
-
当调用 Wrapper时,编译器发现:
- 重载 2 是精确匹配(类型完全一致)。
- 重载 1 需要将 std::function 转换为 std::function。
所以,二义性消失了,编译器开心地选择了重载 2。
更好的方式
template<typename InputProtoT, typename OutputProtoT, typename F>
auto Wrapper(InputProtoT& input, OutputProtoT& output, F func) -> decltype(func(input, output)) {
try {
// 使用 auto 自动接住返回值,不管它是 void 还是 ErrorCode
return func(input, output);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
// 注意:这里如果返回 ErrorCode,需要考虑异常时的默认返回值
// 在实际生产中,通常会结合 if constexpr 来处理 void 和非 void
if constexpr (std::is_same_v<decltype(func(input, output)), void>) {
return;
} else {
return ErrorCode::PROCESSING_ERROR;
}
}
}
后记
真是令人难崩的一天,但是学到了这么多关于C++的知识,还是非常令人开心的!
以上故事纯属胡诌,如有雷同,纯属巧合!