绝大多数情况下应 throw,前提是非法参数属于可恢复的运行期错误(如负超时、非法枚举值),而非编程错误;后者应用 assert 或崩溃。

setter 里参数非法,该 throw 还是 return false?
直接结论:绝大多数情况下,应该 throw,但前提是这个“非法”属于可恢复的运行期错误(比如传入负数给非负字段),且调用方有合理预期去捕获并响应;如果只是编程错误(比如 nullptr 传给非空引用参数),就该用 assert 或直接崩溃,而不是抛异常。
哪些非法参数算“可恢复错误”?
关键看参数是否依赖外部输入或配置——这类值在编译期无法验证,运行时才暴露问题,且上层逻辑可能重试、降级或提示用户。常见例子:
-
set_timeout_ms(-1):负超时无意义,但不是代码写错了,可能是配置解析出错或前端传参失误 -
set_log_level("DEBUG2"):字符串枚举值拼错,应拒绝但不终止进程 -
set_max_connections(1000000):数值合法但超出系统资源上限,需反馈而非静默截断
这些场景下,throw std::invalid_argument 比返回 false 更明确:它强制调用方处理,不会被忽略;同时携带上下文(如错误消息),比布尔返回码信息量大得多。
为什么不该在 setter 里返回错误码?
因为 setter 语义上是“设置一个状态”,不是“尝试执行一个可能失败的操作”。返回 bool 或 int 会带来三个实际问题:
立即学习“C++免费学习笔记(深入)”;
- 调用方必须显式检查每个 setter 调用结果,代码迅速变得冗长脆弱(
if (!obj.set_x(x)) return false;) - 多个 setter 连续调用时,中间某一个失败,对象可能处于半更新状态,难以回滚
- 错误码本身不携带原因,调试时只能靠日志或断点,而异常能直接带出
"set_port(): port 0 is reserved"
更糟的是,一旦习惯返回错误码,团队会不自觉地把所有错误都扁平化成 0/-1,丢失类型和层次——比如内存不足和端口冲突混为一谈。
析构函数、noexcept 和性能陷阱
如果你决定抛异常,必须确保两点:
- 所有持有资源的成员变量都满足异常安全(即构造失败时已构造成员能正确析构),否则栈展开过程会泄漏资源
- 不要在
noexcept函数里抛异常,否则触发std::terminate;尤其注意operator=或移动赋值中调用了带异常的 setter - 避免在 setter 中做耗时操作(如磁盘 I/O、网络请求),异常本不是为慢路径设计的;真有这类需求,应拆成独立的
validate_and_apply()方法
性能上不用过度担心:现代编译器对未触发的 try 块几乎零开销;真正代价来自异常实际抛出后的栈展开和对象析构——所以别在高频循环里校验非法参数,应在入口层(如配置加载、RPC 解包)集中做。
最常被忽略的一点:异常类型要具体。别用 throw std::runtime_error("bad value"),优先用标准异常如 std::invalid_argument 或自定义派生类,方便上层按类型精确捕获,而不是全靠 catch(...) 吞掉再埋雷。


















