¿Cuándo debería usar realmente noexcept?

509

La noexceptpalabra clave se puede aplicar adecuadamente a muchas firmas de funciones, pero no estoy seguro de cuándo debería considerar usarla en la práctica. Según lo que he leído hasta ahora, la adición de última hora de noexceptparece abordar algunos problemas importantes que surgen cuando los constructores de movimientos lanzan. Sin embargo, todavía no puedo proporcionar respuestas satisfactorias a algunas preguntas prácticas que me llevaron a leer más sobre el tema noexcepten primer lugar.

  1. Hay muchos ejemplos de funciones que sé que nunca lanzarán, pero que el compilador no puede determinar por sí solo. ¿Debo adjuntar noexcepta la declaración de función en todos estos casos?

    Tener que pensar si necesito agregar o no noexceptdespués de cada declaración de función reduciría en gran medida la productividad del programador (y, francamente, sería un fastidio). ¿Para qué situaciones debo tener más cuidado con el uso noexcepty para qué situaciones puedo evitar lo implícito 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 que un compilador de C ++ pueda generar un mejor código de máquina después de la adición de noexcept.

    Personalmente, me preocupo noexceptpor la mayor libertad brindada al compilador para aplicar de forma segura ciertos tipos de optimizaciones. ¿Se aprovechan los compiladores modernos de noexceptesta manera? Si no, ¿puedo esperar que algunos de ellos lo hagan en el futuro cercano?

puntero vacío
fuente
40
El código que usa move_if_nothrow(o whatchamacallit) verá una mejora en el rendimiento si hay un movimiento de excepción.
R. Martinho Fernandes
55
Es move_if_noexcept.
Nikos

Respuestas:

180

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

Tener que pensar si necesito agregar o no noexceptdespués de cada declaración de función reduciría en gran medida la productividad del programador (y, francamente, sería un dolor).

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

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

Parece que las mayores ganancias de optimización provienen de las optimizaciones de los usuarios, no de las compiladoras debido a la posibilidad de verificar noexcepty sobrecargarlas. La mayoría de los compiladores siguen un método de manejo de excepciones sin penalización si no lanza, por lo que dudo que cambie mucho (o algo) en el nivel de código de máquina de su código, aunque tal vez reduzca el tamaño binario al eliminar el código de manejo

El uso noexcepten los cuatro grandes (constructores, asignación, no destructores como ya están noexcept) probablemente causará las mejores mejoras ya que las noexceptcomprobaciones son 'comunes' en el código de plantilla, como en los stdcontenedores. Por ejemplo, std::vectorno usará el movimiento de su clase a menos que esté marcado noexcept(o el compilador puede deducirlo de otra manera).

Pubby
fuente
66
Creo que el std::terminatetruco todavía obedece al modelo de costo cero. Es decir, da la casualidad de que el rango de instrucciones dentro de las noexceptfunciones se asigna a llamar std::terminatesi throwse usa en lugar del desbobinador de pila. Por lo tanto, dudo que tenga más gastos generales que el seguimiento regular de excepciones.
Matthieu M.
44
@Klaim Vea esto: stackoverflow.com/a/10128180/964135 En realidad, simplemente no debe tirarse, pero noexceptesto lo garantiza.
Pubby
3
"Si se noexceptlanza una función, entonces std::terminateparece que implicaría una pequeña cantidad de sobrecarga" ... No, esto debería implementarse al no generar tablas de excepción para dicha función, que el despachador de excepciones debe detectar y luego rescatar.
Potatoswatter
8
El manejo de excepciones de @Pubby C ++ generalmente se realiza sin gastos generales, excepto las tablas de salto que asignan las direcciones de sitios de llamadas potencialmente lanzadas a los puntos de entrada del controlador. Eliminar esas tablas es lo más cercano posible a eliminar completamente el manejo de excepciones. La única diferencia es el tamaño del archivo ejecutable. Probablemente no valga la pena mencionar nada.
Potatoswatter
26
"Bueno, entonces úsalo cuando sea obvio que la función nunca se lanzará". Estoy en desacuerdo. noexceptes parte de la interfaz de la función ; no debe agregarlo solo porque su implementación actual no se produce. No estoy seguro de la respuesta correcta a esta pregunta, pero estoy bastante seguro de que la forma en que se comporta hoy su función no tiene nada que ver con eso ...
Nemo
133

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

Sumar noexcept, noexcept(true)y noexcept(false)es ante todo sobre semántica. Solo incidentalmente condiciona varias optimizaciones posibles.

Como programador de lectura de códigos, la presencia de noexceptes similar a la de const: me ayuda a entender mejor lo que puede o no suceder. Por lo tanto, vale la pena pasar un tiempo pensando si sabe o no si la función se lanzará. Como recordatorio, cualquier tipo de asignación de memoria dinámica puede arrojar.


Bien, ahora a las posibles optimizaciones.

Las optimizaciones más obvias se realizan realmente en las bibliotecas. C ++ 11 proporciona una serie de rasgos que permiten saber si una función es noexcepto no, y la implementación de la Biblioteca estándar utilizará esos rasgos para favorecer las noexceptoperaciones en los objetos definidos por el usuario que manipulan, si es posible. Tal como mover la semántica .

El compilador solo puede eliminar un poco de grasa (tal vez) de los datos de manejo de excepciones, porque tiene que tener en cuenta el hecho de que puede haber mentido. Si una función marcada noexceptlanza, entonces std::terminatese llama.

Estas semánticas fueron elegidas por dos razones:

  • beneficiarse de inmediato noexceptincluso cuando las dependencias ya no lo usan (compatibilidad con versiones anteriores)
  • permitiendo la especificación de noexceptcuando se llaman funciones que teóricamente pueden arrojar, pero que no se espera para los argumentos dados
Matthieu M.
fuente
2
Tal vez soy ingenuo, pero me imagino que una función que invoca solo noexceptfunciones no necesitaría hacer nada especial, porque cualquier excepción que pueda surgir se dispara terminateantes de llegar a este nivel. Esto difiere mucho de tener que lidiar y propagar una bad_allocexcepción.
8
Sí, es posible definir noexcepto de la manera que sugiere, pero esa sería una característica realmente inutilizable. Muchas funciones pueden lanzarse si ciertas condiciones no se mantienen, y no podría llamarlas incluso si sabe que se cumplen las condiciones. Por ejemplo, cualquier función que pueda arrojar std :: invalid_argument.
tr3w
3
@MatthieuM. Un poco tarde para una respuesta, pero sin embargo. Las funciones marcadas como noexcept pueden llamar a otras funciones que pueden lanzar, la promesa es que esta función no emitirá una excepción, es decir, ¡solo tienen que manejar la excepción ellos mismos!
Honf
44
Voté esta respuesta hace mucho tiempo, pero después de haberla leído y pensado un poco más, tengo un comentario / pregunta. "Move semántica" es el único ejemplo que he visto a alguien dar donde noexceptes claramente útil / una buena idea. Estoy empezando a pensar que la construcción de movimientos, la asignación de movimientos y el intercambio son los únicos casos que existen ... ¿Conoces otros?
Nemo
55
@Nemo: en la biblioteca estándar, es posiblemente la única, sin embargo, presenta un principio que puede reutilizarse en otros lugares. Una operación de movimiento es una operación que temporalmente pone un estado en el "limbo", y solo cuando es así noexcept, alguien puede usarlo con confianza en un dato al que se pueda acceder después. Podría ver que esta idea se usa en otros lugares, pero la biblioteca estándar es bastante delgada en C ++ y creo que solo se usa para optimizar copias de elementos.
Matthieu M.
77

Esto realmente hace una diferencia (potencialmente) enorme para el optimizador en el compilador. Los compiladores han tenido esta característica durante años a través de la declaración throw () vacía después de una definición de función, así como 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 usan algo llamado "diagrama de flujo" de una función para razonar sobre lo que es legal. Un gráfico de flujo consta de lo que generalmente se llaman "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 hacia dónde puede saltar el flujo. Noexcept altera el diagrama de flujo.

Solicitaste un ejemplo específico. Considera 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 gráfico de flujo para esta función es diferente si barestá etiquetado noexcept(no hay forma de que la ejecución salte entre el final de bary la instrucción catch). Cuando se etiqueta como 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 bar()la instrucción catch.

Luego 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 luego 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, la respuesta corta: noexceptpermite que el compilador genere un gráfico de flujo más ajustado, 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á resolver esto, pero por lo general no puede (la función en cuestión podría estar en otro archivo de objetos no visible para el compilador o usar transitivamente alguna función que no es visible), o cuando lo hace, hay alguna excepción trivial que podría producirse de la que ni siquiera es consciente, por lo que no puede etiquetarla implícitamente como noexcept(asignar memoria podría arrojar bad_alloc, por ejemplo).

Terry Mahaffey
fuente
3
¿Esto realmente hace una diferencia en la práctica? El ejemplo está inventado porque nada antes x = 5puede lanzar. Si esa parte del trybloque sirviera para algo, el razonamiento no sería válido.
Potatoswatter
77
Diría que hace una diferencia real en la optimización de funciones que contienen bloques try / catch. El ejemplo que di, aunque artificial, no es exhaustivo. El punto más importante es que noexcept (como la instrucción throw () anterior) ayuda a que la compilación genere un gráfico de flujo más pequeño (menos bordes, menos bloques), que es una parte fundamental de muchas optimizaciones que hace.
Terry Mahaffey
¿Cómo puede el compilador reconocer que el código puede generar una excepción? ¿Se considera el acceso a la matriz como una posible excepción?
Tomas Kubes
3
@ qub1n Si el compilador puede ver el cuerpo de la función, puede buscar throwdeclaraciones explícitas u otras cosas como newesas. Si el compilador no puede ver el cuerpo, entonces debe confiar en la presencia o ausencia de noexcept. El acceso a una matriz simple no genera excepciones (C ++ no tiene verificación de límites), por lo que no, el acceso a la matriz por sí solo no haría que el compilador piense que una función arroja excepciones. (El acceso fuera del límite es UB, no es una excepción garantizada.)
cdhowie
@cdhowie " debe basarse en la presencia o ausencia de noexcept " o presencia de throw()en C ++ pre-noexcept
curiousguy
57

noexceptpuede mejorar dramáticamente el rendimiento de algunas operaciones. Esto no sucede en el nivel de generación de código de máquina por parte del compilador, sino seleccionando el algoritmo más efectivo: como otros mencionaron, usted hace 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 Tel constructor de movimientos no arroja, simplemente puede mover cada elemento. De lo contrario, debe copiar todos los Ts. Esto se ha descrito en detalle en esta publicación .

Andrzej
fuente
44
Anexo: Eso significa que si define constructores de movimiento o operadores de asignación de movimiento, agréguelos noexcept(si corresponde). Las funciones de miembro de movimiento definidas implícitamente se han noexceptagregado automáticamente (si corresponde).
mucaho
33

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

Um, nunca? Nunca es un tiempo? Nunca.

noexceptes para las optimizaciones de rendimiento del compilador de la misma manera que constpara las optimizaciones de rendimiento del compilador. Es decir, casi nunca.

noexceptse utiliza principalmente para permitir que "usted" detecte en tiempo de compilación si una función puede generar una excepción. Recuerde: la mayoría de los compiladores no emiten código especial para excepciones a menos que realmente arroje algo. Así que noexceptno es una cuestión de dar los consejos del compilador acerca de cómo optimizar una función tanto como dar usted consejos sobre cómo utilizar una función.

Las plantillas como move_if_noexceptdetectarán si el constructor de movimiento está definido noexcepty devolverá un en const&lugar de un &&tipo si no lo está. Es una forma de decir que se mueva si es muy seguro hacerlo.

En general, debe usarlo noexceptcuando crea que realmente será útil hacerlo. Algún código tomará diferentes caminos si is_nothrow_constructiblees cierto para ese tipo. Si está utilizando un código que lo hará, no dude en utilizar noexceptlos constructores adecuados.

En resumen: úselo para mover constructores y construcciones similares, pero no sienta que tiene que volverse loco con él.

Nicol Bolas
fuente
13
Estrictamente, move_if_noexceptno devolverá una copia, devolverá una referencia de valor constante en lugar de una referencia de valor. En general, eso hará que la persona que llama haga una copia en lugar de un movimiento, pero move_if_noexceptno está haciendo la copia. De lo contrario, gran explicación.
Jonathan Wakely
12
+1 Jonathan. Cambiar el tamaño de un vector, por ejemplo, moverá los objetos en lugar de copiarlos si el constructor de movimiento es noexcept. De modo que "nunca" no es cierto.
mfontanini
44
Es decir, el compilador será generar un mejor código en esa situación. OP está pidiendo un ejemplo para el cual el compilador pueda generar una aplicación más optimizada. Este parece ser el caso (aunque no es una optimización del compilador ).
mfontanini
77
@mfontanini: el compilador solo genera un código mejor porque el compilador se ve obligado a compilar una ruta de código diferente . Es solamente funciona porque std::vectorestá escrito para forzar al compilador para compilar diferente código. No se trata de que el compilador detecte algo; se trata de que el código de usuario detecte algo.
Nicol Bolas
3
La cuestión es que parece que no puedo encontrar la "optimización del compilador" en la cita al comienzo de su respuesta. Como dijo @ChristianRau, si el compilador genera un código más eficiente, no importa cuál sea el origen de esa optimización. Después de todo, el compilador está generando un código más eficiente, ¿no? PD: Nunca dije que fuera una optimización del compilador, incluso dije "No es una optimización del compilador".
mfontanini
22

En palabras de Bjarne ( The C ++ Programming Language, 4th Edition , página 366):

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

Los sistemas exitosos de tolerancia a fallas son multinivel. Cada nivel hace frente a tantos errores como sea posible sin deformarse demasiado y deja el resto a niveles más altos. Las excepciones respaldan esa opinión. Además, terminate()admite esta vista al proporcionar un escape si el mecanismo de manejo de excepciones en sí está dañado o si se ha utilizado de forma incompleta, dejando así excepciones sin detectar. Del mismo modo, noexceptproporciona un escape simple para los errores donde intentar recuperarlos parece inviable.

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

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

Saurav Sahu
fuente
2
¿Tienes una fuente para esta cita?
Anton Golov
55
@AntonGolov "El lenguaje de programación C ++, 4ª edición" pág. 366
Rusty Shackleford
1
Esto me suena como si en realidad debería agregar un noexceptcada vez, excepto que explícitamente quiero ocuparme de las excepciones. Seamos realistas, la mayoría de las excepciones son tan improbables y / o tan fatales que el rescate es apenas razonable o posible. Por ejemplo, en el ejemplo citado, si la asignación falla, la aplicación difícilmente podrá continuar funcionando correctamente.
Neonit
21
  1. Hay muchos ejemplos de funciones que sé que nunca lanzarán, pero que el compilador no puede determinar por sí solo. ¿Debo agregar no, excepto a la declaración de función en todos estos casos?

noexceptes 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 noexceptpropiedad. Puede ser difícil cambiarlo más tarde, ya que podría romper el código existente. Eso podría ser menos preocupante cuando implementa código que solo usa su aplicación.

Si tiene una función que no puede lanzar, pregúntese si le gustaría quedarse noexcepto eso restringiría futuras implementaciones? Por ejemplo, es posible que desee introducir la comprobación de errores de argumentos ilegales lanzando 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 lanzarse y es correcto que sea parte de la especificación, debe declararla noexcept. Sin embargo, tenga en cuenta que el compilador no podrá detectar violaciones noexceptsi su implementación cambia.

  1. ¿En qué situaciones debo tener más cuidado con el uso de noexcept y en qué situaciones puedo evitar el noexcept implícito (falso)?

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

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

Estas funciones generalmente deberían serlo noexcept, y lo más probable es que las implementaciones de la biblioteca puedan hacer uso de la noexceptpropiedad. Por ejemplo, std::vectorpuede usar operaciones de movimiento sin lanzamiento sin sacrificar fuertes garantías de excepción. De lo contrario, tendrá que recurrir a la copia de 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 caros de copiar.

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

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

En el noexceptcaso, por otro lado, no es necesario hacer eso. No hay ningún requisito de que la pila tenga que desenrollarse (pero el compilador aún puede 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 noexcept, desbobinado y rendimiento de la pila entra en más detalles sobre la sobrecarga cuando se requiere el desbobinado de la pila.

También recomiendo el libro de Scott Meyers "Effective Modern C ++", "Item 14: Declarar funciones sin excepción si no emiten excepciones" para leer más.

Philipp Claßen
fuente
Aún así, tendría mucho más sentido si se implementaran excepciones en C ++ como en Java, donde se marca un método que puede arrojarse con una throwspalabra clave en lugar de noexceptnegativo. Yo simplemente no puede conseguir un poco de C ++ opciones de diseño ...
doc
Lo nombraron noexceptporque throwya estaba ocupado. En pocas palabras, throwse puede usar casi de la manera que mencionas, excepto que estropearon el diseño, por lo que se volvió casi inútil, incluso perjudicial. Pero estamos atascados con eso ahora, ya que eliminarlo sería un cambio radical con pocos beneficios. Así noexceptes básicamente throw_v2.
AnorZaken
¿Cómo throwno es útil?
curioso
@curiousguy "throw" en sí (para lanzar excepciones) es útil, pero "throw" como especificador de excepción ha quedado en desuso y en C ++ 17 incluso se ha eliminado. Para conocer los motivos por los que los especificadores de excepción no son útiles, consulte esta pregunta: stackoverflow.com/questions/88573/…
Philipp Claßen
1
@ PhilippClaßen El throw()especificador de excepción no proporcionó la misma garantía que nothrow?
curioso
17

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

Cuando dice "Sé que [nunca] lanzarán", quiere decir que al examinar la implementación de la función, sabe que la función no lanzará. Creo que ese enfoque es de adentro hacia afuera.

Es mejor considerar si una función puede generar excepciones para formar 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 arroja excepciones" es una restricción para 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 arrojar excepciones. Es una restricción que dificulta la implementación. Pero algunos métodos deben tener la restricción para ser prácticamente útiles; lo más importante, para que puedan llamarse desde los destructores, pero también para la implementación del código de "reversión" en los métodos que proporcionan la garantía de excepción fuerte.

Raedwald
fuente
Esta es la mejor respuesta con diferencia. Está garantizando a los usuarios de su método, que es otra forma de decir que está restringiendo su implementación para siempre (sin cambio de ruptura). Gracias por la perspectiva esclarecedora.
AnorZaken
Vea también una pregunta relacionada con Java .
Raedwald