c++ exception exception-handling c++11 noexcept

c++ - ¿Cuándo debo usar realmente noexcept?



exception exception-handling (8)

  1. Hay muchos ejemplos de funciones que sé que nunca ejecutarán, pero que el compilador no puede determinar por sí mismo. ¿Debo anexar noexcept a la declaración de función en todos estos casos?

noexcept es complicado, ya que es parte de la interfaz de funciones. Especialmente, si está escribiendo una biblioteca, su código de cliente puede depender de la propiedad noexcept . Puede ser difícil cambiarlo más tarde, ya que podría romper el código existente. Eso podría ser una preocupación menor cuando está implementando un código que solo utiliza su aplicación.

Si tiene una función que no se puede lanzar, pregúntese si le gustaría permanecer sin noexcept o ¿eso restringiría las implementaciones futuras? Por ejemplo, es posible que desee introducir la comprobación de errores de los argumentos ilegales al lanzar excepciones (por ejemplo, para pruebas unitarias), o puede depender de otro código de biblioteca que podría cambiar su especificación de excepción. En ese caso, es más seguro ser conservador y omitir noexcept .

Por otro lado, si está seguro de que la función nunca debería ejecutarse y es correcto que forma parte de la especificación, debe declararla no noexcept . Sin embargo, tenga en cuenta que el compilador no podrá detectar violaciones de noexcept si su implementación cambia.

  1. ¿Para qué situaciones debo tener más cuidado con el uso de noexcept y en qué situaciones puedo salirme con la noexcept implícita (false)?

Hay cuatro clases de funciones en las que deberías concentrarte porque probablemente tendrán el mayor impacto:

  1. Mover operaciones (mover operador de asignación y mover constructores)
  2. operaciones de intercambio
  3. desasignadores de memoria (operador eliminar, operador eliminar [])
  4. destructores (aunque estos son implícitamente noexcept(true) menos que los haga noexcept(false) )

Estas funciones generalmente deben ser noexcept , y es muy probable que las implementaciones de la biblioteca puedan hacer uso de la propiedad noexcept . Por ejemplo, std::vector puede usar operaciones de movimiento no lanzadas sin sacrificar fuertes garantías de excepción. De lo contrario, tendrá que recurrir a copiar elementos (como lo hizo en C ++ 98).

Este tipo de optimización está en el nivel algorítmico y no se basa en optimizaciones del compilador. Puede tener un impacto significativo, especialmente si los elementos son costosos de copiar.

  1. ¿Cuándo puedo realmente esperar observar una mejora en el rendimiento después de usar noexcept? En particular, dé un ejemplo de código para el cual un compilador de C ++ pueda generar un mejor código de máquina después de la adición de noexcept.

La ventaja de noexcept contra ninguna especificación de excepción o throw() es que el estándar permite a los compiladores más libertad cuando se trata de desenrollar pilas. Incluso en el caso de throw() , el compilador tiene que desenrollar completamente la pila (y debe hacerlo en el orden inverso exacto de las construcciones de objetos).

En el caso de noexcept , por otro lado, no se requiere hacer eso. No hay ningún requisito de que la pila deba ser desenrollada (pero al compilador todavía se le permite hacerlo). Esa libertad permite una mayor optimización del código, ya que reduce la sobrecarga de poder siempre desenrollar la pila.

La pregunta relacionada sobre noexcepto, desenvolvimiento de pila y rendimiento entra en más detalles sobre la sobrecarga cuando se requiere el desenrollado de pila.

También recomiendo el libro de Scott Meyers "Effective Modern C ++", "Artículo 14: Declare funciona no excepto si no emiten excepciones" para leer más.

La palabra clave noexcept se puede aplicar de manera adecuada a muchas firmas de funciones, pero no estoy seguro de cuándo debería considerar su uso en la práctica. Según lo que he leído hasta ahora, la adición de último minuto de noexcept parece abordar algunos problemas importantes que surgen cuando se lanzan los constructores de movimientos. Sin embargo, todavía no puedo dar respuestas satisfactorias a algunas preguntas prácticas que me llevaron a leer más sobre noexcept en primer lugar.

  1. Hay muchos ejemplos de funciones que sé que nunca ejecutarán, pero que el compilador no puede determinar por sí mismo. ¿Debo anexar noexcept a la declaración de función en todos estos casos?

    Tener que pensar si debo o no agregar noexcept después de cada declaración de función reduciría enormemente la productividad del programador (y, francamente, sería un dolor de cabeza). ¿Para qué situaciones debo tener más cuidado con el uso de noexcept y en qué situaciones puedo noexcept(false) con la noexcept(false) implícita noexcept(false) ?

  2. ¿Cuándo puedo esperar de manera realista observar una mejora en el rendimiento después de usar noexcept ? En particular, dé un ejemplo de código para el cual un compilador de C ++ pueda generar un mejor código de máquina después de la adición de noexcept .

    Personalmente, me preocupo por la noexcept debido a la mayor libertad proporcionada al compilador para aplicar ciertos tipos de optimizaciones de forma segura. ¿Los compiladores modernos se aprovechan de noexcept de esta manera? Si no, ¿puedo esperar que algunos de ellos lo hagan en un futuro cercano?


¿Cuándo puedo realistamente excepto observar una mejora en el rendimiento después de usar noexcept ? En particular, dé un ejemplo de código para el cual un compilador de C ++ pueda generar un mejor código de máquina después de la adición de noexcept.

Um nunca Nunca es un tiempo? Nunca.

noexcept es para las optimizaciones de rendimiento del compilador de la misma manera que const es para las optimizaciones de rendimiento del compilador. Es decir, casi nunca.

noexcept se usa principalmente para permitir que "usted" detecte en tiempo de compilación si una función puede lanzar una excepción. Recuerde: la mayoría de los compiladores no emiten un código especial para excepciones a menos que realmente arroje algo. Por noexcept tanto, noexcept no es una cuestión de dar al compilador sugerencias sobre cómo optimizar una función tanto como darle sugerencias sobre cómo usar una función.

Plantillas como move_if_noexcept detectarán si el constructor de movimiento está definido con noexcept y devolverá una const& lugar de un && del tipo si no lo es. Es una forma de decir que te muevas si es muy seguro hacerlo.

En general, debe usar noexcept cuando piense que realmente será útil hacerlo. Algunos códigos tomarán rutas diferentes si is_nothrow_constructible es verdadero para ese tipo. Si está utilizando un código que lo hará, no dude en no noexcept los constructores apropiados.

En resumen: utilícelo para constructores de movimiento y construcciones similares, pero no piense que debe volverse loco con él.


Hay muchos ejemplos de funciones que sé que nunca ejecutarán, pero que el compilador no puede determinar por sí mismo. ¿Debo anexar noexcept a la declaración de función en todos estos casos?

Cuando dices "Sé que [ellos] nunca lanzarán", te refieres a que al examinar la implementación de la función sabes que la función no se lanzará. Creo que ese enfoque está al revés.

Es mejor considerar si una función puede lanzar excepciones para ser parte del diseño de la función: tan importante como la lista de argumentos y si un método es un mutador (... const ). Declarar que "esta función nunca produce excepciones" es una restricción en la implementación. Omitirlo no significa que la función pueda generar excepciones; significa que la versión actual de la función y todas las versiones futuras pueden generar excepciones. Es una restricción que dificulta la implementación. Pero algunos métodos deben tener la restricción de ser útiles en la práctica; lo más importante es que se pueden llamar desde los destructores, pero también para la implementación del código de "retroceso" en los métodos que proporcionan la garantía de excepción sólida.


Como sigo repitiendo estos días: la semántica primero .

Agregar noexcept , noexcept(true) y noexcept(false) es, ante todo, semántica. Solo condiciona incidentalmente una serie de optimizaciones posibles.

Como programador que lee el código, la presencia de noexcept es similar a la de const : me ayuda a asimilar mejor lo que puede o no suceder. Por lo tanto, vale la pena dedicar algún tiempo a pensar si sabe o no si la función se realizará. Como recordatorio, cualquier tipo de asignación de memoria dinámica puede lanzar.

Bien, ahora a las posibles optimizaciones.

Las optimizaciones más obvias se realizan realmente en las bibliotecas. C ++ 11 proporciona una serie de características que permiten saber si una función es noexcept o no, y la implementación de la biblioteca estándar utilizará esas características para favorecer noexcept operaciones noexcept en los objetos definidos por el usuario que manipulan, si es posible. Como mover la semántica .

El compilador solo puede eliminar un poco de grasa (quizás) de los datos de manejo de excepciones, ya que debe tener en cuenta el hecho de que puede haber mentido. Si una función marcada como noexcept se lanza, entonces se llama a std::terminate .

Estas semánticas fueron elegidas por dos razones:

  • se beneficia inmediatamente de noexcept incluso cuando las dependencias ya no lo usan (compatibilidad con versiones anteriores)
  • permitiendo la especificación de noexcept al llamar a funciones que teóricamente pueden lanzar pero que no se espera para los argumentos dados

Creo que es demasiado pronto para dar una respuesta de "mejores prácticas" para esto ya que no ha habido suficiente tiempo para usarlo en la práctica. Si se preguntara esto sobre los especificadores de lanzamiento justo después de que salieran, las respuestas serían muy diferentes a las de ahora.

Tener que pensar si debo o no agregar noexcept después de cada declaración de función reduciría en gran medida la productividad del programador (y, francamente, sería una molestia).

Bueno, entonces úselo cuando sea obvio que la función nunca se ejecutará.

¿Cuándo puedo esperar de manera realista observar una mejora en el rendimiento después de usar noexcept ? [...] Personalmente, me preocupo por la noexcept debido a la mayor libertad otorgada al compilador para aplicar ciertos tipos de optimizaciones de forma segura.

Parece que las mayores ganancias de optimización provienen de las optimizaciones del usuario, no de compiladores debido a la posibilidad de verificar noexcept y sobrecargarlo. La mayoría de los compiladores siguen un método de manejo de excepciones sin penalización si usted no lanza, por lo que dudo que cambie mucho (o cualquier cosa) en el nivel de código de máquina de su código, aunque tal vez reduzca el tamaño binario eliminando el manejo código.

El uso de noexcept en los 4 grandes (constructores, asignación, no destructores, ya que son noexcept ) probablemente causará las mejores mejoras, ya que noexcept verificaciones de noexcept son "comunes" en el código de la plantilla, como en los contenedores estándar. Por ejemplo, std::vector no usará el movimiento de su clase a menos que esté marcado como no noexcept (o el compilador puede deducirlo de otra manera).


En palabras Bjarne:

Cuando la terminación es una respuesta aceptable, una excepción no detectada lo logrará porque se convierte en una llamada de terminate () (§13.5.2.5). Además, un especificador noexcept (§13.5.1.1) puede hacer que ese deseo sea explícito.

Los sistemas exitosos tolerantes a fallas son multinivel. Cada nivel hace frente a tantos errores como puede sin contorsionarse demasiado y deja el resto a niveles más altos. Las excepciones apoyan esa vista. Además, terminate() admite esta vista al proporcionar un escape si el mecanismo de manejo de excepciones está dañado o si se ha utilizado de forma incompleta, por lo que no se detectan excepciones. De manera similar, noexcept proporciona un escape simple para los errores en los que tratar de recuperarse parece inviable.

double compute(double x) noexcept; { string s = "Courtney and Anya"; vector<double> tmp(10); // ... }

El constructor vectorial puede no adquirir memoria para sus diez dobles y lanzar un std::bad_alloc . En ese caso, el programa termina. Termina incondicionalmente invocando std::terminate() (§30.4.1.3). No invoca a los destructores desde las funciones de llamada. Se define por la implementación si se invocan los destructores de los ámbitos entre el throw y el noexcept (por ejemplo, para s en compute ()). El programa está a punto de terminar, por lo que no deberíamos depender de ningún objeto de todos modos. Al agregar un especificador de no noexcept , indicamos que nuestro código no fue escrito para hacer frente a un lanzamiento.


Esto realmente hace una diferencia (potencialmente) enorme al optimizador en el compilador. En realidad, los compiladores han tenido esta característica durante años a través de la declaración empty throw () después de la definición de una función, así como las extensiones de propiedad. Les puedo asegurar que los compiladores modernos aprovechan este conocimiento para generar un mejor código.

Casi todas las optimizaciones en el compilador utilizan algo llamado "gráfico de flujo" de una función para razonar sobre lo que es legal. Un gráfico de flujo consiste en lo que generalmente se denomina "bloques" de la función (áreas de código que tienen una sola entrada y una única salida) y bordes entre los bloques para indicar a dónde puede saltar el flujo. Noexcept altera el diagrama de flujo.

Usted pidió un ejemplo específico. Considere este código:

void foo(int x) { try { bar(); x = 5; // other stuff which doesn''t modify x, but might throw } catch(...) { // don''t modify x } baz(x); // or other statement using x }

El diagrama de flujo para esta función es diferente si la bar está etiquetada como noexcept (no hay forma de que la ejecución salte entre el final de la bar y la instrucción catch). Cuando está etiquetado como no noexcept , el compilador está seguro de que el valor de x es 5 durante la función baz: se dice que el bloque x = 5 "domina" el bloque baz (x) sin el borde de la bar() a la instrucción catch. Entonces puede hacer algo llamado "propagación constante" para generar un código más eficiente. Aquí, si baz está en línea, las declaraciones que usan x también pueden contener constantes y entonces lo que solía ser una evaluación en tiempo de ejecución se puede convertir en una evaluación en tiempo de compilación, etc.

De todos modos, respuesta corta: noexcept permite al compilador generar un gráfico de flujo más estricto, y el gráfico de flujo se usa para razonar sobre todo tipo de optimizaciones comunes del compilador. Para un compilador, las anotaciones de usuario de esta naturaleza son impresionantes. El compilador intentará descifrar estas cosas, pero generalmente no puede (la función en cuestión podría estar en otro archivo de objeto no visible para el compilador o usar de forma transitiva alguna función que no está visible), o cuando lo hace, hay alguna Excepción trivial que puede ser lanzada de la que ni siquiera está al tanto, por lo que no puede etiquetarla implícitamente como noexcept (la asignación de memoria puede generar bad_alloc, por ejemplo).


noexcept puede mejorar dramáticamente el rendimiento de algunas operaciones. Esto no sucede en el nivel de generación de código de máquina por el compilador, pero seleccionando el algoritmo más efectivo: como lo mencionaron otros, realice esta selección usando la función std::move_if_noexcept . Por ejemplo, el crecimiento de std::vector (por ejemplo, cuando llamamos reserve ) debe proporcionar una fuerte garantía de seguridad de excepción. Si sabe que el constructor de movimientos de T no lanza, solo puede mover cada elemento. De lo contrario debe copiar todos los T s. Esto ha sido descrito en detalle en este post .