关于使用std::is_nothrow_invocable变体的包装仿函数noexcept限定符的澄清
组件背景与实现
我正在开发一个小型个人库,其中实现了一个包装仿函数,可将静态可调用对象便捷转换为仿函数。例如在使用自定义哈希xxhash的std::unordered_map场景中,原本冗余的写法:
std::unordered_map<K, V, decltype(&xxhash)> map(&xxhash);
可以简化为:
std::unordered_map<K, V, func<xxhash>> map;
组件的实现代码如下:
template <auto Fn> struct func { template <typename... Ts> decltype(auto) operator()(Ts&&... p_ts) const noexcept(std::is_nothrow_invocable_r_v< std::invoke_result_t<decltype(Fn), Ts&&...>, decltype(Fn), Ts&&...>) { return std::invoke(Fn, std::forward<Ts>(p_ts)...); } };
核心疑问
按值返回是否需要在noexcept限定符中加以考量?根据调用上下文和定义,返回值存在多种处理可能(移动/复制、赋值/构造),这些操作抛出的异常会发生在函数外部。因此我猜测使用不带_r_的std::is_nothrow_invocable版本即可(当前写法的重复也让我存疑),但我无法严谨地理解return语句的机制,担心在C抽象模型的实际原理上出错。此外,我不确定C17中RVO的改动是否会对此产生影响,望得到相关澄清。
问题解答
1. 返回值处理的异常归属
返回值的复制/移动操作抛出的异常不属于当前函数的执行范围,而是发生在调用者的上下文里。operator()的职责只是完成std::invoke(Fn, ...)的调用并传递返回值,后续的返回值构造/移动是调用方的代码逻辑,所以这些异常不需要包含在当前函数的noexcept限定中。
2. is_nothrow_invocable vs is_nothrow_invocable_r
当前代码里用std::is_nothrow_invocable_r_v的写法确实冗余且没必要:
std::is_nothrow_invocable_r<R, F, Args...>用于判断std::invoke(F, Args...)能否以不抛异常的方式生成可转换为R类型的结果- 你的
operator()用decltype(auto)完全转发std::invoke的返回类型,此时std::is_nothrow_invocable_v<decltype(Fn), Ts&&...>就足够——它直接判断std::invoke(Fn, Ts&&...)本身是否不会抛出异常,完全匹配需求。
当前写法中,std::invoke_result_t<decltype(Fn), Ts&&...>作为std::is_nothrow_invocable_r的R类型,和std::is_nothrow_invocable的判断逻辑完全等价,属于重复冗余,换成不带_r_的版本更简洁准确。
3. C++17 RVO的影响
C++17强制了RVO(返回值优化):当返回局部对象的纯右值时,编译器必须省略复制/移动操作。但这对noexcept限定没有影响——无论是否触发RVO,返回值的复制/移动异常都不属于当前函数的责任范围。RVO只是消除了这些操作的发生,但即使没有RVO,异常也是在调用方抛出,和你的operator()无关。
最终简化后的noexcept部分应为:
noexcept(std::is_nothrow_invocable_v<decltype(Fn), Ts&&...>)
内容的提问来源于stack exchange,提问作者Matias Grioni

