noexcept-ctcheck or a few simple macros to help the compiler write noexcept code

When developing in C ++, you have to write code from time to time in which exceptions should not occur. For example, when we need to write an exception-free swap for native types or define a noexcept move statement for our class, or manually implement a nontrivial destructor.







In C ++ 11, the noexcept modifier was added to the language, which allows the developer to understand that exceptions cannot be thrown out of the function (or method) marked with noexcept. Therefore, functions with such a mark can be safely used in contexts where exceptions should not arise.







For example, if I have these types and functions:







class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
      
      





and there is a certain resources_owner



class that owns objects like first_resource



and second_resource



:







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





then I can write the resources_owner



destructor as follows:







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





In a way, noexcept in C ++ 11 made the life of a C ++ developer easier. But the current noexcept implementation in modern C ++ has one unpleasant side ...







The compiler does not help control the contents of noexcept functions and methods



Suppose that in the above example I was mistaken: for some reason, I considered release()



marked as noexcept, but in reality it is not and can throw exceptions. This means that when I write a destructor using such a throwing release()



:







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





then I beg for trouble. Sooner or later, this release()



will throw an exception and my whole application will crash due to the automatically called std::terminate()



. It will be even worse if not my application crashes, but someone else's, in which they used my library with such a problematic destructor for resources_owner



.







Or another variation of the same problem. Suppose I was not mistaken that release()



indeed marked as noexcept. It was.







It was tagged in version 1.0 of a third-party library from which I took first_resource



and release()



. And then, after several years, I upgraded to version 3.0 of this library, but in version 3.0 release()



no longer has the noexcept modifier.







So what? The new major version, they could easily break the API.







Only now, most likely, I will forget to fix the implementation of the resources_owner



destructor. And if instead of me someone else is engaged in the support of resource_owner



, who never looked into this destructor, then the changes in the release()



signature will probably go unnoticed.







Therefore, I personally do not like the fact that the compiler does not warn the programmer in any way that the programmer inside the noexcept method / function makes an exception-throwing method / function call.







It would be better if the compiler would issue such warnings.







The rescue of drowning is the work of the drowning themselves



OK, the compiler does not give any warnings. And nothing can be done about this simple developer. Do not deal with modifications of the C ++ compiler for your own needs. Especially if you have to use not one compiler, but different versions of different C ++ compilers.







Is it possible to get help from the compiler without getting into its giblets? Those. Is it possible to make some kind of tools to control the contents of noexcept methods / functions, even if the dendro-fecal method?







Can. Sloppy, but possible.







Where do the legs grow from?



The approach described in this article was tested in practice when preparing the next version of our small embedded HTTP server RESTinio .







The fact is that as RESTinio is filled with functionality, we have lost sight of the exception safety issues in several places. In particular, over time it became clear that exceptions can sometimes fly out of callbacks transferred to Asio (which should not be), as well as exceptions, in principle, can also fly out when cleaning resources.







Fortunately, in practice, these problems have never been manifested, but the technical debt has accumulated and something had to be done. And you had to do something with the code that was already written. Those. working non-noexcept code should have been converted to working noexcept code.







This was done with the help of several macros, which were arranged by code in the right places. For example, a trivial case:







 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() ); }
      
      





And here is a less trivial fragment:







 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 ); } ); } } }
      
      





Using these macros several times shook hands, pointing to places that I had inadvertently perceived as noexcept, but which were not.







So the approach described below, of course, is a self-made lisoped with square wheels, but it goes ... I mean it works.







The rest of the article will focus on an implementation that has been extracted from RESTinio code into a separate set of macros.







The essence of the approach



The essence of the approach is to pass the statement / operator (stmt), which must be checked for noexcept, into a certain macro. This macro uses static_assert(noexcept(stmt), msg)



to verify that stmt is really noexcept, and then substitutes stmt into the code.







In fact, this is:







 ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
      
      





will be replaced with something like:







 static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
      
      





Why was the choice made in favor of macros made?



In principle, one could do without macros and one could write static_assert(noexcept(...))



right in the code immediately before the actions being checked. But macros have at least a couple of virtues that tip the scales in favor of using macros specifically.







First, macros reduce code duplication. There is a comparison:







 static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
      
      





and







 ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
      
      





it is clear that with macros the main expression, i.e. release(some_resource)



can only be written once. This reduces the likelihood of the code "creeping" over time, with its accompaniment, when a correction was made in one place and forgotten in the second.







Secondly, macros and, accordingly, the checks hidden behind them can be very easily disabled. Say, if the abundance of static_assert-s began to adversely affect compilation speed (although I did not notice such an effect). Or, more significantly, when updating some third-party library, compilation errors from static_assert hidden behind macros can sprinkle directly into the river. Temporarily disabling macros can allow for a smooth update of the code, including verification macros sequentially first in one file, then in the second, then in the third, etc.







So macros, although they are an outdated and highly controversial feature in C ++, in this particular case, the life of the developer is simplified.







Main macro ENSURE_NOEXCEPT_STATEMENT



The main macro ENSURE_NOEXCEPT_STATEMENT is implemented trivially:







 #define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
      
      





It is used to verify that the called methods / functions are really noexcept and that their calls do not need to be framed by try-catch blocks. For example:







 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()); ... } ... };
      
      





In addition, there is also the ENSURE_NOT_NOEXCEPT_STATEMENT macro. It is used to make sure that an additional try-catch block is required around the call so that possible exceptions do not fly out:







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





Helper macros STATIC_ASSERT_NOEXCEPT and STATIC_ASSERT_NOT_NOEXCEPT



Unfortunately, the ENSURE_NOEXCEPT_STATEMENT and ENSURE_NOT_NOEXCEPT_STATEMENT macros can only be used for statements / statements, but not for expressions that return a value. Those. you cannot write with ENSURE_NOEXCEPT_STATEMENT like this:







 auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
      
      





Therefore, ENSURE_NOEXCEPT_STATEMENT cannot be used, for example, in loops, where you often have to write something like:







 for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
      
      





and you need to make sure that the calls to get_first()



, get_next()



, as well as the assignment of new values ​​for i, do not throw an exception.







To combat such situations, the macros STATIC_ASSERT_NOEXCEPT and STATIC_ASSERT_NOT_NOEXCEPT were written, behind which only static_assert s are hidden and nothing more. Using these macros, I can achieve the result I need in some way (the compilability of this particular fragment was not checked):







 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()) {...}
      
      





Obviously, this is not the best solution, because it leads to duplication of code and increases the risk of its "creep" with further maintenance. But as a first step, these simple macros were useful.







Noexcept-ctcheck library



When I shared this experience on my blog and on Facebook, I received a proposal to arrange the above developments in a separate library. Which was done: github now has a tiny header-only library noexcept-compile-time-check (or noexcept-ctcheck, if you save on letters) . So all of the above you can take and try. True, the names of the macros are a bit longer than used in the article. Those. NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT instead of ENSURE_NOEXCEPT_STATEMENT.







What didn't get into noexcept-ctcheck (yet?)



There is a desire to make the macro ENSURE_NOEXCEPT_EXPRESSION, which could be used like this:







 auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
      
      





In a first approximation, he might look something like this:







 #define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
      
      





But there are vague suspicions that there are some pitfalls that I have not thought about. In general, hands have not yet reached ENSURE_NOEXCEPT_EXPRESSION :(







And if you dream?



My old dream is to have a noexcept block in C ++, in which the compiler itself checks for throwing exceptions and issues warnings if exceptions can be thrown. It seems to me that this would make it easier to write exception-safe code. And not only in the obvious cases mentioned above (swap, move-operators, destructors). For example, a noexcept block could help in this situation:







 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; } }
      
      





Here, for the correctness of the code, it is very important that the actions performed inside noexcept blocks do not throw exceptions. And if the compiler can trace this, then this will be a serious help for the developer.







But perhaps a noexcept block is just a special case of a more general problem. Namely: checking the expectations of the programmer that some block of code has certain properties. Be it the absence of exceptions, the absence of side effects, the absence of recursion, data races, etc.







Reflections on this topic a couple of years ago led to the idea of ​​implies and expects attributes . This idea didn’t go further than a blog post, because while she lies away from my current interests and opportunities. But all of a sudden it will be interesting to someone and will push someone to create something more viable.







Conclusion



In this article, I tried to talk about my experience in simplifying the writing of exception-safe code. Using macros, of course, does not make the code more beautiful and compact. But it does work. And the coefficient of my restful sleep even increases such primitive macros very significantly. So, if someone else has not thought about how to control the contents of their own noexcept methods / functions, then maybe this article will inspire you to think about this topic.







And if someone found a way to simplify their life when writing noexcept code, then it would be interesting to know what this method is, in which it helps and in which it doesn’t. And how satisfied are you with what you use.








All Articles