noexcept-ctcheck o algunas macros simples para ayudar al compilador a escribir código noexcept

Al desarrollar en C ++, debe escribir código de vez en cuando en el que no se produzcan excepciones. Por ejemplo, cuando necesitamos escribir un intercambio sin excepciones para tipos nativos o definir una declaración de movimiento noexcept para nuestra clase, o implementar manualmente un destructor no trivial.


En C ++ 11, el modificador noexcept se agregó al lenguaje, lo que permite al desarrollador comprender que no se pueden eliminar las excepciones de la función marcada con noexcept. Por lo tanto, las funciones con dicha marca se pueden usar de forma segura en contextos donde no deberían surgir excepciones.


Por ejemplo, si tengo estos tipos y funciones:


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

y hay una cierta clase resources_owner que posee objetos como first_resource y second_resource :


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

entonces puedo escribir el destructor resources_owner siguiente manera:


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

En cierto modo, noexcept en C ++ 11 hizo la vida de un desarrollador de C ++ más fácil. Pero la implementación actual sin excepción en C ++ moderno tiene un lado desagradable ...


El compilador no ayuda a controlar el contenido de las funciones y métodos noexcept


Supongamos que en el ejemplo anterior me equivoqué: por alguna razón, consideré que release() marcado como noexcept, pero en realidad no lo es y puede arrojar excepciones. Esto significa que cuando escribo un destructor usando una release() :


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

entonces suplico por problemas. Tarde o temprano, esta release() arrojará una excepción y toda mi aplicación se bloqueará debido a que se llama automáticamente std::terminate() . Será aún peor si no se bloquea mi aplicación, sino la de otra persona, en la que usaron mi biblioteca con un destructor tan problemático para resources_owner .


O otra variación del mismo problema. Supongamos que no me equivoqué al decir que release() marcado como noexcept. Fue.


Fue etiquetado en la versión 1.0 de una biblioteca de terceros de la que tomé first_resource y release() . Y luego, después de varios años, actualicé a la versión 3.0 de esta biblioteca, pero en la versión 3.0 release() ya no tiene un modificador noexcept.


Bueno que? La nueva versión principal, podrían romper fácilmente la API.


Solo ahora, lo más probable, me olvidaré de arreglar la implementación del destructor resources_owner . Y si en mi lugar, alguien más está involucrado en el soporte de resource_owner , que nunca investigó este destructor, entonces los cambios en la firma release() probablemente pasarán desapercibidos.


Por lo tanto, personalmente no me gusta el hecho de que el compilador no advierte al programador de ninguna manera que el programador dentro del método / función noexcept realiza una llamada a un método / función de lanzamiento de excepciones.


Sería mejor si el compilador emitiera tales advertencias.


El rescate del ahogamiento es obra del ahogado.


OK, el compilador no da ninguna advertencia. Y no se puede hacer nada con este simple desarrollador. No trate con modificaciones del compilador de C ++ para sus propias necesidades. Especialmente si tiene que usar no un compilador, sino diferentes versiones de diferentes compiladores de C ++.


¿Es posible obtener ayuda del compilador sin entrar en sus menudillos? Es decir ¿Es posible hacer algún tipo de herramientas para controlar el contenido de métodos / funciones no exceptuadas, incluso si se trata del método dendrofescal?


Usted puede Descuidado, pero posible.


¿De dónde crecen las piernas?


El enfoque descrito en este artículo se probó en la práctica al preparar la próxima versión de nuestro pequeño servidor HTTP integrado RESTinio .


El hecho es que a medida que RESTinio está lleno de funcionalidad, hemos perdido de vista los problemas de seguridad de excepción en varios lugares. En particular, con el tiempo se hizo evidente que las excepciones a veces pueden salirse de las devoluciones de llamadas transferidas a Asio (lo que no debería ser así), así como las excepciones, en principio, pueden salir volando cuando se limpian los recursos.


Afortunadamente, en la práctica, estos problemas nunca se han manifestado, pero la deuda técnica se ha acumulado y hay que hacer algo al respecto. Y tenías que hacer algo con el código que ya estaba escrito. Es decir el código de trabajo sin excepción debe convertirse en código de trabajo sin excepción.


Esto se hizo con la ayuda de varias macros, que se organizaron por código en los lugares correctos. Por ejemplo, un caso trivial:


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

Y aquí hay un fragmento menos trivial:


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

El uso de estas macros me dio la mano varias veces, señalando lugares que había percibido inadvertidamente como no, excepto que no lo eran.


Por lo tanto, el enfoque que se describe a continuación, por supuesto, es un liso hecho a sí mismo con ruedas cuadradas, pero funciona ... Quiero decir, funciona.


Más adelante en el artículo, discutiremos la implementación que se aisló del código RESTinio en un conjunto separado de macros.


La esencia del enfoque.


La esencia del enfoque es pasar la declaración / operador (stmt), que debe verificarse para noexcept, en una macro determinada. Esta macro usa static_assert(noexcept(stmt), msg) para verificar que stmt es realmente noexcept, y luego sustituye stmt en el código.


Básicamente, esto es:


 ENSURE_NOEXCEPT_STATEMENT(release(some_resource)); 

será reemplazado por algo como:


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

¿Por qué se hizo la elección a favor de las macros?


En principio, uno podría prescindir de macros y podría escribir static_assert(noexcept(...)) directamente en el código inmediatamente antes de las acciones que se verifican. Pero las macros tienen al menos un par de virtudes que inclinan la balanza a favor del uso de macros específicamente.


Primero, las macros reducen la duplicación de código. Hay una comparación:


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

y


 ENSURE_NOEXCEPT_STATEMENT(release(some_resource)); 

Está claro que con las macros la expresión principal, es decir release(some_resource) solo se puede escribir una vez. Esto reduce la probabilidad de que el código se "arrastre" con el tiempo, con su acompañamiento, cuando se realizó una corrección en un lugar y se olvidó en el segundo.


En segundo lugar, las macros y, en consecuencia, los controles ocultos detrás de ellos pueden deshabilitarse muy fácilmente. Digamos, si la abundancia de static_assert-s comenzó a afectar negativamente la velocidad de compilación (aunque no noté tal efecto). O, lo que es más importante, al actualizar alguna biblioteca de terceros, los errores de compilación de static_assert ocultos detrás de las macros pueden rociarse directamente con el río. La desactivación temporal de las macros puede permitir una actualización sin problemas del código, incluidas las macros de verificación secuencialmente primero en un archivo, luego en el segundo, luego en el tercero, etc.


Entonces, las macros, aunque son una característica obsoleta y muy controvertida en C ++, en este caso particular, la vida del desarrollador se simplifica.


Macro principal ENSURE_NOEXCEPT_STATEMENT


La macro principal ENSURE_NOEXCEPT_STATEMENT se implementa trivialmente:


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

Se utiliza para verificar que los métodos / funciones a los que se llama no son, a excepción, y que sus llamadas no necesitan ser enmarcadas por bloques try-catch. Por ejemplo:


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

Además, también existe la macro ENSURE_NOT_NOEXCEPT_STATEMENT. Se utiliza para garantizar que se requiera un bloqueo adicional de try-catch alrededor de la llamada para que las posibles excepciones no salgan volando:


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

Macros auxiliares STATIC_ASSERT_NOEXCEPT y STATIC_ASSERT_NOT_NOEXCEPT


Desafortunadamente, las macros ENSURE_NOEXCEPT_STATEMENT y ENSURE_NOT_NOEXCEPT_STATEMENT solo se pueden usar para declaraciones / declaraciones, pero no para expresiones que devuelven un valor. Es decir no puedes escribir con ENSURE_NOEXCEPT_STATEMENT así:


 auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params)); 

Por lo tanto, ENSURE_NOEXCEPT_STATEMENT no se puede usar, por ejemplo, en bucles donde a menudo tiene que escribir algo como:


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

y debe asegurarse de que las llamadas get_first() , get_next() , así como la asignación de nuevos valores para i no arrojen una excepción.


Para combatir tales situaciones, se escribieron las macros STATIC_ASSERT_NOEXCEPT y STATIC_ASSERT_NOT_NOEXCEPT, detrás de las cuales solo se ocultan static_assert s y nada más. Usando estas macros, puedo lograr el resultado que necesito de alguna manera (no se verificó la compilación de este fragmento en particular):


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

Obviamente, esta no es la mejor solución, porque conduce a la duplicación de código y aumenta el riesgo de su "deslizamiento" con más mantenimiento. Pero como primer paso, estas macros simples resultaron útiles.


Biblioteca Noexcept-ctcheck


Cuando compartí esta experiencia en mi blog y en Facebook, recibí una propuesta para organizar los desarrollos anteriores en una biblioteca separada. Lo que se hizo: github ahora tiene una pequeña biblioteca de solo encabezado noexcept-compile-time-check (o noexcept-ctcheck, si guarda en letras) . Entonces, todo lo anterior puede tomar y probar. Es cierto que los nombres de las macros son un poco más largos de lo que se usa en el artículo. Es decir NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT en lugar de ENSURE_NOEXCEPT_STATEMENT.


Lo que no entró en noexcept-ctcheck (¿todavía?)


Existe el deseo de crear la macro ENSURE_NOEXCEPT_EXPRESSION, que podría usarse así:


 auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params)); 

En una primera aproximación, podría verse así:


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

Pero hay vagas sospechas de que hay algunas trampas en las que no he pensado. En general, las manos aún no han alcanzado ENSURE_NOEXCEPT_EXPRESSION :(


¿Y si sueñas?


Mi antiguo sueño es obtener un bloque noexcept en C ++ en el que el compilador mismo compruebe si se lanzan excepciones y emite advertencias si se pueden lanzar excepciones. Me parece que esto facilitaría la escritura de código seguro de excepciones. Y no solo en los casos obvios mencionados anteriormente (intercambio, operadores de movimiento, destructores). Por ejemplo, un bloque noexcept podría ayudar en esta situación:


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

Aquí, para la corrección del código, es muy importante que las acciones realizadas dentro de los bloques noexcept no arrojen excepciones. Y si el compilador puede rastrear esto, entonces esto será de gran ayuda para el desarrollador.


Pero quizás un bloque noexcept es solo un caso especial de un problema más general. A saber: verificar las expectativas del programador de que algún bloque de código tiene ciertas propiedades. Ya sea la ausencia de excepciones, la ausencia de efectos secundarios, la ausencia de recursividad, carreras de datos, etc.


Reflexiones sobre este tema hace un par de años llevaron a la idea de implica y espera atributos . Esta idea no fue más allá de la publicación del blog, porque mientras ella se aleja de mis intereses y oportunidades actuales. Pero de repente será interesante para alguien y alguien presionará para crear algo más viable.


Conclusión


En este artículo, traté de hablar sobre mi experiencia en la simplificación de la escritura de código seguro de excepción. El uso de macros, por supuesto, no hace que el código sea más hermoso y compacto. Pero funciona. E incluso esas macros primitivas aumentan significativamente el coeficiente de mi sueño reparador. Por lo tanto, si alguien más no ha pensado en cómo controlar el contenido de sus propios métodos / funciones sin excepción, entonces quizás este artículo lo inspire a pensar sobre este tema.


Y si alguien encuentra una manera de simplificar su vida al escribir código sin excepción, entonces sería interesante saber qué es este método, en qué ayuda y en qué no. Y qué tan satisfecho está con lo que usa.

Source: https://habr.com/ru/post/466849/


All Articles