noexcept-ctcheck或一些简单的宏来帮助编译器编写noexcept代码

在使用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_resourcesecond_resource类的对象:


 class resources_owner { first_resource first_resource_; second_resource second_resource_; ... }; 

然后我可以按如下方式编写resources_owner析构函数:


 resources_owner::~resources_owner() noexcept { //  release()   ,    . release(first_resource_); //    close()   ,  //   try-catch. try{ close(second_resource_); } catch(...) {} } 

从某种意义上说,C ++ 11中的noexcept使C ++开发人员的生活更加轻松。 但是现代C ++中当前的noexcept实现有一个令人讨厌的方面...


编译器无助于控制noexcept函数和方法的内容


假设在上面的示例中我弄错了:出于某种原因,我考虑将release()标记为noexcept,但实际上不是,并且可以引发异常。 这意味着当我使用这种抛出release()编写析构函数时:


 resources_owner::~resources_owner() noexcept { release(first_resource_); //  try-catch   ... } 

那我就麻烦了 迟早该release()都会引发异常,并且由于自动调用std::terminate() ,我的整个应用程序将崩溃。 如果不是我的应用程序崩溃,而是其他人的崩溃,那将更加糟糕,因为他们使用我的库时遇到了一个有问题的针对resources_owner析构函数。


或同一个问题的另一个变体。 假设我没有把release()确实标记为noexcept。 是的


我在first_resourcerelease()的第三方库的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 { // An exception from logger/msg_builder shouldn't prevent // a call to close(). restinio::utils::log_error_noexcept( m_logger, std::move(msg_builder) ); RESTINIO_ENSURE_NOEXCEPT_CALL( close() ); } 

这是一个不那么琐碎的片段:


 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; //  swap  noexcept,    . ENSURE_NOEXCEPT_STATEMENT(swap(a.first_data_part_, b.first_data_part_)); ENSURE_NOEXCEPT_STATEMENT(swap(a.second_data_part_, b.second_data_part_)); ... } ... void clean() noexcept { //  clean()  noexcept,    . ENSURE_NOEXCEPT_STATEMENT(first_data_part_.clean()); ENSURE_NOEXCEPT_STATEMENT(second_data_part_.clean()); ... } ... }; 

此外,还有ENSURE_NOT_NOEXCEPT_STATEMENT宏。 它用于确保在调用周围需要一个额外的try-catch块,以便不会出现可能的异常:


 class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try { //  release   noexcept,  try-catch     //      . ENSURE_NOT_NOEXCEPT_STATEMENT(release(resource_)); } catch(...) {} ... } ... }; 

辅助宏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() { //   . one_container_.modify(); // ,   . ,      . //         try. noexcept { current_age_.increment(); } //    ,      . try { another_container_.modify(); ... } catch(...) { noexcept { //  ,     . current_age_.decrement(); one_container_.rollback_modifications(); } throw; } } 

在这里,为了确保代码的正确性,在noexcept块内执行的操作不要引发异常非常重要。 而且,如果编译器可以跟踪此内容,那么这对开发人员将是一个严重的帮助。


但是也许noexcept块只是更普遍问题的特例。 即:检查程序员对某些代码块具有某些属性的期望。 无论是没有例外,没有副作用,没有递归,数据竞争等。


几年前对这个话题的反思导致了蕴涵和期望属性观念 。 这个想法没有比博客文章更进一步,因为 而她却远离了我目前的兴趣和机会。 但是突然之间,这对于某人将变得很有趣,并且某人将推动创造更可行的东西。


结论


在本文中,我试图谈谈我在简化异常安全代码的编写方面的经验。 当然,使用宏不会使代码更美观,更紧凑。 但这确实有效。 即使是这样的原始宏,也极大地增加了我的宁静睡眠的系数。 因此,如果其他人没有考虑过如何控制自己的noexcept方法/函数的内容,那么本文可能会激发您思考该主题。


而且,如果有人在编写noexcept代码时发现了一种简化生活的方法,那么了解这种方法的意义,实用性和实用性会很有趣。 您对自己的使用方式有多满意。

Source: https://habr.com/ru/post/zh-CN466849/


All Articles