记一次使用C++ std::function制作包装器模式遇到的问题

24 阅读4分钟

记一次使用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":

当定义了两个重载:

  1. Wrapper(..., std::function<void(...)>)
  2. 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++的知识,还是非常令人开心的!

以上故事纯属胡诌,如有雷同,纯属巧合!