在使用C ++开发时,您必须不时编写代码,在这种情况下不应发生异常。 例如,当我们需要为本机类型编写无异常交换或为我们的类定义noexcept move语句或手动实现非平凡的析构函数时。
在C ++ 11中,将noexcept修饰符添加到该语言中,这使开发人员可以理解标记为noexcept的函数(或方法)的异常不能被抛出。 因此,带有这种标记的功能可以在不应出现异常的情况下安全使用。
例如,如果我具有以下类型和功能:
class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
并且有一个resources_owner
类拥有诸如first_resource
和second_resource
类的对象:
class resources_owner { first_resource first_resource_; second_resource second_resource_; ... };
然后我可以按如下方式编写resources_owner
析构函数:
resources_owner::~resources_owner() noexcept {
从某种意义上说,C ++ 11中的noexcept使C ++开发人员的生活更加轻松。 但是现代C ++中当前的noexcept实现有一个令人讨厌的方面...
编译器无助于控制noexcept函数和方法的内容
假设在上面的示例中我弄错了:出于某种原因,我考虑将release()
标记为noexcept,但实际上不是,并且可以引发异常。 这意味着当我使用这种抛出release()
编写析构函数时:
resources_owner::~resources_owner() noexcept { release(first_resource_);
那我就麻烦了 迟早该release()
都会引发异常,并且由于自动调用std::terminate()
,我的整个应用程序将崩溃。 如果不是我的应用程序崩溃,而是其他人的崩溃,那将更加糟糕,因为他们使用我的库时遇到了一个有问题的针对resources_owner
析构函数。
或同一个问题的另一个变体。 假设我没有把release()
确实标记为noexcept。 是的
我在first_resource
和release()
的第三方库的1.0版中对其进行了标记。 然后,几年后,我升级到了该库的3.0 release()
,但在3.0 release()
不再具有noexcept修饰符。
恩,什么? 在新的主要版本中,他们可以轻松地破坏API。
直到现在,最有可能的是,我将忘记修复resources_owner
析构函数的实现。 如果不是我而是其他人参与了resource_owner
的支持,而从未研究过此析构函数,那么release()
签名中的更改可能不会引起注意。
因此,我个人不喜欢这样的事实,即编译器不会以任何方式警告程序员noexcept方法/函数内部的程序员进行了异常抛出方法/函数调用。
如果编译器发出这样的警告会更好。
抢救溺水是溺水本身的工作
好的,编译器不会发出任何警告。 这个简单的开发人员无法做任何事情。 不要根据自己的需要处理C ++编译器的修改。 特别是如果您不必使用一个编译器,而是使用不同C ++编译器的不同版本。
是否有可能从编译器那里获得帮助而无需陷入内脏? 即 是否可以使用某种工具来控制noexcept方法/函数的内容,即使使用dendro-fecal方法也是如此?
可以的 马虎,但可能。
腿从哪里长出来?
在准备我们的小型嵌入式HTTP服务器RESTinio的下一版本时,已对本文中描述的方法进行了实践测试。
事实是,由于RESTinio充满了功能,因此我们在很多地方都没有注意到异常安全问题。 特别是随着时间的流逝,很明显异常有时可以从发送给Asio的回调中飞出(不应该这样),并且原则上清理资源时可以飞出异常。
幸运的是,在实践中,这些问题从未表现出来,但是技术债务已经积累,因此必须采取一些措施。 而且您必须对已经编写的代码进行一些处理。 即 非正常工作代码应转换为正常工作代码。
这是在几个宏的帮助下完成的,这些宏由代码安排在正确的位置。 例如,一个简单的情况:
template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept {
这是一个不那么琐碎的片段:
void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } }
使用这些宏几次握手,指向我无意间被视为无例外的地方,但实际上并非如此。
因此,以下所述的方法当然是用方形轮毂自制的自制齿轮,但它确实可行。
在本文的进一步内容中,我们将讨论与RESTinio代码隔离到单独的一组宏中的实现。
方法的本质
该方法的本质是将需要检查的语句/运算符(stmt)传递给某个宏。 此宏使用static_assert(noexcept(stmt), msg)
来验证stmt确实为noexcept,然后在代码中替换stmt。
本质上,这是:
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
将被替换为:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
为什么要做出选择以支持宏?
原则上,一个人可以没有宏,一个人可以在检查动作之前立即在代码中编写static_assert(noexcept(...))
。 但是宏至少有两个优点,可以扩展规模以利于专门使用宏。
首先,宏减少了代码重复。 有一个比较:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
和
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
很明显,使用宏的主要表达即 release(some_resource)
只能写入一次。 当在一个位置进行校正而在第二个位置忘记校正时,这会减少伴随时间的代码“爬行”的可能性。
其次,可以很容易地禁用宏以及相应地隐藏在其后的检查。 说,如果大量的static_assert -s开始对编译速度产生不利影响(尽管我没有注意到这种影响)。 或者,更重要的是,在更新某些第三方库时,隐藏在宏后面的static_assert中的编译错误可能直接散布在河流中。 暂时禁用宏可以允许代码的平滑更新,包括先在一个文件中依次验证宏,然后在第二个文件中依次验证,然后在第三个文件中依次类推。
因此,尽管宏是C ++中过时且引起争议的功能,但在这种情况下,可以简化开发人员的工作。
主宏ENSURE_NOEXCEPT_STATEMENT
主要宏ENSURE_NOEXCEPT_STATEMENT的实现很简单:
#define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
它用于验证被调用的方法/函数确实没有异常,并且它们的调用不需要由try-catch块构成。 例如:
class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap;
此外,还有ENSURE_NOT_NOEXCEPT_STATEMENT宏。 它用于确保在调用周围需要一个额外的try-catch块,以便不会出现可能的异常:
class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try {
辅助宏STATIC_ASSERT_NOEXCEPT和STATIC_ASSERT_NOT_NOEXCEPT
不幸的是,ENSURE_NOEXCEPT_STATEMENT和ENSURE_NOT_NOEXCEPT_STATEMENT宏只能用于语句,而不能用于返回值的表达式。 即 您不能像这样使用ENSURE_NOEXCEPT_STATEMENT进行书写:
auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
因此,例如在经常需要编写以下内容的循环中,不能使用ENSURE_NOEXCEPT_STATEMENT:
for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
并且您需要确保对get_first()
, get_next()
的调用以及为我分配的新值不会引发异常。
为了应对这种情况,编写了STATIC_ASSERT_NOEXCEPT和STATIC_ASSERT_NOT_NOEXCEPT宏,其后仅隐藏了static_assert,仅此而已。 使用这些宏,我可以通过某种方式获得所需的结果(未检查此特定片段的编译):
STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
显然,这不是最佳解决方案,因为 它导致代码重复,并随着进一步的维护而增加了其“蠕变”的风险。 但是作为第一步,这些简单的宏被证明是有用的。
Noexcept-ctcheck库
当我在博客和Facebook上分享这种经验时,我收到了一个建议,将上述开发安排在一个单独的库中。 这样做是:github现在有一个很小的仅标头的库noexcept-compile-time-check(或者,如果保存为字母,则为noexcept-ctcheck) 。 因此,以上所有内容都可以尝试。 的确,宏的名称比本文中使用的名称长一些。 即 NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT而不是ENSURE_NOEXCEPT_STATEMENT。
noexcept-ctcheck没有得到什么?
希望创建宏ENSURE_NOEXCEPT_EXPRESSION,该宏可以这样使用:
auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
初步近似,他可能看起来像这样:
#define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
但是有模糊的怀疑是我没有想到过一些陷阱。 通常,手还没有达到ENSURE_NOEXCEPT_EXPRESSION :(
如果你做梦?
我的旧梦想是在C ++中获得一个noexcept块,在该块中,编译器本身会检查是否抛出异常,并在可能抛出异常的情况下发出警告。 在我看来,这将使编写异常安全代码更加容易。 而且不仅在上述明显的情况下(交换,移动运算符,析构函数)。 例如,在这种情况下,noexcept块可能会有所帮助:
void modify_some_complex_data() {
在这里,为了确保代码的正确性,在noexcept块内执行的操作不要引发异常非常重要。 而且,如果编译器可以跟踪此内容,那么这对开发人员将是一个严重的帮助。
但是也许noexcept块只是更普遍问题的特例。 即:检查程序员对某些代码块具有某些属性的期望。 无论是没有例外,没有副作用,没有递归,数据竞争等。
几年前对这个话题的反思导致了蕴涵和期望属性的观念 。 这个想法没有比博客文章更进一步,因为 而她却远离了我目前的兴趣和机会。 但是突然之间,这对于某人将变得很有趣,并且某人将推动创造更可行的东西。
结论
在本文中,我试图谈谈我在简化异常安全代码的编写方面的经验。 当然,使用宏不会使代码更美观,更紧凑。 但这确实有效。 即使是这样的原始宏,也极大地增加了我的宁静睡眠的系数。 因此,如果其他人没有考虑过如何控制自己的noexcept方法/函数的内容,那么本文可能会激发您思考该主题。
而且,如果有人在编写noexcept代码时发现了一种简化生活的方法,那么了解这种方法的意义,实用性和实用性会很有趣。 您对自己的使用方式有多满意。