中断调用方函数执行流程的函数
有没有办法使用一个类似以下宏的函数(它会在被使用的任意函数中添加一个return路径):
#define macro(boolean) if(boolean) return
void f(){
macro(2 > 1);
}
并使它变成如下形式:
T check(bool boolean){ ... }
void f(){
check(2 > 1);
}
所以f() 函数在check() 的条件为假时就返回。我找不到任何指引我走向那条路的东西。
check函数可以是任何东西,比如调用另一个返回布尔值的函数,或者如果结果不是预期就必须打断流程的东西。
之所以提出这个问题,是因为我正在处理使用宏来检查数值的遗留代码。并不是说我喜欢宏。
解决方案
通常没有一些间接的权宜之计,这是做不到的。 正如评论中所指出,这正是宏派上用场的地方。 GoogleTest 正是通过宏(ASSERT_*)来解决类似的问题,使其执行返回。
我能想到的两种可能的变通办法:1) 异常2) 协程。两者都需要额外的工作,我仍然更倾向于宏。虽然这些未必能直接解决你所面临的确切困境,但我认为值得讨论。
基于异常的方法
把函数体包裹在一个大块 try,并带一个catch-anything子句来执行返回,在 check 函数中抛出异常,希望编译器能将其优化为简单的控制流转移。
我好像在某个开源项目中见过这种模式。但通常不推荐这样做,因为它违反 CppCoreGuidelines E.3: Use exceptions for error handling only。它也会干扰正常的异常处理路径。
void check(bool cond) {
if (cond) throw 1;
}
void foo(int x) {
try {
check(cond1(x));
do_something(x);
check(cond2(x));
do_something_else(x);
check(cond3(x));
do_some_other_thing(x);
} catch (...) {
return;
}
}
基于协程的方法
通过让函数成为协程,可以实现更灵活的控制流转移。然而,这要求原始函数本身就是一个协程。此外,我也不确定这会带来多少效率开销。
// 1. The return object for our coroutine function
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 2. The Custom Awaiter that intercepts control flow
struct FlowChecker {
bool condition_met;
// If the condition is true, don't suspend; continue execution normally.
bool await_ready() const noexcept { return condition_met; }
// If the condition is false, this gets called.
// By destroying the handle, we abort the remaining execution of the caller!
void await_suspend(std::coroutine_handle<> handle) const noexcept {
handle.destroy();
}
void await_resume() const noexcept {}
};
// A helper function to make syntax cleaner
FlowChecker check(bool cond) {
return FlowChecker{cond};
}
// The caller MUST be a coroutine (returning a Task and using co_await)
Task foo(int x) {
co_await check(cond1(x));
do_something(x);
co_await check(cond2(x));
do_something_else(x * 2);
}
顺便提一下,有一个有趣的库 zpp::throwing(作者在CppCon2021的演讲)利用协程实现一个独立的、类似异常的机制。
“子程序”问题
在这种情境下一个值得注意的问题是检查是在另一个函数中执行的情况:
void checks_group1(int x) {
CHECK(cond1(x));
CHECK(cond2(x));
// ...
}
void foo(int x) {
checks_group1(x);
checks_group2(x);
}
如果这个 CHECK 是一个执行 return 的宏,它只会导致最内层的函数退出,并不会向上传播。基于异常的解决方案和基于协程的解决方案都可以处理这一点。异常会自动向上传播。对于协程,我们只需要让子例程也成为协程,并一直把 co_await 向上。
其他可能的做法:std::optional/std::expected
与其试图在子函数内部打断控制流,不如采用更地道、现代的C++做法:使用结构化类型向上传播状态。如果检查是异构的或不同操作,形成一个验证管线,std::optional 或 std::expected 提供的单子操作对清晰表达这样的管线很有帮助。
bool isEven(int x) { return x % 2 == 0; }
bool isWithinLimit(int x) { return x < 100; }
bool isNotZero(int x) { return x != 0; }
auto verify(auto predicate, std::string errMsg) {
return [predicate, errMsg](int val) -> std::expected<int, std::string> {
if (predicate(val)) return val; // Pass the value forward
return std::unexpected(errMsg); // Break the chain with this error
};
}
void foo(int x) {
auto result = std::expected<int, std::string>(x)
.and_then(verify(isNotZero, "Value cannot be zero."))
.and_then(verify(isEven, "Value must be even."))
.and_then(verify(isWithinLimit, "Value exceeds maximum limit."));
if (!result) {
std::cout << "Error: " << result.error() << "\n";
return;
}
std::cout << "Success! Ultimate processed value: " << *result << "\n";
}