为何不能像‘给所有对象加const’那样,随意给所有函数加上noexcept?
其实这个问题得从几个角度掰扯清楚——先看看大家常说的「给一切加const」「给一切加constexpr」是啥情况,再对比noexcept的特殊性,最后聊聊为啥const的「不可逆」好像没noexcept那么棘手。
首先说const和constexpr的现状:
- 我没找到专门讲「const all the things」的视频,但Cpp核心指南里明确有Con.3:默认给指针/引用参数加const;
- 「constexpr all the things」倒是有不少分享,但
constexpr和前两者不一样——它不会改变函数的接口,只是给函数加了「能在编译期执行」的额外功能,就算后来你把constexpr去掉,调用方的代码也不会直接炸掉。
但到了noexcept这里,大家的态度就保守太多了,核心原因就是:noexcept是函数接口的核心组成部分!
- 尤其是写库的时候,客户端代码会直接依赖函数的
noexcept属性——比如有些代码会用std::move_if_noexcept,或者基于「这个函数不会抛异常」做性能优化、逻辑分支。后来要是你想把noexcept去掉,很可能会直接break现有客户端的代码,而且这种破坏可能是隐性的、难排查的。 - Cpp核心指南里的E.12也明确说:只有当函数绝对不可能抛出异常,或者抛出异常是完全不可接受的情况,才应该用
noexcept——这和大家脑补的「给一切加noexcept」完全不是一个尺度。Scott Meyers也强调过:给函数加了noexcept就相当于给了一个不可逆的承诺,几乎没法回头。
这时候有人会问:那const不也是接口的一部分吗?比如把参数声明成const&,后来想改成非const,不也会break客户端?
但实际用下来,「到处加const」好像更经得起时间考验,就算需要调整,也有兼容的空间:
比如举个例子:C++11早期,有人写了这么个函数,用来遍历容器做只读操作:
void work(auto const& r) { for (auto const& e : r) { // 只读逻辑 } }
当时觉得加const&很合理——毕竟这个函数本来就只需要读,为啥要给它写权限?但到了C++20,要是调用方传的是filter_view这种视图,就会编译失败:
std::vector<int> v; work(v); // 没问题 work(v | std::views::transform(std::identity{})); // 没问题 work(v | std::views::filter(std::identity{})); // 编译失败!
因为filter_view在遍历的时候需要自身做一些修改,而const&不允许它这么做。但解决办法很简单:把函数参数改成auto&&就行,而且原来传容器、传transform_view的代码还能正常跑——也就是说,就算当初加了const,后来调整也不会完全炸掉所有客户端。
但noexcept就没这么灵活了:要是你当初拍脑袋给函数加了noexcept,后来发现实现不得不抛出异常(比如依赖的底层库变了,或者需求加了可能抛异常的逻辑),想去掉noexcept的话,所有依赖这个属性的客户端代码都会出问题,而且这种问题可能是运行时的、难定位的,不像const的问题是编译期就能直接发现的。
所以总结下来:
const的接口承诺,就算需要调整,往往有兼容的方式,对现有代码的破坏性相对小;noexcept的承诺是完全不可逆的,它绑定的是函数的异常行为,客户端会基于这个承诺写代码,一旦反悔,代价极高;- 这也是为啥大家对「给一切加const」相对宽容,但对「给一切加noexcept」极其谨慎的原因。
内容来源于stack exchange

